Skip to content

fix(session): settle approval decisions an interrupt skips or a closing reactor never ran - #1577

Open
bwachman wants to merge 3 commits into
mainfrom
cl-11433-record-skipped-and-unrun-approval-decisions
Open

bwachman wants to merge 3 commits into
mainfrom
cl-11433-record-skipped-and-unrun-approval-decisions

Conversation

@bwachman

@bwachman bwachman commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A decision that an interrupt skips while it waits on the session queue counts as a dropped approval, not as handed over, so the rebuilt agent settles it.
  • A decision handed to a reactor that closes before running the call is tracked the same way until a rebuilt store shows the call closed.
  • A decision whose delivery fails is tracked the same way, including one the deliverer handed over but never saw accepted.
  • createQueuedApprovalDeliver is the deliver seam the TUI and the tests share.
  • Builds on fix(session): settle a dropped approval on the rebuilt agent without a new turn #1576, which settles dropped approvals on the rebuilt agent.

Verification

  • bun run check passes on each commit (9527 tests at the tip)
  • e2e, through the real store: an approval skipped behind a busy session queue, an approval the reactor accepted behind a busy loop before closing, and an approval whose acceptance the deliverer never observed each leave the call parked at the rebuild, never run it, and settle once with no model turn
  • Each new e2e test fails with its commit's fix removed

Fixes CL-11433

The parked cancel is unregistered before an operator's decision is
delivered, so an interrupt that bumped the generation while the
decision waited on the session queue skipped it silently. Nothing
reached either agent, the call was marked handed over so a later
recovery ignored it, and the rebuilt agent answered it with "approval
timed out" at the old deadline.
A decision handed to a reactor that closes before running the call,
such as an interrupt landing while the loop is busy, left the call
parked on disk with nothing to settle it: rehydration announces no
gate, recovery ignores a correlation it handed over, and no drop was
recorded. The rebuilt agent answered it with "approval timed out" at
the old deadline. A handed-over decision is tracked like a drop until
a rebuilt store shows its call closed.

@TheGreatAxios TheGreatAxios left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review · Request changes

This records an approval that an interrupt skipped, or that was handed to an agent which closed before the tool ran, and settles it on the rebuilt session so it does not later time out as if nobody decided.

The case where delivery hands the decision over and then times out while watching for acceptance is handled on this branch: that call is kept and settled after the rebuild.

bun test --config ./bunfig.toml src/session/approval-resume-interrupt.test.ts src/session/approval-resume.test.ts src/tui/approval-delivery.test.ts src/tui/runner/exit.test.ts src/tui/runner/queued-approval-delivery.test.ts — 174 pass, 0 fail. Those tests cover the skipped, unrun, and failed deliveries this branch settles. They do not cover an approved tool that has already started.

What is still wrong: an approval you already gave can be recorded as denied if the session is rebuilt while that tool is still running.

Every decision handed to the agent is marked unfinished (src/session/approval-resume.ts:613), approval or rejection. On the next rebuild, a call that is still saved with no result is rejected as dropped (src/session/approval-resume.ts:771). The tool's result is not saved until the tool finishes. Approve a call, let the tool start, then interrupt before that result is written, and the rebuilt agent is told the call was denied. The tool may already have done its work. The transcript says it was dropped, so the model can try the same call again, and a grant saved before the interrupt can allow that retry without asking again.

A call whose result is already saved is left alone. The hole is the stretch after the tool starts and before its result is saved.

A deliverer gives up waiting to observe a decision's acceptance only
after it has handed the decision to the agent, so the reactor may still
hold it, but a delivery that threw was never tracked for the rebuild
settle. An interrupt then rebuilt with the call still parked and nothing
to settle it, and the rebuilt agent answered it with "approval timed
out" at the old deadline. A decision whose delivery throws is tracked
like a handed-over one, whether it failed before or after the hand-over,
since either way the call stays parked.
@bwachman

Copy link
Copy Markdown
Contributor Author

Fixed in c0eccec. deliverDecision now records the decision for the rebuild settle whenever deliver throws, then rethrows, so an acceptance-observation timeout after the hand-over is settled on the rebuilt agent.

I didn't split pre-handoff failures from post-handoff ones. A pre-handoff failure, like a failed agent build, leaves the call just as parked, and without tracking the rebuilt agent answers it with "approval timed out" at the old deadline. The silent settle is the right outcome there too, and tracking every throw keeps the session layer from having to recognize ApprovalDeliveryTimeoutError. Handed-over status is unchanged: a thrown delivery still isn't marked handed over, so recovery retries behave as before.

Tests:

  • e2e an approval whose acceptance was never observed settles with no model turn: the real createApprovalDeliverer with an acceptance that is never observed hands an approval to a busy loop, the wait times out, then interrupt, then still parked in the real store at the rebuild, then one silent settle and no model turn. It fails without the fix with timed out: dropped approval settled.
  • unit an approval whose delivery failed before the hand-over is rejected on the rebuilt agent with no turn covers the pre-handoff side.

bun run check passes: 9527 tests, 0 failures.

@bwachman
bwachman changed the base branch from cl-11433-settle-dropped-approvals-silently to main October 29, 2026 18:17
TheGreatAxios

This comment was marked as duplicate.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants