Skip to content

fix: emit lifecycle events for ACP sessions to enable auto-announce (#40693)#41309

Closed
jzOcb wants to merge 1 commit into
openclaw:mainfrom
jzOcb:fix/acp-lifecycle-announce
Closed

fix: emit lifecycle events for ACP sessions to enable auto-announce (#40693)#41309
jzOcb wants to merge 1 commit into
openclaw:mainfrom
jzOcb:fix/acp-lifecycle-announce

Conversation

@jzOcb

@jzOcb jzOcb commented Mar 9, 2026

Copy link
Copy Markdown

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 lifecycle events with phase: 'end':

  1. Standard subagent runs emit lifecycle events via handleAgentEnd()emitAgentEvent({ stream: 'lifecycle', phase: 'end' })
  2. The subagent registry listener picks up that event → calls completeSubagentRun() → triggers announce to parent

ACP sessions had two gaps:

  • acp-spawn.ts never called registerSubagentRun(), so the registry had no entry
  • dispatch-acp.ts never emitted lifecycle events after acpManager.runTurn() completed

Changes

src/agents/acp-spawn.ts

  • Call registerSubagentRun() after successful gateway dispatch, matching the pattern in subagent-spawn.ts

src/auto-reply/reply/dispatch-acp.ts

  • Emit lifecycle events (phase: 'end' on success, phase: 'error' on failure) after ACP turn completion
  • Add resolveAcpLifecycleRunId() to look up the registered runId from session context

src/infra/agent-events.ts

  • Add sessionKey→runId reverse index (runIdsBySessionKey) so dispatch-acp.ts can resolve the correct runId without threading it through the entire dispatch pipeline
  • Export getLatestAgentRunIdForSession() for the reverse lookup

Tests

  • dispatch-acp.test.ts: Added tests for lifecycle end/error event emission
  • acp-spawn.test.ts: Added assertions for registerSubagentRun() calls

Testing

  • All modified-file tests pass (23/23)
  • pnpm build passes
  • Format check passes on all modified files

Fixes #40693

AI-assisted (Codex + manual review/fixes). Fully tested.

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-apps

greptile-apps Bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the missing auto-announce flow for ACP sessions by adding two things that standard subagent runs already had: (1) a registerSubagentRun() call in acp-spawn.ts so the subagent registry knows about ACP child runs, and (2) lifecycle event emission (phase: "end" / phase: "error") in dispatch-acp.ts so the registry listener can trigger the announce-to-parent flow. To support the second change, agent-events.ts gains a runIdsBySessionKey reverse index and a getLatestAgentRunIdForSession() export.

Key observations:

  • The primary fix is sound and aligns with how standard subagent runs already work. commands/agent.ts already calls registerAgentRunContext(runId, { sessionKey }) for ACP runs, so getLatestAgentRunIdForSession will resolve correctly at dispatch time.
  • The else if (nextSessionKey) branch added to registerAgentRunContext re-orders a runId to "latest" on any context update, not just initial registration. In concurrent-run scenarios this can cause getLatestAgentRunIdForSession to return a stale runId, routing lifecycle events to the wrong subagent-registry entry.
  • Test coverage for the new lifecycle event emission is well-structured and covers both success and failure paths.

Confidence Score: 2/5

  • Core fix is correct, but a logic issue in context update re-ordering can cause lifecycle events to route to wrong runs in concurrent scenarios.
  • The primary functionality (lifecycle event emission for auto-announce) is sound and the PR passes tests. However, the else if (nextSessionKey) branch in registerAgentRunContext at line 78 re-orders runs in the Set on every context property update, not just session key changes. In concurrent-run scenarios, this can cause getLatestAgentRunIdForSession() to return a stale runId, routing lifecycle events to the wrong run's announce handler. This is a subtle logic bug that warrants fixing before merge.
  • src/infra/agent-events.ts — the registerAgentRunContext update path needs revision to avoid re-ordering runs on property-only updates

Last reviewed commit: 6ade546

Comment thread src/infra/agent-events.ts
Comment on lines +78 to 80
} else if (nextSessionKey) {
addRunIdToSession(nextSessionKey, runId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +114 to +116
const bySession = getLatestAgentRunIdForSession(sessionKey);
if (bySession) {
return bySession;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +277 to +281
if (lifecycleRunId) {
emitAgentEvent({
runId: lifecycleRunId,
stream: "lifecycle",
sessionKey,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Mar 9, 2026
@thepastaclaw

thepastaclaw commented Mar 16, 2026

Copy link
Copy Markdown

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 BenliusYang left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR looks exactly like what we need for the ACP completion relay issue #49782, please prioritize review.

@BenliusYang BenliusYang left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR looks exactly like what we need for the ACP completion relay issue #49782, please prioritize review.

@steipete

Copy link
Copy Markdown
Contributor

Closing this as implemented after Codex review.

Current main already implements the ACP auto-announce plumbing this PR was trying to add, via a different path: ACP sessions_spawn runs are registered in the subagent registry from the spawn tool, the active runId is forwarded into ACP reply dispatch, and ACP dispatch now emits terminal lifecycle events for that runId on both success and failure.

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.

@steipete steipete closed this Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ACP sessions never trigger auto-announce (missing lifecycle event in dispatch-acp path)

4 participants