Repository navigation
fix(native-chat): stop an unsettleable dispatch from blocking /clear and /compact - #20791
brennanb2025 wants to merge 2 commits into
Conversation
…and /compact Conversation-command admission refused every command while any submission was `pending` or `unknown`. A recovered `unknown` is terminal: crash recovery skips rows it already marked, restart reconciliation only narrows accepted outcomes, and Retry drops the outbox entry without touching the journal. Nothing settles it, so the refusal named a step the user could not take for the rest of the session. Reuse `hasUnansweredStructuredAgentSessionDispatch`, which already excludes recovered rows and keeps the legacy-host fallback for hosts that publish the recovery reason without the marker. Scope it to the caller's runtime fence: a dispatch from a dead generation is owed by a host that is gone, so no live provider answers it and no user action settles it either.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthrough
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to The change continues blocking commands for live unresolved dispatches while allowing stale recovered records, so it is ready to merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Admission reuses the shared unanswered-dispatch predicate —
structured-conversation-command-admission.ts:58now callshasUnansweredStructuredAgentSessionDispatch(ctx.journal.submissions(), ctx.fence), replacing the inlinepending || unknownscan. A recoveredunknownand a dead-generation row no longer refuse/clear,/compact, or rewind for the life of the session. - Fence-scoped to the current generation — a live in-generation
pending/unknownstill refuses; only rows at an older fence, or markedrecovered/ carrying the legacyhost_restarted_before_acknowledgementreason, are admitted. This matches the fence semantics the same helper already applies to session status. - Coverage — five new cases in
structured-conversation-command-admission.test.ts: three admitted states (recovered unknown, legacy reason without the marker, stale fence) and two refusal guards (live pending/unacknowledged, terminally settled). The three admitted cases fail against the base predicate, so they pin the fix rather than the status quo; all 10 pass here, and the related command/projection/rewind suites pass 59/59.
I independently checked the two directions that matter and both hold. A current-generation send is appended with fence: ctx.fence, and runtimeFence only advances with proven prior-owner death or an unused reservation, so no live dispatch can be excluded by the fence comparison; the handoffStage window is refused earlier at structured-conversation-command-admission.ts:29-31. The recovered and legacy-reason relaxations are written only by crash/restart/provider-exit recovery, never on a normal-operation path, and /compact remains protected by the unresolved-turn check.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
|
Closing as obsolete: main no longer has the code this PR changes, or already behaves as it intended. Main structured-conversation-command-admission.ts:110 refuses /clear and /compact only for sends work.owesSend() says the live generation still holds (current-work.ts:228; #28103 3bfb114, #29905), not for any recovered pending/unknown (Stale-PR cleanup 2026-10-20, approved by Brennan. Reopen if something here is still missing on main.) |

ELI5
If Orca could not confirm whether one of your messages reached the agent, it refused
/clearand/compactfor the rest of the session, telling you to "resolve pending or unconfirmed messages" — something you had no way to do. Now it only refuses while this session genuinely still owes an answer on a message.What Changed
structured-conversation-command-admission.tsno longer runs its own inline scan over submissions. It reuses the existinghasUnansweredStructuredAgentSessionDispatchpredicate fromstructured-agent-session-projection.ts, scoped to the caller's runtime fence.Why
Failure mechanism. Conversation-command admission refused every command when any submission was
pendingorunknown:unknownhas two very different meanings, and this collapsed them. A liveunknownis a send still outstanding — refusing on it is right. A recoveredunknownis terminal and unsettleable:markJournalPendingSubmissionsUnknownskips rows already markedrecovered, so crash recovery never revisits one.accepted/rejected; anything it leavesunknownstays exactly as the crash boundary wrote it.Nothing re-derives the row and no user action settles it, so the command stayed refused for the life of the session behind an instruction nobody could follow. The refusal was a durable obligation with no way to die.
This is pre-existing, but #20723 makes a recovered
unknownthe normal terminal outcome for a restart-stranded send, which takes this from rare to routine.The predicate being reused already encodes exactly the right distinction: it excludes
recoveredrows and carries a fallback for older hosts that publish the recovery reason without the optional marker.Fence decision — the admission check now passes
ctx.fence, deliberately.Every submission this generation appends carries
fence: ctx.fence, and the journal write guard refuses any fence belowhighestFence, sosubmission.fence >= ctx.fenceselects exactly the rows the current generation owns.A stale-fence unanswered dispatch is work owed by a host generation that is gone. No live provider will answer it, and no user action settles it — so blocking on one reproduces the very same unfollowable-instruction bug, just one state over (for example a
pendingrow from a dead generation whose crash-boundary settle write failed, or a legacy-host row with no marker). Fence-scoping does not weaken the real guard: a send genuinely in flight in this generation hasfence == ctx.fenceand still refuses the command. It also brings this call site in line with the helper's other callers, which all pass a fence; passingundefinedwould have left this the only caller that still strands on dead-generation rows.Linked Issue
STA-7416. No GitHub issue for this one; the admission defect was found while reviewing the plan for #20723.
Visual Proof
N/A— no UI or layout change. The only user-visible difference is that a refusal string stops appearing in a state the user could not exit; there is no rendered surface that differs to capture side by side.Testing
Guard is a vitest test collected by
pnpm test(structured-conversation-command-admission.test.ts), not e2e.Failing-first evidence. Source change reverted to the base file with the final test in place, then restored:
Tests 3 failed | 7 passed (10)— the three failures are exactly the new cases (recoveredunknown, legacy-host reason without the marker, dead-generation fence), eachexpected 'Resolve pending or unconfirmed messag…' to be null.Tests 10 passed (10).The ablation was instrumented rather than assumed: the reverted file was asserted to contain the old inline predicate (1 match) and none of the new helper (0 matches), and to carry the helper again after restore.
Tests cover both directions — the three admitted cases above, plus that a live
pendingand a liveunknownin the current generation still refuse, and that fully settled submissions admit.Gates:
pnpm tcexit 0,oxlintexit 0, changed-code quality gate passed with 0 new findings across both files.Platforms: pure decision logic in the execution host with no platform-dependent behavior, so it carries across macOS / Linux / Windows and the SSH execution host identically. No wire shape, journal schema, or published content changed, so there is no mixed-version client concern.
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Reuse over reimplementation: the inline scan is deleted in favour of the predicate that already existed, so there is no second copy of this rule to drift.
Checked every other
dispatchState === 'unknown'site insrc/for the same class of defect. No other site blocks a user action on an unsettleable row:journal-submission-reconciler.ts/journal-restart-reconciliation.tsinclude recovered rows on purpose — that inclusion is how a recoveredunknowncan still be narrowed toaccepted. Settlement machinery, not a user gate.structured-agent-session-dead-generation-settlement.tsalready excludesrecovered.journal-pending-submission-recovery.tsskipsrecoveredfor idempotence.journal-store.tspendingSubmissionsreadspendingonly.unknownby dropping or parking the entry; neither blocks.structured-agent-session-send-settlement.tswaits onpendingonly, per message id and with a bounded timeout.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)