Skip to content

[BUG]: WakeupDispatcher drops userId, causing wakeup rounds to lose the original conversation in multi-tenant runtimes #2000

Description

@hansiweicn-debug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions