You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a subagent invokes A2A and its reply arrives after the subagent terminates, the result is silently dropped (see A2ATaskResultHandler:78-82 and siblings). PendingA2ATask already carries PrimarySessionId — the routing data exists, just unused on the late path.
The existing a2aAwaiter already prevents most late arrivals — subagents block on outstanding A2A before publishing their result (SubagentRunner.cs:283-300). The fold-back triggers when the awaiter times out, the subagent is cancelled, or the primary cancels the subagent's parent task. This is the safety net, not the hot path.
(Tool-profile scoping for this new handler — and the rest of the in-process roles — is split out to #425 and will land after this issue. Until then, the new handler uses the unfiltered tool set, matching today's UserMessageHandler surface.)
Design
In the 4 A2A handlers (A2ATaskResultHandler, A2ATaskStatusHandler, A2ATaskErrorHandler, InputRequiredHandler), after tracker.TryRemove(correlationId) returns a PendingA2ATask:
If PendingA2ATask.SubagentSessionId is set AND SubagentManager.IsActive(subagentSessionId) → route to subagent (current path; existing a2aAwaiter handles the still-running case).
Else if PendingA2ATask.PrimarySessionId is set → fold back to primary (new path).
Else → log Warning and drop (current behavior).
Fold-back path:
Stash the reply payload to working memory under notifications/a2a/{subagentTaskId}/{kind} (where kind is one of result | status | error | input-required) in the primary's namespace.
Append a one-line entry to notifications/index so list_working_memory surfaces pending notifications.
LateA2ANotificationHandler (new, registered in RockBot.Agent/Program.cs) receives the message, loads the primary session, and runs AgentLoopRunner.RunAsync (unfiltered tool set; see note above re: Per-invocation tool profiles for in-process roles #425) with a prompt of the form: A late {kind} arrived from A2A peer {peer} for your completed subagent {name}. The payload is in working memory at {key}. Read it, decide whether to act on it, and inform the user if relevant. The prompt requires the model to surface the context to the user.
Why a message + handler rather than a synchronous call: the bus handles queueing, multiple late arrivals serialize into separate primary turns without colliding with an in-progress turn, and the original A2A handler returns promptly.
Primary-agent directive update: add a line telling the primary to check notifications/ on each turn as defense-in-depth.
Implementation order
src/RockBot.Subagent/SubagentManager.cs — add IsActive(subagentSessionId) accessor backed by existing SubagentEntry registry
src/RockBot.A2A/LateA2ANotificationMessage.cs (new) — record + NotificationKind enum
src/RockBot.A2A/LateA2ANotificationHandler.cs (new) + registration in src/RockBot.Agent/Program.cs
No ROCKBOT_RABBITMQ_HOST-gated integration test changes needed — this is in-process routing, not transport
Risks
Existing a2aAwaiter already prevents most late arrivals. This fix is the safety net.
Notification storms. Several late replies arriving back-to-back create N primary turns. Acceptable for v1; bus serializes them. If noisy in practice, add debounce later (handler reads notifications/index and consolidates pending entries into one turn).
User-side surprise. The primary speaks without a user prompt. The LateA2ANotificationHandler prompt requires the model to state the source so it is not opaque.
Cancellation correctness. If PrimarySessionId resolves to nothing (CLI one-shot, disconnected user), the handler drops cleanly with a Warning. Matches existing "session not found" patterns.
Background
When a subagent invokes A2A and its reply arrives after the subagent terminates, the result is silently dropped (see
A2ATaskResultHandler:78-82and siblings).PendingA2ATaskalready carriesPrimarySessionId— the routing data exists, just unused on the late path.The existing
a2aAwaiteralready prevents most late arrivals — subagents block on outstanding A2A before publishing their result (SubagentRunner.cs:283-300). The fold-back triggers when the awaiter times out, the subagent is cancelled, or the primary cancels the subagent's parent task. This is the safety net, not the hot path.(Tool-profile scoping for this new handler — and the rest of the in-process roles — is split out to #425 and will land after this issue. Until then, the new handler uses the unfiltered tool set, matching today's
UserMessageHandlersurface.)Design
In the 4 A2A handlers (
A2ATaskResultHandler,A2ATaskStatusHandler,A2ATaskErrorHandler,InputRequiredHandler), aftertracker.TryRemove(correlationId)returns aPendingA2ATask:PendingA2ATask.SubagentSessionIdis set ANDSubagentManager.IsActive(subagentSessionId)→ route to subagent (current path; existinga2aAwaiterhandles the still-running case).PendingA2ATask.PrimarySessionIdis set → fold back to primary (new path).Fold-back path:
notifications/a2a/{subagentTaskId}/{kind}(wherekindis one ofresult | status | error | input-required) in the primary's namespace.notifications/indexsolist_working_memorysurfaces pending notifications.LateA2ANotificationMessage:LateA2ANotificationHandler(new, registered inRockBot.Agent/Program.cs) receives the message, loads the primary session, and runsAgentLoopRunner.RunAsync(unfiltered tool set; see note above re: Per-invocation tool profiles for in-process roles #425) with a prompt of the form: A late {kind} arrived from A2A peer {peer} for your completed subagent {name}. The payload is in working memory at{key}. Read it, decide whether to act on it, and inform the user if relevant. The prompt requires the model to surface the context to the user.Why a message + handler rather than a synchronous call: the bus handles queueing, multiple late arrivals serialize into separate primary turns without colliding with an in-progress turn, and the original A2A handler returns promptly.
Primary-agent directive update: add a line telling the primary to check
notifications/on each turn as defense-in-depth.Implementation order
src/RockBot.Subagent/SubagentManager.cs— addIsActive(subagentSessionId)accessor backed by existingSubagentEntryregistrysrc/RockBot.A2A/LateA2ANotificationMessage.cs(new) — record +NotificationKindenumsrc/RockBot.A2A/LateA2ANotificationHandler.cs(new) + registration insrc/RockBot.Agent/Program.csA2ATaskResultHandler.cs,A2ATaskStatusHandler.cs,A2ATaskErrorHandler.cs,InputRequiredHandler.cs: WM stash → publishLateA2ANotificationMessage→ returnsrc/RockBot.Agent/agent/directives.md) — line telling the primary to checknotifications/on each turnLateA2ANotificationHandlerhappy path + missing-WM-key pathTests
ROCKBOT_RABBITMQ_HOST-gated integration test changes needed — this is in-process routing, not transportRisks
a2aAwaiteralready prevents most late arrivals. This fix is the safety net.notifications/indexand consolidates pending entries into one turn).LateA2ANotificationHandlerprompt requires the model to state the source so it is not opaque.PrimarySessionIdresolves to nothing (CLI one-shot, disconnected user), the handler drops cleanly with a Warning. Matches existing "session not found" patterns.Out of scope
AgentLoopRunner.RunAsync's signatureRelated