Problem
When a background task (async subagent / async tool) completes, WakeupDispatcher wakes the idle parent session to run another reasoning round. In runtimes that isolate agent state by (userId, sessionId), this wakeup round fails with a provider 400:
{"code":20015,"message":"No user query found in messages."}
Observed in logs: the wakeup round starts with user=null and an almost-empty context (contextMsgs=1, only the system prompt), because state is loaded from the wrong slot.
Root cause
MessageBus.enqueueWakeup(userId, sessionId, agentId) already stores userId in the wakeup queue entry:
default Mono<Void> enqueueWakeup(String userId, String sessionId, String agentId) {
return queuePush("agentscope:wakeups",
Map.of("userId", userId != null ? userId : "",
"sessionId", sessionId,
"agentId", agentId))
.then(publish("agentscope:wakeup_signal", Map.of()));
}
But WakeupDispatcher.dispatch reads only sessionId and agentId, then calls target.runWakeup(sessionId) — the userId is dequeued and discarded. WakeupTarget#runWakeup(String sessionId) has no parameter to receive it.
HarnessGateway.runWakeup therefore builds the RuntimeContext without a userId:
RuntimeContext rtc = RuntimeContext.builder()
.sessionId(sessionId)
.put("gateKey", gateKey)
.put("outboundAddress", ...)
.build(); // no userId
The wakeup round loads state from the anonymous (null, sessionId) slot instead of the original (userId, sessionId) slot — the original conversation (incl. the user's query) is absent, the model receives no user message, the provider returns 400.
runStream propagates userId correctly (if (ctx.userId() != null && !ctx.isBlank()) rtcBuilder.userId(ctx.userId())); only the wakeup path is broken.
Impact
Any runtime that isolates agent state by (userId, sessionId) and relies on the wakeup mechanism (async subagents, async tools, future team messages / schedulers) cannot complete a wakeup-driven round. Single-tenant / anonymous runtimes are unaffected (the missing userId is a no-op there).
Proposed fix
Backward-compatible default method on WakeupTarget:
WakeupTarget: add default Mono<Msg> runWakeup(String userId, String sessionId) delegating to the legacy overload; mark runWakeup(String sessionId) @Deprecated.
WakeupDispatcher.dispatch: read userId from the payload and call runWakeup(userId, sessionId).
HarnessGateway: override the new overload and set rtc.userId(...) when non-blank (matching runStream).
Existing WakeupTarget implementers stay source/binary compatible. Happy to open a PR.
Problem
When a background task (async subagent / async tool) completes,
WakeupDispatcherwakes the idle parent session to run another reasoning round. In runtimes that isolate agent state by(userId, sessionId), this wakeup round fails with a provider 400:Observed in logs: the wakeup round starts with
user=nulland an almost-empty context (contextMsgs=1, only the system prompt), because state is loaded from the wrong slot.Root cause
MessageBus.enqueueWakeup(userId, sessionId, agentId)already storesuserIdin the wakeup queue entry:But
WakeupDispatcher.dispatchreads onlysessionIdandagentId, then callstarget.runWakeup(sessionId)— theuserIdis dequeued and discarded.WakeupTarget#runWakeup(String sessionId)has no parameter to receive it.HarnessGateway.runWakeuptherefore builds theRuntimeContextwithout a userId:The wakeup round loads state from the anonymous
(null, sessionId)slot instead of the original(userId, sessionId)slot — the original conversation (incl. the user's query) is absent, the model receives no user message, the provider returns 400.runStreampropagates userId correctly (if (ctx.userId() != null && !ctx.isBlank()) rtcBuilder.userId(ctx.userId())); only the wakeup path is broken.Impact
Any runtime that isolates agent state by
(userId, sessionId)and relies on the wakeup mechanism (async subagents, async tools, future team messages / schedulers) cannot complete a wakeup-driven round. Single-tenant / anonymous runtimes are unaffected (the missing userId is a no-op there).Proposed fix
Backward-compatible default method on
WakeupTarget:WakeupTarget: adddefault Mono<Msg> runWakeup(String userId, String sessionId)delegating to the legacy overload; markrunWakeup(String sessionId)@Deprecated.WakeupDispatcher.dispatch: readuserIdfrom the payload and callrunWakeup(userId, sessionId).HarnessGateway: override the new overload and setrtc.userId(...)when non-blank (matchingrunStream).Existing
WakeupTargetimplementers stay source/binary compatible. Happy to open a PR.