Skip to content

Completion notifications for already-joined agents arrive after the final answer #185

Description

@vincelwt

Background agent completion notifications can be delivered long after they stop being useful — after the final answer, for agents whose results were already read with get_subagent_result. Each one costs a turn to answer "already incorporated".

Why the existing suppression misses it. The notification is sent with deliverAs: "followUp", which pi delivers only once the agent has no more tool calls. Emitting one mid-run parks it in pi's follow-up queue until the run ends, and a parked message can't be withdrawn. emitIndividualNudge does re-check record.resultConsumed, but that check runs when the message is enqueued — 200ms after completion (NUDGE_HOLD_MS) — while the orchestrator typically joins the agent minutes later. So a 200ms cancellation window is guarding a queue with multi-minute latency.

With pi's default followUpMode: "one-at-a-time" the parked notifications then drain one turn each.

Repro: spawn a background agent during a long run, join it with get_subagent_result a few minutes later, keep working. Its notification still arrives after the final answer.

From a real session (pi 0.82.1, extension 0.14.3, macOS):

22:09:39  agent spawned (run_in_background: true)
22:10:42  agent completes
22:13:55  get_subagent_result  ->  resultConsumed = true
22:23:40  final answer
22:23:40  notification delivered (13 min late)
22:23:44  extra turn: "Already incorporated; no further action needed."

A worse one in the same setup: 7 agents that had finished up to 1h41m earlier were flushed in a 54-second burst after the final answer — 7 wasted turns, and the burst pushed the session into a compaction.

Suggested fix: hold due notifications in-process rather than in pi's queue, and flush them at agent_settled — the first point where pi won't continue on its own. agent_end and turn_end both still have retries, compaction, or further tool-calling turns ahead, so emitting there parks them exactly as before. Since each send closure already re-checks resultConsumed, deferring the call defers the check, and a joined agent simply never notifies.

I have this implemented with tests and would like to submit it — opening a PR right after this so there's something concrete to react to. Happy to rework it if you'd rather solve it a different way.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions