fix: emit lifecycle events for ACP sessions to enable auto-announce (#40693)#41309
fix: emit lifecycle events for ACP sessions to enable auto-announce (#40693)#41309jzOcb wants to merge 1 commit into
Conversation
ACP sessions spawned via sessions_spawn(runtime='acp') never triggered the auto-announce flow to the parent session because: 1. acp-spawn.ts never called registerSubagentRun(), so the subagent registry had no entry to match against lifecycle events. 2. dispatch-acp.ts never emitted lifecycle events (phase: 'end' or 'error') after acpManager.runTurn() completed, so even if registered, the registry listener would never fire. This commit fixes both gaps: - acp-spawn.ts: Register ACP spawns with the subagent registry after successful gateway dispatch, matching the pattern in subagent-spawn.ts. - dispatch-acp.ts: Emit lifecycle events (end/error) after ACP turn completion, using a new getLatestAgentRunIdForSession() reverse lookup to resolve the correct runId from the session context. - agent-events.ts: Add sessionKey→runId reverse index so dispatch-acp can look up the registered runId without threading it through the entire dispatch pipeline. Fixes openclaw#40693 AI-assisted (Codex + manual review). All modified-file tests pass.
Greptile SummaryThis PR fixes the missing auto-announce flow for ACP sessions by adding two things that standard subagent runs already had: (1) a Key observations:
Confidence Score: 2/5
Last reviewed commit: 6ade546 |
| } else if (nextSessionKey) { | ||
| addRunIdToSession(nextSessionKey, runId); | ||
| } |
There was a problem hiding this comment.
The else if (nextSessionKey) branch re-orders runs in the Set whenever any context property (e.g., verboseLevel, isHeartbeat) is updated on an existing run, not just when the session key changes.
Problem: Given two concurrent runs (A and B) registered for the same session, if A's context is updated (e.g., registerAgentRunContext("a", { verboseLevel: "on" })) after B is registered, A will be deleted and re-added to the Set, moving it to the end. getLatestAgentRunIdForSession() then returns A instead of B, routing lifecycle events to the wrong run.
Fix: Only call addRunIdToSession when the session key actually changes (inside the first if block at line 72). Remove the else if (nextSessionKey) block (lines 78-80), which is only needed for initial registration (already handled at lines 66-67).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/agent-events.ts
Line: 78-80
Comment:
The `else if (nextSessionKey)` branch re-orders runs in the Set whenever **any** context property (e.g., `verboseLevel`, `isHeartbeat`) is updated on an existing run, not just when the session key changes.
**Problem:** Given two concurrent runs (A and B) registered for the same session, if A's context is updated (e.g., `registerAgentRunContext("a", { verboseLevel: "on" })`) *after* B is registered, A will be deleted and re-added to the Set, moving it to the end. `getLatestAgentRunIdForSession()` then returns A instead of B, routing lifecycle events to the wrong run.
**Fix:** Only call `addRunIdToSession` when the session key actually changes (inside the first `if` block at line 72). Remove the `else if (nextSessionKey)` block (lines 78-80), which is only needed for initial registration (already handled at lines 66-67).
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ade54692b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const bySession = getLatestAgentRunIdForSession(sessionKey); | ||
| if (bySession) { | ||
| return bySession; |
There was a problem hiding this comment.
Resolve lifecycle run ID from request context first
resolveAcpLifecycleRunId() currently prefers getLatestAgentRunIdForSession(sessionKey) before checking whether requestId already belongs to the same session, so overlapping ACP turns can be misattributed when the session index order does not match the active turn. In that case tryDispatchAcpReply emits lifecycle end/error for the wrong run, and the intended subagent run may never be completed/announced by the registry. This is most likely when multiple runs for one session are in flight.
Useful? React with 👍 / 👎.
| if (lifecycleRunId) { | ||
| emitAgentEvent({ | ||
| runId: lifecycleRunId, | ||
| stream: "lifecycle", | ||
| sessionKey, |
There was a problem hiding this comment.
Emit lifecycle end after post-turn steps complete
The phase: "end" lifecycle event is emitted immediately after runTurn returns, but the function still executes projector.flush() and additional delivery logic inside the same try block. If any of those later steps throw, the catch path emits phase: "error" for the same runId, producing contradictory terminal lifecycle events for one turn and potentially marking successful ACP subagent runs as failures during announce/cleanup handling.
Useful? React with 👍 / 👎.
|
Of the three PRs addressing #40693, this one has the cleanest approach: registration at the spawn layer + lifecycle events at dispatch. Would love to see maintainers review/merge this. The existing AI review comments look worth addressing if you get a chance — resolving those would probably speed up the review process. Thanks for the fix! |
BenliusYang
left a comment
There was a problem hiding this comment.
This PR looks exactly like what we need for the ACP completion relay issue #49782, please prioritize review.
BenliusYang
left a comment
There was a problem hiding this comment.
This PR looks exactly like what we need for the ACP completion relay issue #49782, please prioritize review.
|
Closing this as implemented after Codex review. Current What I checked:
So I’m closing this as already implemented rather than keeping a duplicate issue open. Review notes: reviewed against e94c0bf51542; fix evidence: commit e94c0bf51542. |
Summary
ACP sessions spawned via
sessions_spawn(runtime='acp')never trigger the auto-announce flow to the parent session. This PR fixes that.Root Cause
The announce system depends on
lifecycleevents withphase: 'end':handleAgentEnd()→emitAgentEvent({ stream: 'lifecycle', phase: 'end' })completeSubagentRun()→ triggers announce to parentACP sessions had two gaps:
acp-spawn.tsnever calledregisterSubagentRun(), so the registry had no entrydispatch-acp.tsnever emitted lifecycle events afteracpManager.runTurn()completedChanges
src/agents/acp-spawn.tsregisterSubagentRun()after successful gateway dispatch, matching the pattern insubagent-spawn.tssrc/auto-reply/reply/dispatch-acp.tslifecycleevents (phase: 'end'on success,phase: 'error'on failure) after ACP turn completionresolveAcpLifecycleRunId()to look up the registered runId from session contextsrc/infra/agent-events.tssessionKey→runIdreverse index (runIdsBySessionKey) sodispatch-acp.tscan resolve the correct runId without threading it through the entire dispatch pipelinegetLatestAgentRunIdForSession()for the reverse lookupTests
dispatch-acp.test.ts: Added tests for lifecycle end/error event emissionacp-spawn.test.ts: Added assertions forregisterSubagentRun()callsTesting
pnpm buildpassesFixes #40693
AI-assisted (Codex + manual review/fixes). Fully tested.