Skip to content

fix: Pre-merge stateDelta before onUserMessageCallback#1117

Open
YuqiGuo105 wants to merge 1 commit intogoogle:mainfrom
YuqiGuo105:fix/statedelta-onusermessagecallback-1099
Open

fix: Pre-merge stateDelta before onUserMessageCallback#1117
YuqiGuo105 wants to merge 1 commit intogoogle:mainfrom
YuqiGuo105:fix/statedelta-onusermessagecallback-1099

Conversation

@YuqiGuo105
Copy link
Copy Markdown
Contributor

@YuqiGuo105 YuqiGuo105 commented Apr 7, 2026

Summary

Fixes #1099

When runner.runAsync(userId, sessionId, message, config, stateDelta) is called, the stateDelta map was not merged into the session before pluginManager.onUserMessageCallback was executed. This caused plugins to see null when reading caller-provided state entries.

Root Cause

In Runner.runAsyncImpl, the execution order was:

  1. Create initialContext using the original session (no stateDelta)
  2. Call onUserMessageCallback(initialContext, message) - plugins see stale state
  3. Call appendNewMessageToSession(...) which writes stateDelta via EventActions
  4. Fetch updated session and call beforeRunCallback - plugins see correct state

Fix

Pre-merge stateDelta into the session's in-memory state before creating the initial context. This is safe because the session is already a copy from getSession(). Persistence still happens via EventActions in appendNewMessageToSession, making this an idempotent optimization.


Workflow Comparison

BEFORE (Bug)

runAsync(userId, sessionId, message, stateDelta={transactionId: "TXN-0042"})
    |
    |-- getSession()
    |       returns session copy, state = {}
    |
    |-- initialContext = new Context(session)
    |       session.state() = {}   <-- empty
    |
    |-- onUserMessageCallback(initialContext)
    |       state.get("transactionId") = null   <-- BUG
    |
    |-- appendNewMessageToSession(..., stateDelta)
    |       writes stateDelta to session.state()
    |       persists to storage
    |
    |-- getSession()
    |       returns new copy, state = {transactionId: "TXN-0042"}
    |
    |-- beforeRunCallback(newContext)
            state.get("transactionId") = "TXN-0042"   <-- OK

AFTER (Fixed)

runAsync(userId, sessionId, message, stateDelta={transactionId: "TXN-0042"})
    |
    |-- getSession()
    |       returns session copy, state = {}
    |
    |-- PRE-MERGE stateDelta into session copy
    |       session.state() = {transactionId: "TXN-0042"}
    |
    |-- initialContext = new Context(session)
    |       session.state() = {transactionId: "TXN-0042"}   <-- contains stateDelta
    |
    |-- onUserMessageCallback(initialContext)
    |       state.get("transactionId") = "TXN-0042"   <-- FIXED
    |
    |-- appendNewMessageToSession(..., stateDelta)
    |       idempotent write (same values)
    |       persists to storage
    |
    |-- getSession()
    |       returns new copy, state = {transactionId: "TXN-0042"}
    |
    |-- beforeRunCallback(newContext)
            state.get("transactionId") = "TXN-0042"   <-- unchanged

Changes

  • Runner.java: Pre-merge stateDelta before creating initialContext (2-line comment + 3-line code)
  • RunnerTest.java: Added onUserMessageCallback_withStateDelta_seesMergedState test

Testing

All 56 RunnerTest tests pass, including the new test and existing beforeRunCallback_withStateDelta_seesMergedState.

The stateDelta was not merged into session state before onUserMessageCallback was invoked, causing plugins to see null values when reading caller-provided state entries.

This fix pre-merges stateDelta into the session's in-memory state before creating the InvocationContext. Since the session is already a copy from getSession(), this is safe and does not affect the persistence path which still happens via EventActions in appendNewMessageToSession.

Added test: onUserMessageCallback_withStateDelta_seesMergedState
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runner.runAsync applies stateDelta too late, causing missing(outdated) context in onUserMessageCallback

1 participant