Skip to content

RFC: ACP completion relay — unified approach to close the parent notification loop #49782

@jorgensandhaug

Description

@jorgensandhaug

Problem

ACP sessions spawned via sessions_spawn(runtime: "acp") never close the loop with the parent session. When an ACP turn completes (success or error), the parent that spawned or messaged the session has no way to learn:

  • That the turn finished
  • What the final assistant reply was
  • Whether the turn errored

This contrasts with standard subagents, which have a full completion pipeline: lifecycle event → subagent registry → announce flow → parent re-entry.

The same gap exists for follow-up messages via sessions_send — the parent sends a message to an existing ACP session but never gets notified when the resulting turn completes.

Root Cause (traced in source)

Two things are missing from the ACP path:

1. No lifecycle event emission. Standard subagent runs go through runEmbeddedPiAgent, which emits emitAgentEvent({ runId, stream: "lifecycle", data: { phase: "end" } }). ACP turns go through tryDispatchAcpReplyacpManager.runTurn(), which transitions to idle/error state but never emits a lifecycle event. The event bus never learns the turn ended.

2. No subagent registry entry. spawnSubagentDirect calls registerSubagentRun() after dispatch. spawnAcpDirect does not. Without a registry entry, the announce pipeline (runSubagentAnnounceFlow) is never triggered.

The existing parent stream relay (acp-spawn-parent-stream.ts) already listens for lifecycle phase: "end" events per runId — it just never receives one from ACP. And it's only set up at spawn time, not for follow-up sessions_send calls.

Existing Community Work

There are 7 open issues and 6 open PRs addressing parts of this:

Issues

PRs (all open, no maintainer reviews)

Each PR tackles a piece independently. I'd like to propose a unified approach that addresses the full pipeline.

Proposed Approach: Plug ACP into the existing subagent completion pipeline

Rather than creating a parallel ACP-specific completion path, the cleanest fix is to make ACP emit the same signals that the existing subagent infrastructure already consumes. Three small, focused changes:

Change 1: Emit lifecycle events from ACP dispatch

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

After acpManager.runTurn() completes, emit the standard lifecycle event (phase: "end" or phase: "error"). This makes ACP turns visible to the same event bus that the subagent pipeline, parent stream relay, and control UI all consume.

Change 2: Register ACP spawned runs in subagent registry

File: src/agents/acp-spawn.ts

After successful ACP dispatch, call registerSubagentRun() with cleanup: "keep" (ACP sessions manage their own lifecycle) and expectsCompletionMessage: true. This enables the existing announce flow to handle ACP completion delivery.

Change 3: Enhance completion relay with assistant reply capture

File: src/agents/acp-spawn-parent-stream.ts

On phase: "end", read the last assistant reply from the child session (reusing readLatestSubagentOutput) and include it in the parent system event — not just "run completed in Xs."

Scoping: per-run, not per-session

The completion callback should be per-run (tied to a specific runId), not per-session. If the parent spawns a turn, it gets notified when that turn completes. If someone else messages the same ACP session, no callback to the original parent. This matches how subagents already work and is the natural scoping since a run ≈ a turn.

For sessions_send follow-ups: once Change 1 lands, the parent stream relay (or announce flow) would need to register a listener for the new runId. This could be handled by registering in dispatch-acp.ts for any ACP run where the session has a spawnedBy parent, or by extending sessions_send to set up a per-run callback when targeting ACP sessions.

Questions for Maintainers

  1. Is plugging ACP into the existing subagent registry + announce pipeline the right approach? Or would you prefer a separate ACP-specific completion path? The subagent pipeline is mature and handles dedupe, retries, and multiple delivery modes — reimplementing that seems wasteful.

  2. Should this be 2-3 small PRs or one cohesive PR? I'm leaning toward separate PRs (lifecycle events first, then registry, then relay enhancement) since each is independently useful and testable. But if you'd prefer one PR that tells the full story, that works too.

  3. For sessions_send follow-ups: should the completion callback registration happen in the ACP dispatch path (automatic for any ACP turn with a parent) or in sessions_send (explicit, only when the sender opts in)?

  4. Any concerns about registering ACP runs with cleanup: "keep"? ACP sessions have their own lifecycle management; the registry entry would be for announce/completion tracking only, not cleanup.

  5. Relationship to the unified streaming refactor (docs/experiments/plans/acp-unified-streaming-refactor.md): These changes are compatible with the refactor plan and could be seen as incremental steps toward it. The lifecycle event emission (Change 1) is exactly what the refactor's canonical event contract calls for.

Happy to take feedback and adjust the approach before writing code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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