fix(pane): retire a blocked turn-end gate only on sustained output [no-ticket] - #54
Conversation
A blocked closing-block gate was retired by any second content change at least a grace period after the first, so one isolated late write cleared a badge the human was still reading: a background job printing a line, an OSC synchronised-update sequence, or a herdr window resize. Measure a run of content changes instead. A silence longer than the quiet reset starts a new run, so retirement now needs output that keeps arriving the way a resumed turn does. Resizes are excluded at their own callsite by moving the retirement baseline, since they are herdr's write and not the agent's, and still invalidate the idle scan skip.
The closing-block gate is raised on panes whose prompt box matches no detection rule, so the pane has no detected agent. Ending that gate with the next turn's report went through process_not_contradicted untested. Both the idle and the working report end it.
…t loops The spawn path and the restored/attached path carried the same retirement wiring verbatim, so a fix could land on one loop only. Both now call one helper, covered by tests over the helper itself.
|
No description provided. |
|
Correction to the flake-triage claim in this PR's description: it was disproved during review. Post-merge verification (herdr a54fe79, protocol 19), on all four hosts — Mac, ub1, ub2, mbair: a pane that receives a closing-block report with 1 blocking gate, immediately after a burst of turn-end repaint output, holds |
Follow-up to #53 (688143a). Three commits, one concern each.
1.
fix(pane): a single stray late write no longer retires a gate#53 measured first content change to latest content change, which is the rule
"any second change at least a grace period after the first" -- not "output is
still arriving". Confirmed failure: gate raised, turn-end prompt repaint arms
the window, pane quiet for 30s, then one content-seq bump retires the gate
instantly. Sources of that bump include a background job printing a line, an
OSC/synchronised-update sequence, and a herdr window resize -- so resizing the
herdr window silently cleared a blocked badge the human was still reading.
Retirement now measures a run of content changes: a silence longer than
FULL_LIFECYCLE_HOOK_OUTPUT_QUIET_RESET(2s) starts a new run, and a run hasto span the existing 5s grace before the gate goes. The detect loop ticks every
300ms and a real turn writes on essentially every tick, so 2s is six ticks of
headroom while the multi-second silences around isolated writes are rejected.
Resizes are excluded at the callsite rather than by a time heuristic:
resizestill bumps
detection_content_seq(the idle scan skip must be invalidated)but now also moves the retirement baseline, because it is herdr's own write.
That keeps a "not agent output" fact out of the timing window entirely, which
is the only place that distinction is actually known.
Properties held, each with a test:
a_turn_end_repaint_alone_does_not_retire_a_hook_gate)an_isolated_late_write_does_not_retire_a_hook_gate)isolated_writes_never_accumulate_into_a_retirement)continuing_output_retires_a_hook_gate_after_the_grace_period,output_resuming_after_a_pause_restarts_the_grace_period)a_resize_does_not_arm_retirement_of_a_blocked_gate)Blocked stays exitable: forwarded input, a contradicting detected agent, a
process exit and the next hook report all still end it, unchanged.
2.
test(state): AC4 gap from the #53 reviewA fresh non-blocked closing-block report must end a Blocked state when screen
detection is unavailable (
detected_agent == None) -- the exact panes the gateis raised on, since a claude prompt box matches no detection rule. The path
(
route_full_lifecycle_hook_report->process_not_contradicted->set_hook_authority_at->recompute_effective_state) was believed correct bycode reading and untested.
It is correct: both new tests passed first run, no defect found. Covered for
the idle report and the working report.
3.
refactor(pane): one retirement poll, not twohook_output_retirementwas wired verbatim in both detect loops (spawn pathand restored/attached path) with no test over either, so a future fix could
land on one loop only. Both now call
poll_full_lifecycle_hook_retirement;behaviour is identical, and the helper has its own tests.
Verification
cargo test --bin herdr: 3567 passed. 12 failures are parallel-isolationflakes in unrelated modules (
detect::manifest*mutateXDG_CONFIG_HOMEprocess-globally, plus
server::headlesskeybindings andworkspace::generated_workspace_ids); all 12 pass under--test-threads=1.cargo fmt --checkclean,cargo clippy --bin herdr --all-featuresclean(also clean with
--tests).