Skip to content

fix(background-agent): force-deliver parent wakes held past a stale tool-call ceiling - #7117

Open
andrei-kiparuk wants to merge 1 commit into
code-yeongyu:devfrom
andrei-kiparuk:fix/parent-wake-stale-toolblock-hold-ceiling
Open

fix(background-agent): force-deliver parent wakes held past a stale tool-call ceiling#7117
andrei-kiparuk wants to merge 1 commit into
code-yeongyu:devfrom
andrei-kiparuk:fix/parent-wake-stale-toolblock-hold-ceiling

Conversation

@andrei-kiparuk

@andrei-kiparuk andrei-kiparuk commented Aug 21, 2026

Copy link
Copy Markdown

Problem

Background-task completion notifications can be starved forever by the stale tool-call deferral, leaving a fully idle parent session that never resumes work.

When the latest assistant turn in the parent session ends with an unresolved tool part, getParentWakeSessionHistoryDeferralDecision holds the wake (admitting it as a noReply deposit at most) and waits for "the idle/consumption machinery" to resume. That is correct while the turn is genuinely mid-flight, but there is no upper bound on the hold. If the blocking tool part can never resolve — e.g. a bash tool left at state.status: "running" after its stream died mid-tool — the state is permanent:

  • the zombie assistant turn keeps latestAssistantTurnBlocksInternalPrompt true forever,
  • every future flush hits deferReplyWakeWhileUnsafe and reschedules,
  • no new internal prompt (or anything else) ever starts a stream again.

Observed in production

From a single real session (ses_fda080e47…):

[20:39:42] Sent deferred parent wake
[20:39:47] Requeued dispatched parent wake after no assistant output (retryCount: 1)
[20:39:48] Sent deferred parent wake
[20:39:52 → 20:52:46+] Holding parent wake during stale tool-call deferral   ← every ~1s, 13+ min and counting
[20:39:52 → 20:52:46+] Deferred retained reply-required parent wake until parent session is safe

209 hold events across two sessions on one day. The session history confirmed the blocker: the latest assistant message had finish: null, 0 tokens, and exactly one part — a bash tool stuck at "running" (a gh run watch) from hours earlier. Every background task had long since completed; the parent just sat idle.

Fix

Cap the hold with PARENT_WAKE_STALE_TOOL_BLOCK_MAX_HOLD_MS (60s), wired through as an optional staleToolBlockMaxHoldMs option:

  • Before the ceiling: behavior is unchanged — hold, admit-only deposits, reply liveness retained. A live-but-quiet turn (long silent tools are legitimate) still gets its full grace window, preserving the intent of the ses_14a3ab27bffe fix.
  • At/after the ceiling: the wake is force-delivered (defer: false). Sixty seconds of zero activity across message time, part time, and tool-state timestamps overwhelmingly indicates a dead turn rather than a quiet one, and unbounded starvation is worse than the residual fork risk of dispatching into an idle-status session.
  • toolCallDeferralStartedAt is deliberately not cleared on the escape path so the pre-dispatch confirmation re-check reaches the same decision instead of re-arming the deferral.
  • No ceiling configured (undefined) preserves the historical unbounded hold for existing callers.

Also marks the admit-only/noReply wake text part as synthetic: true, so deferred notifications no longer render as raw <system-reminder> user messages in the TUI while they sit deposited during the hold window.

Test updates

Two tests in task-completion-cleanup.test.ts pinned the unbounded-hold behavior at a 60s-stale zombie tool block ("admitted as noReply with reply liveness retained"). That exact scenario is the starvation case this PR fixes, so they now assert the post-ceiling contract: force-delivered reply dispatch, pending wake consumed. Sub-ceiling admission behavior remains covered by the untouched neighboring tests plus three new cases in parent-wake-history-deferral.test.ts (held under ceiling → still held; past ceiling → delivered, including confirmation re-check convergence; no ceiling configured → historical hold preserved).

Testing

  • bun test packages/omo-opencode/src/features/background-agent/ → 746 pass, 0 fail
  • tsgo --noEmit -p packages/omo-opencode/tsconfig.json → clean

Summary by cubic

Stops indefinite starvation of background-task completion notifications by capping the stale tool-call deferral and force-delivering the parent wake after 60s. Previously we held reply-required wakes indefinitely when the last assistant turn had a zombie tool; now we hold under the ceiling and dispatch after it. Deferred noReply deposits are marked synthetic to avoid TUI noise.

  • Adds optional staleToolBlockMaxHoldMs through ParentWakeNotifier and ParentWakeSessionInspector; BackgroundManager sets it to 60_000 ms. Omitting it preserves the legacy unbounded hold for other callers.
  • Leaves toolCallDeferralStartedAt set on escape so pre-dispatch confirmation re-checks converge on delivery.
  • Marks admit-only/noReply text parts synthetic to hide raw system-reminder messages in the TUI.
  • Updates tests to expect force-delivery past the ceiling and adds history-deferral coverage.

Written for commit c92f51a. Summary will update on new commits.

Review in cubic

…ool-call ceiling

The stale tool-call deferral holds reply-required parent wakes while the
latest assistant turn looks mid-flight, admitting them as noReply deposits
and waiting for the idle/consumption machinery. When the blocking tool part
can never resolve (e.g. a bash tool left at status "running" after its
stream died), the hold never releases: observed in the wild as a wake held
for 13+ minutes with a retry log entry every second while every background
task had already completed, leaving the parent session idle indefinitely.

Cap the hold at PARENT_WAKE_STALE_TOOL_BLOCK_MAX_HOLD_MS (60s). After the
ceiling the wake is force-delivered; toolCallDeferralStartedAt stays set so
the pre-dispatch confirmation re-check converges on the same decision.
Without a configured ceiling the historical unbounded hold is preserved.

Also hide admit-only/noReply wake deposits from the TUI by marking their
text part synthetic, so deferred notifications no longer render as raw
system-reminder user messages during the hold window.
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA. Thank you! ✅
Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added the opencode OpenCode edition: packages/omo-opencode label Aug 21, 2026
@andrei-kiparuk

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

opencode OpenCode edition: packages/omo-opencode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant