Skip to content

feat(daemon): infer the parent reply when a needsReply delegation turn ends without its report - #984

Open
Poytr1 wants to merge 2 commits into
mainfrom
claude/lever1-inferred-reply
Open

feat(daemon): infer the parent reply when a needsReply delegation turn ends without its report#984
Poytr1 wants to merge 2 commits into
mainfrom
claude/lever1-inferred-reply

Conversation

@Poytr1

@Poytr1 Poytr1 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

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'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 [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-inferred where current main's truth was lost — 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-acp forwards sessionMeta.claudeCode.options.disallowedTools, so the colliding built-in SendMessage could 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

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

@agentconnect-md-test agentconnect-md-test Bot left a comment

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.

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

Comment thread packages/daemon/src/daemon.ts Outdated
// 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

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

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.

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>

@agentconnect-md-test agentconnect-md-test Bot left a comment

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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant