feat(daemon): infer the parent reply when a needsReply delegation turn ends without its report - #984
feat(daemon): infer the parent reply when a needsReply delegation turn ends without its report#984Poytr1 wants to merge 2 commits into
Conversation
…n ends without its report (#800) The mechanism half the parked directive fix (#905) could not substitute for, measured on the webchat night-collection cell: a COLD needsReply child mostly answers its delegation as its ordinary assistant response — a correct answer delivered to nobody — and never reaches for any messaging tool (9/9 lost at baseline, 8/9 still lost with #905's directive). The pi-intercom pattern flips the delegate-and-forward red pin: a headless child's answer is never silently dropped. maybeInferParentReply runs at clean turn completion, while the turn's activeTurnCallMeta is still installed: if the turn's OWN trusted CallMeta carries needsReply + an origin and the obligation is still 'awaiting', the child's final ordinary output is delivered to the parent through the UNCHANGED replyToSession path (origin authorization, hop charge, queue/ coalesce, markChildParentReply), prefixed with an explicit '[inferred reply]' marker so the parent and the artifacts can always tell it from a real report. Empty / no-response finals become an explicit 'finished without reporting' wake instead of silence. Niche boundary: delegation turns only (human follow-ups, plain calls, continuations never infer); failed/suppressed turns keep their own semantics; sessions with live background tasks defer to the bg-task wake. Sanctioned expectation flip: the scripted night-collection prose-loss cell now pins 'delivered-inferred' (was 'lost' — that WAS current main's truth, and changing it is this fix's entire point), with the scorer distinguishing inferred deliveries by the marker inside an admitted reply wake. New unit pins in inferred-parent-reply.test.ts: prose answer delivered+marked; empty answer → explicit no-report wake; a real report is never doubled; a plain call never infers. Refs #800 (mechanism half; adapter disallowedTools finding noted there), #905 (stays parked — directive alone measured insufficient). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Requesting changes for one background-task lifecycle race in the new inferred-reply path. The plain prose, empty output, real-report deduplication, and non-needsReply cases are otherwise well scoped, and the focused tests, daemon lifecycle suite, daemon typecheck, lint, and formatting checks all passed. However, a task can leave the live-task set before its deferred completion wake is delivered, so the new code may infer a premature report and the later background wake may produce a second real report.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
| // task settles (see wakeOnBackgroundTaskDone). Do not preempt that with a premature | ||
| // inference of "I started the task…" narration. | ||
| const sessionId = this.store.getSession(childKey)?.acpSessionId ?? undefined | ||
| if (sessionId !== undefined && (this.sdkLease.get(sdkLeaseKey(agentId, sessionId))?.tasks.size ?? 0) > 0) return |
There was a problem hiding this comment.
This fence misses a settled-but-not-yet-delivered background task. settle() deletes the task from lease.tasks before scheduleBackgroundTaskWake() increments armedWakes, and wakeOnBackgroundTaskDone() explicitly defers that wake when the current dispatch is still finalizing. In that window this check sees zero tasks and infers the turn's narration (for example, “I started the task”) to the parent; the deferred wake then runs with the sticky parent-report directive and can send the actual result as a second parent wake. Please also defer inference while a completion wake is armed/delivering (or otherwise use the existing background-work fence), so the background-task path still owns the one report.
There was a problem hiding this comment.
Fixed in bef225d: the guard now also treats lease.armedWakes > 0 as live background work, closing the settle window — a task that left lease.tasks with its (possibly deferred) wake still armed defers inference to the bg-task wake path instead of shipping the turn's narration.
There was a problem hiding this comment.
Confirmed—the armedWakes guard closes the settle/deferred-wake race. I re-reviewed bef225d5 and approved the revision. Thanks for the fix.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
…ound guard (review) A task that just settled leaves lease.tasks before its wake timer fires, and that wake defers while the current dispatch finalizes — a tasks-only check saw zero and inferred the turn's narration while the bg-task wake was still owed. Guard on armedWakes too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Approved. The synchronized revision fixes the prior background-task race at the correct lifecycle boundary: inference now stands down while a settled task still owns an armed completion wake, preventing the premature inferred report followed by a second real report. I found no remaining blocking issues in the complete PR patch. Verification on bef225d59555d8aa97b378ce4e615e30817b79d7: 120 focused inferred-reply/message-agent/night-collection tests passed, 63 daemon lifecycle tests passed, and daemon typecheck, targeted ESLint, and Prettier checks passed.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
What
The #800 mechanism half — measured to be what the parked directive fix (#905) cannot substitute for: on the webchat night-collection cell, a COLD needsReply child answers its delegation as its ordinary assistant response (a correct answer delivered to nobody) and never reaches for any messaging tool — 9/9 lost at baseline, 8/9 still lost WITH #905's directive; 7 of 9 children made zero messaging-tool calls.
maybeInferParentReply(pi-intercom's inferred-reply pattern) runs at clean turn completion while the turn'sactiveTurnCallMetais still installed: if the turn's OWN trusted CallMeta carriesneedsReply+ an origin and the obligation is stillawaiting, the child's final ordinary output is delivered to the parent through the unchangedreplyToSessionpath (origin authorization, hop charge, queue/coalesce,markChildParentReply), prefixed[inferred reply]so the parent — and every artifact — can always tell it from a real report. An empty / no-response final becomes an explicit finished without reporting wake instead of silence.Exactly-one-obligation scoping (the niche boundary): delegation turns only — human follow-ups, plain calls, and continuations never infer; failed/suppressed turns keep their own semantics; a session with live background tasks defers to the existing bg-task wake.
Sanctioned expectation flip (the one intended behavior change): the scripted night-collection prose-loss cell now pins
delivered-inferredwhere current main's truth waslost— flipping that pin is this fix's entire point. The scorer distinguishes inferred deliveries by the marker inside an admitted reply wake (daemon evidence, not content visibility).Lever-1b finding (noted on #800, not wired):
claude-agent-acpforwardssessionMeta.claudeCode.options.disallowedTools, so the colliding built-inSendMessagecould be suppressed per session — more than a trivial flip to do safely (runtime-gated seam + subagent implications); the inferred reply catches that failure mode regardless.Tests
New
packages/daemon/test/inferred-parent-reply.test.ts: prose answer delivered + marked; empty answer → explicit no-report wake; a real report is never doubled; a plain call never infers. Night-collection + webchat Werewolf scripted games green; daemon webchat/continuation/session-manager suites unmodified.Refs #800, #905 (stays parked), #941 (the measuring cell).
🤖 Generated with Claude Code