Skip to content

fix(agents): keep an unread stale completion actionable for the parent (#1092) - #1574

Closed
BGamboa13 wants to merge 4 commits into
Gentleman-Programming:mainfrom
BGamboa13:fix/unread-stale-completion-signal
Closed

BGamboa13 wants to merge 4 commits into
Gentleman-Programming:mainfrom
BGamboa13:fix/unread-stale-completion-signal

Conversation

@BGamboa13

@BGamboa13 BGamboa13 commented Sep 30, 2026 •

Copy link
Copy Markdown

Linked issue

Closes #1092

Problem

On main, a completion that settles as stale is delivered only as a TUI transcript entry: deliverStale calls pi.appendEntry(AGENTS_STALE_RESULT_TYPE, …) and nothing else. When an unread first completion settles while the parent is inside one tool call longer than STALE_COMPLETION_MS (90 s), it never reaches the model and requests no wake. The parent can keep reporting the task as running.

Approach

  • The stale rule stays: the full report is still never replayed into model context.
  • The stale path also sends one compact, model-facing notice (gentle-agents.stale-notice, display: false, no report text). The notice names the task and tells the parent to call subagent_result.
  • The notice goes through the idle/run/hold router from fix(agents): preserve prompt lifecycle for idle child delivery #1631:
    • an idle parent gets one coalesced wake through the prompt lifecycle;
    • an active run gets the notice steered in;
    • content held during compaction waits for the boundary.
  • Results already consumed (consume()) and tasks from other sessions still produce neither the entry nor the notice.

The production change is +29/−12, in extensions/gentle-agents.ts; the change in lib/agents-completion-delivery.ts is a comment update. The rest is tests.

Verification

Branch updated by merging main @ 2fb7700a.

  • Six new issue #1092 tests in tests/gentle-agents.test.ts. Against main's sources, four fail:

    • idle wake;
    • active-run steer;
    • compaction hold;
    • a stale and a fresh completion sharing one wake.

    With the change, all six pass. The other two are guards (a consumed result stays silent; another session's task stays silent) and pass both ways.

  • The CI steps were run locally on Node 24.21.0 with a clean HOME:

    • pnpm run typecheck, check:runtime-modules, scripts/verify-package-files.mjs and test:packed-package pass.
    • pnpm test: provider-contract and runtime-harness pass. In unit-tests, only the four on a TTY … launcher tests fail, and they fail identically on a clean main checkout in the same environment (WSL, no real TTY).
  • @MarsSall independently reviewed the earlier head 19297002 and found no blocking defects. Since then, the notice has been moved onto fix(agents): preserve prompt lifecycle for idle child delivery #1631's router.

Out of scope

Summary by CodeRabbit

  • Bug Fixes
    • Stale task completions no longer replay the full report. Unread results instead trigger a brief notice with the task outcome and age, plus instructions for retrieving the result.
    • Notices are routed to the active parent task or stored for an idle parent, with a coalesced wake requested when needed. Results already retrieved or owned by another session are not announced again.
  • Documentation
    • Clarified when stale reports are withheld and how unread results can be retrieved.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 7f148840-6ed5-4df3-a8ab-81c07e9b78e7
📥 Commits

Reviewing files that changed from the base of the PR and between 82a6b8b and 5762f93.

📒 Files selected for processing (2)
  • extensions/gentle-agents.ts
  • tests/gentle-agents.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Unread stale completions for the active session now retain a durable transcript entry and trigger a compact steer notice. The notice identifies the task outcome and age, and directs retrieval through subagent_result or subagent_status without including the report.

Changes

Stale Completion Delivery

Layer / File(s) Summary
Stale notice delivery and validation
extensions/gentle-agents.ts, lib/agents-completion-delivery.ts, tests/gentle-agents.test.ts
deliverStale sends a hidden, turn-triggering notice for an unread stale completion in the active session while retaining the transcript entry. The notice omits the report and directs retrieval through subagent_result or subagent_status. Tests check notice content, duplicate suppression, consumed results, and session ownership. Documentation clarifies the 90-second freshness behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: alan-thegentleman

Merge Risk: ⚪ Minimal · up to 5762f

Unread stale completions remain retrievable, and the parent is woken to act on the notice. No material merge-blocking risk is established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 82a6b

Unread completions gain a compact notification without automatically replaying their reports or granting new privileges. Session and consumption checks remain in place. Delivery-failure recovery remains uncertain, but no new security defect was demonstrated.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The newly delivered information is task metadata and retrieval instructions directed to the owning parent session. The stale notice does not itself spawn work, modify permissions, or replay the completed report. Any subsequent retrieval uses existing tools and authority.

Trust Boundaries and Controls

  • observed — Push delivery enforces session ownership, but pull lookup is broader: resolveTask accepts a matching live task or a stored task ID without a parent-session check in that resolver. This behavior exists in the authoritative base. The new notice is session-gated and supplies that session's task ID, so broader lookup is not established as an introduced or worsened exposure.

Resilience and Maintainability Implications

  • observed — The wake dispatcher clears the owed-wake flag before sending and does not restore it on a synchronous send exception. This is unchanged from base and limits automatic recovery rather than weakening session isolation. Held content has later boundary-flush paths, but those paths do not establish a retry after a failed wake send.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #1092 requires an unread completion to remain actionable after the 90-second threshold, while preserving consumed-result suppression and session ownership. deliverStale keeps the durable stale…
Out of Scope Changes check ✅ Passed The production change, comment update, and tests support issue #1092. They do not change the stale threshold, fresh-completion delivery, or unrelated behavior. No unrelated changes are shown.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: keeping unread stale completions actionable for the parent.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MarsSall

MarsSall commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Thanks for this fix. We independently reviewed head 19297002215df8bf8e25613a20c474a8775b7090 against base 4b6b148b7dbd741b3097c00d5888f0209a567f94 and found no blocking candidate-caused defects in this scope.

Component verification used exact-SHA source reads and production queue/delivery snippets in memory, with a mocked host and clock:

  • Base reproduced the missing model-facing notice at 90,000, 90,001 and 102,000 ms; the PR produced exactly one hidden steer notice with triggerTurn: true, without report/error replay.
  • Fresh delivery below the threshold remained unchanged. Consumed results and foreign-session completions remained suppressed; repeated flushes and duplicate enqueueing produced no duplicate delivery.

Update: independently executed the focused suite on the same pinned head. With Node 24.14.1 and pnpm 11.1.1, frozen-lockfile installation with lifecycle scripts disabled succeeded. In an isolated temporary environment with network disabled during tests, this command passed:

node --experimental-strip-types --test tests/agents-completion-delivery.test.ts tests/gentle-agents.test.ts

138 passed, 0 failed, 0 skipped; exit 0. An earlier sandbox attempt had nine Git-fixture failures because /dev/null was read-only; after validating private sandbox devices and git init, the final run passed all 138. All temporary dependencies, caches and source fixtures were removed; the original checkout and live installation were unchanged.

This verifies the focused suite, not the full suite, typecheck, packaging, official CI or live Pi integration. Hidden-message semantics were checked statically against Pi 0.99.1, not exercised in a live host. Could you confirm the repository CI results and a real-host case where an unread completion ages past 90 seconds during a long parent tool call? This evidence supports the scoped fix, but is not a formal approval or a merge-readiness claim.

@BGamboa13

Copy link
Copy Markdown
Author

Thanks for the careful independent run, @MarsSall. Here are the two things you asked for.

Repository CI

Neither workflow has run on 19297002 yet. Both runs (CI 36670380566 and Windows Hidden Internal Processes 36670380728) are sitting in action_required, because this is a fork PR and needs a maintainer to approve the workflow run.

Until someone approves it, I ran the Linux CI job steps myself on the exact head. I used Node 24.14.1, the same major as the workflow, with pnpm install --frozen-lockfile. I compared against the PR's merge-base, 664bfdd. The 4b6b148 base in your note is current main, which has moved on since this branch was cut, so a diff against it picks up unrelated files.

step PR head 19297002 merge-base 664bfdd
pnpm run typecheck pass —
pnpm run check:runtime-modules pass —
node scripts/verify-package-files.mjs pass —
pnpm run test:packed-package pass pass
Darwin transport subset (includes tests/gentle-agents.test.ts) 179/179 pass —
pnpm test 62 failing 63 failing
  • The PR adds no failures. The 62 failing tests on the head are a strict subset of the 63 failing on the merge-base.
  • They are environmental. They are all in review-*, gentle-ai, Herdr permission-lifecycle and package-manifest suites, which this PR does not touch, and they fail the same way on the untouched merge-base. The machine is WSL2, so I read them as host-specific rather than something the official runner will hit.
  • Node 26 run. On Node 26.10 the packed-package step also failed, identically on both sides. It passes on 24.

Real-host cases (Pi 0.99.1)

Setup. Pi 0.99.1 with gentle-pi 3.7.0. Its lib/agents-completion-delivery.ts is byte-identical to this PR's base before the patch, and to this PR's head after it. The session transcript gives exact timestamps: when each task settled, when it was flushed, which parent tool call was running, and when the parent first read the result.

Before the patch: 9 cases

Each background completion settled while the parent was inside one long tool call:

  • bash running a build or test suite, 5–20 min
  • ask_user_choice waiting on the human, 3.5–7 min
  • a long codemode call

The result was flushed at the next turn_end, 140–1148 s old. All 9 were recorded only as gentle-agents.stale-result, with no model-facing message at all. None had been read, and none had any fresh delivery before the flush.

What happened to them:

  • Never read (3). The three that flushed together at 186, 152 and 140 s were never read by the parent.
  • Read late (6). The rest were read 18 s to 60 min later, only when the conversation happened to come back to them.

After the patch: 3 cases

Each produced exactly one hidden gentle-agents.stale-notice, and the parent called subagent_result for that task within 5–13 s:

  • 126 s: a deliberate probe. I launched a task and then held the parent in a 151 s bash.
  • 487 s: organic. The parent was in a 646 s bash waiting on external CI.
  • 101 s: organic, with no tool in flight. The task settled while the parent's model was generating a single 133 s turn. A slow model turn alone can cross the 90 s threshold, not just a long tool call.

One caveat on my host

My install also carries a separate local workaround for the idle-wake problem (earendil-works/pi#5581 / #1528). It wraps sendMessage so that an idle parent gets nextTurn plus a prompt, and my backport of this PR's notice goes through that wrapper.

  • 126 s and 487 s cases: these flushed mid-run, where the wrapper delegates to exactly the sendMessage(..., { deliverAs: "steer", triggerTurn: true }) this PR makes.
  • 101 s case: the flush landed at the same moment the run ended, and a user message arrived. The notice then surfaced about 10 minutes later, at the following turn boundary. That delay comes from my wrapper's idle path, not from this PR.

To be clear about scope, this is the same as yours: evidence for the scoped fix, not a merge-readiness claim.

@BGamboa13

Copy link
Copy Markdown
Author

Follow-up on the caveat in my previous comment. The ~10 minute delay in the 101 s case came from my local idle-wake wrapper, which decided "idle" by counting agent_start/agent_end. Pi emits that pair once per inner prompt()/continue(), so the count read 0 in the middle of a run. That bug is fixed, and the fix is proposed upstream in #1595 (for #1528).

It touches this PR in one place. The stale notice here is also a triggerTurn: true wake-up, and it can be flushed on agent_settled, when the host is idle. So it should go through #1595's createIdleWakeSender, or it hits earendil-works/pi#5581 the same way. I'll rebase whichever of the two lands second.

Gentleman-Programming#1092)

A stale completion was delivered only as a TUI transcript entry. An unread
first completion that settled during one long parent tool call therefore
never reached the model and requested no wake, so the parent could report
the work as still running.

The stale path now also sends one compact model-facing notice, without the
report, telling the parent to pull the result. The notice takes the same
idle/run route as other child content (Gentleman-Programming#1528), so an idle parent gets a
coalesced wake through the prompt lifecycle instead of a direct turn.

Closes Gentleman-Programming#1092
@BGamboa13

Copy link
Copy Markdown
Author

Updated the branch by merging main @ 2fb7700a (no conflicts, no force push). On the merged head 82a6b8bc, the transport/agents suites (agents-session-transport*, gentle-agents, bridge-wake-*) pass 250/250, and typecheck and check:runtime-modules pass. pnpm test has 11 failures (dev-binary, gentle-ai/Herdr, package-manifest, review-host-relay, yolo-mode); the same 11 fail on a clean main checkout in the same environment. deliverStale on main is unchanged since this PR was opened (it still only calls pi.appendEntry), so the fix still applies.

Another real-host case without the patch (gentle-pi 4.0.0)

This adds a case on the current release to the real-host evidence @MarsSall asked for. The installed extensions/gentle-agents.ts was byte-identical to v4.0.0, i.e. without this PR.

  • Two background tasks settled while the parent was inside one 133 s gentle_review_capture_group call (15:59:24 → 16:01:37 UTC).
  • At the flush right after that call, both were recorded only as gentle-agents.stale-result, at ageSeconds 171 and 142. Neither had been read, and no model-facing message was sent.
  • The parent then ended its turn saying it was waiting for exactly those two results. Over the next 2.5 h it answered three unrelated user messages without pulling them. At 17:46 UTC it still listed the verify task as "waiting for verify" in its todo list, 1 h 45 min after that task had completed.
  • It called subagent_result for both at 19:33 UTC, 3 h 32 min after the flush, and only after a human nudge.

This is #1092 on the current release. It shows only the base behaviour; the after-patch real-host cases are in my earlier comment.

@MarsSall

MarsSall commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating this to use the shared idle/run/hold router. We independently reviewed head 82a6b8bcd991daed0ef2837359cd37c7de531530 and ran:

node --experimental-strip-types --test tests/agents-completion-delivery.test.ts tests/gentle-agents.test.ts

207 passed, 0 failed, 0 skipped, using Node 24.14.1 and pnpm 11.1.1. Tests ran in a network-disabled Bubblewrap sandbox with candidate source read-only. No blocking candidate-caused routing defects were identified in this scope.

One non-blocking suggestion at extensions/gentle-agents.ts:796: recommend subagent_result directly rather than “subagent_result or subagent_status”. Status returns metadata, not the final report, so following that alternative can leave the result unread without another notice.

This verifies the focused suites, not the full suite, typecheck, official CI, or live-provider integration. It is scoped verification evidence, not a formal approval or merge-readiness claim.

Gentleman-Programming#1092)

subagent_status returns task metadata, not the report, so a parent that followed it would still leave the result unread, with no further notice. Suggested in review.
@Alan-TheGentleman

Copy link
Copy Markdown
Collaborator

Thanks a lot for this, @BGamboa13. Your diagnosis of the stale path and the idea of a compact model-facing notice routed through the idle/run/hold router were exactly right, and #1833 keeps that approach.

I'm closing this in favor of #1833 because #1821 showed the stale path was only one of three ways a finished task could be lost silently. Forwarding errors were swallowed after the task was marked delivered, and a rejected idle wake was never retried. #1833 fixes all three in one place, so landing both PRs would conflict in the same functions.

If you have a moment, a review on #1833 would be very welcome since you know this code well.

@BGamboa13

Copy link
Copy Markdown
Author

Thanks, @Alan-TheGentleman, closing this in favor of #1833 makes sense. I read its diff: the stale path now sends a compact gentle-agents.stale-notice through the same idle/run/hold route, and its test "an unread completion held past the stale window steers a compact notice into the running parent" is the case from the gentle-pi 4.0.0 session I documented above: two completions flushed only as stale-result (171 s and 142 s) after one 133 s tool call, then read 3 h 32 min later.

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.

bug(agents): unread background completion becomes transcript-only during a long parent tool call

3 participants