ReviewOS

stacks/harness

A client that dies mid-approval wedges the turn

#8
Closed chris opened this 9 days ago ·
9 days ago

Found while testing checkpoint revert.

An agent requests approval, the client that would answer goes away — killed, crashed, closed the laptop — and the session sits running forever. Nothing times out and nothing notices.

Concretely, from a real session's log:

turn.started
checkpoint.captured
tool.call.began     · approval.requested · approval.resolved · tool.call.ended
tool.call.began     · approval.requested ← nobody ever answered
(no turn.completed)

Why it matters beyond being stuck

A wedged turn is not just idle, it blocks recovery: session.checkpoint.revert is deliberately refused while a turn is running, so the one action that would undo the agent's half-finished work is unavailable exactly when you want it.

Stop settles it — the button exists and works, and clicking it moved the session to idle and revealed the revert control. But a turn should not depend on a person noticing that it is stuck.

Options

  • A timeout on an unanswered approval, defaulting to deny. Simple, and denial is the safe direction, but it punishes someone who stepped away for coffee.
  • Deny pending approvals when the last subscriber for a session disconnects. Wrong on its own — the desktop window and a phone are both legitimate clients and either may reconnect.
  • Surface the pending approval as session state (awaiting-approval already exists in SessionState['state'] and nothing sets it), so any client that connects sees there is a decision waiting and can answer it. This is probably the right one: it makes the state visible rather than guessing at intent.

The third option also fixes a smaller thing: awaiting-approval is declared in the projection today and never set, so the UI cannot distinguish "thinking" from "waiting for you".

9 days ago

Fixed in 0feed5e..f80f0c5.

Both halves:

  • Client died, server lived — the pending approval is projected onto the session and rendered server-side, so any client can answer it. Verified by loading a wedged session in a browser that was never connected when the approval fired.
  • Server restarted — serve() closes out turns left in flight, recorded as turn.interrupted rather than quietly rewritten. Verified live.

Also fixed the display bug this exposed: the island collapsed every non-running state to idle, so a session waiting on you looked idle and offered Send instead of Stop.

Sign in to comment on this issue.