Repository navigation
fix(hooks): release a hold when the turn ends, not when the answer arrives - #396
Conversation
CLOUD-511 Two handoffs in one turn: the hold's tool-path release disarms the second, so `ExitPlanMode` after `AskUserQuestion` is unguarded
Why CLOUD-485 landed It also makes the second handoff in a turn unguarded, and the plan-mode workflow actively prescribes two: use Measured 2026-08-13, one turn, task ids from the harness:
Why it is worth more than a re-arm. Here the guard was wired, so the failure surfaced as a visible refusal costing one extra tool call. With The chosen event is what produced it, and CLOUD-485's own Ready block named the alternative. That block recommended a Related but not this. CLOUD-491 is whether a live hold defers the reclaim at all and that nothing records it was live. CLOUD-500 is that the hold's occupancy does no I/O. Both are about a hold that exists; this is about a hold that has been correctly released while the turn still needs one. Searched for an existing home before filing. The only open, unpulled sibling is CLOUD-500 (Todo — the hold's occupancy does no I/O), and that is a different object: it asks whether a live hold is doing enough to keep the container alive, while this asks why a hold that was correctly released leaves a handoff still to come unguarded. A fix for either changes nothing about the other. Everything else in the family — CLOUD-485, CLOUD-451, CLOUD-491 — is In Review, so it cannot host this: adding scope to a landed issue is the "reopen a closed-out ticket" failure the board avoids. Ready
Acceptance
CLOUD-451 A turn that hands control to a human ends idle, so the VM is reclaimed while the human is still typing — and their input is destroyed
Waiting on a human is the one legitimate idle turn end, and it is the only one with no mechanism. What happensA session calls So the review step this repo depends on — a human reading a plan before an autonomous agent executes it — is not merely inconvenient, it is anti-correlated with care: the longer the human reads, the likelier their input is destroyed. Why nothing catches it
A hook cannot fix it alone. The hold has to be a real backgrounded tool call the harness knows about; a Mechanism
Refinement — ReadySpecializations only; the shared clauses live in Definition of Ready & Done.
Acceptance
Not in this issueWhether every other idle turn end should arm the same hold. The predicate would be broader and the enforcement point different ( |
…rives CLOUD-485 moved the hold's release onto the tool path so a human answering AskUserQuestion or approving ExitPlanMode releases it. Correct, and it made the second handoff in a turn unguarded: the plan-mode workflow prescribes clarify then propose, and the first answer removed the sentinel the second handoff is judged by. Measured as a live guard refusal; silently, wherever the guard does not reach, it is an idle turn end with no hold at all — the reclaim CLOUD-451 exists to prevent, arriving through its own fix. Release now needs BOTH conditions, which stopped being the same moment: the human answered, and the turn is over. The tool path records the answer as a mark; a new Stop hook releases only when that mark is present, then spends it so one answer licenses one release. No new PreToolUse entry, which the issue's own Ready block first proposed: that event is paid on every Bash call, CLOUD-435 deleted six of them over ~150ms of task-runner startup each, and CLOUD-479 is removing that same class of cost. Stop fires once per turn, so this costs a test -e per turn. Invoked by path. The mark lives BESIDE the hold directory, never inside it, for the reason CLOUD-491 already documented about its heartbeat: both loops iterate the hold directory and delete every file whose first line is not a pid, so a mark filed inside would be reaped by the check that reads it. Also closes the manual path's hang, absorbed into this issue rather than filed apart since it is the same two files: both release paths read a bare `cat`, which is right for a hook whose pipe the harness closes and blocks forever when invoked by hand with no redirect — measured at exit 143 after the harness's ~2-minute kill, in a session whose container is what the hold protects. Bounded now, and it reports that it released nothing rather than exiting 0 in silence. Shown able to fail in both directions, each reddening a different case: revert the mark and the two-handoff case reds; release at turn end unconditionally and the unanswered-turn case reds, which is CLOUD-485's anti-vacuity row. Refs: CLOUD-511
7968bb4 to
07223cd
Compare
|
|
Heads-up before you spend more CI here: the repo owner has asked for The six The reasoning, short version: CLOUD-491 measured a live hold failing to defer a container restart twice; CLOUD-500 conceded the occupancy premise is untested and gated its own stage 1 on a Nothing here is wasted if the mechanism comes back: restoring it is a Generated by Claude Code |
|
/fast-forward |



Closes CLOUD-511.
CLOUD-485 moved the hold's release onto the tool path, so a human answering
AskUserQuestionor approvingExitPlanModereleases it. That was correct and itclosed CLOUD-451's third acceptance bullet — and it made the second handoff in a
turn unguarded, because the plan-mode workflow prescribes exactly two: clarify
with
AskUserQuestion, then propose withExitPlanMode.Measured in one turn, from the harness's own task ids:
plan-holdarmed (bm78255x5)1 hold(s) liveAskUserQuestionansweredExitPlanMode, same turnbk0tm0tc3), retriedWith the guard wired that costs one extra tool call and is visible. With
BATTEN_PLAN_HOLD_BYPASS=1, or on any handoff the guard does not reach, the samesequence ends the turn idle with no hold — the reclaim CLOUD-451 exists to
prevent, arriving through its own fix, with nothing announcing it.
Release now needs both conditions
They stopped being the same moment: the human answered and the turn is over.
plan-hold-release-tool(PostToolUse, already wired) records the answer as amark instead of removing the sentinel.
plan-hold-release-turnonStopremoves the sentinel only when the markis present, then spends the mark, so one answer licenses exactly one release.
plan-hold-release(UserPromptSubmit) is unchanged: a typed prompt is both ananswer and a new turn, so both conditions already hold there.
ExitPlanMode→ turn ends, nobody has answeredAskUserQuestionanswered →ExitPlanModesame turnNo new
PreToolUseentry, which the issue's own Ready block first proposed.That event is paid on every Bash call: CLOUD-435 deleted six of them over ~150ms of
task-runner startup each, and CLOUD-479 is currently removing that same class of
cost.
Stopfires once per turn, so this costs atest -eper turn, invoked bypath.
The mark lives beside the hold directory, never inside it — the reason
CLOUD-491 already documented for its heartbeat: both loops iterate the hold
directory and delete every file whose first line is not a pid, so a mark filed
inside would be reaped by the check that reads it. CLOUD-491's poll/exit records
keep their present meaning, or its sensor would start lying.
The manual hang, absorbed rather than filed apart
Both release paths read a bare
cat. Right for a hook — the harness pipes JSON andcloses it. Invoked by hand with no redirect it blocks forever: measured this
session at exit 143 after the harness's ~2-minute kill, in a session whose
container is the thing the hold protects. It is reachable by following the guard's
own deny message, which points at the manual path. CLOUD-485 recorded the adjacent
case (empty stdin → silent no-op); this is the stronger neighbour it missed.
Bounded now, and it reports that it released nothing instead of exiting 0 silently.
Same two files as the change above, which is why it rides here rather than in a
ticket of its own.
Verification
tests/plan-hold.bats25/25. One existing case was rewritten, not deleted —"answering a handoff tool releases the hold" becomes "…marks the answer and leaves
the hold standing", because the property still holds one step later.
Mutation-checked in both directions, each reddening a different specific case:
anti-vacuity row, the one whose failure would silently restore the reclaim).
A gate seen failing one way is half-tested, which is why both are recorded.