fix(state): keep a blocked turn-end gate until the pane really works again [no-ticket] - #53
Merged
Conversation
…again A closing-block report that says "blocked" was reaching the pane record -- gates, tokens and state labels all applied -- and then losing its state within seconds, so the sidebar never showed a gate badge on any host. Two paths dropped it, both by treating the absence of screen evidence, or the pane's own turn-end repaint, as evidence against the hook: - Retirement armed on the first content change after the blocked report and expired on wall clock alone. Every agent repaints its prompt box the moment the hook returns, so a gate was retired five seconds after it was raised, handing the pane back to the screen fallback (a stale spinner OSC title, read as working). Retirement now needs output that is still arriving a grace period after it started: a resumed turn always is, a turn-end repaint never is. - Both the admission route and the effectiveness check required the agent to be detected on screen. At turn end the prompt box matches no rule -- and the scan is skipped outright while the authority is live -- so the hook was demoted exactly when it knew something the screen could not. Only a *different* detected agent or an observed process exit now contradicts it.
Owner
Author
|
No description provided. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
On three hosts, every pane whose last turn ended with a blocking gate showed
agent_status: working(oridle) inherdr agent list— neverblocked— so the sidebar gate badge never appeared. The gate data itself was correct on the same record (tokens.closing_blocking = "1",state_labels.blocked = "gate",gates: [1 item]), which proves the report was accepted:handle_pane_report_agentonly applies the gates array when the state report was accepted.Confirmed cause
Live evidence on
w1C:pKwhile blocked:screen_detection_skippedwas absent (i.e.full_lifecycle_hook_authority_active() == false) while sibling non-gate panes had ittrue, andherdr agent explain w1C:pKreturnedstate: working, rule: osc_title_working, evidence: "⠐ Prepare coaching kickoff notes with Davud"— a stale spinner OSC title. So the pane still detected claude (the authority was therefore effective), yet the full-lifecycle authority was no longer live, and the screen fallback won.herdr:claudewas ruled out as a competing authority: the installed integration hook only ever sendspane.report_agent_session, never a state report.That leaves
AppEvent::HookAuthorityRetired→retire_blocked_full_lifecycle_hook_authority_at.FullLifecycleHookOutputRetirement::observearmed on the first content change after the blocked report and then expired on wall clock alone, five seconds later, whether or not anything else was written. Ending a turn is itself a write: the agent repaints its prompt box as soon as the Stop hook returns, which bumpsdetection_content_seq(any non-empty PTY chunk does). So every gate was retired ~5s after it was raised, and the pane fell back tofallback_state.Fix
src/pane/agent_detection.rs— retirement now measures the window between the first and the latest content change. Continuing output (a resumed turn: tokens, spinner, counters) still retires the gate after the same grace; a single turn-end repaint followed by silence never does.src/terminal/state.rs—hook_authority_is_effectiveand the closing-block admission arm ofroute_full_lifecycle_hook_reportno longer require the agent to be detected on screen. Absence of detection is not evidence of absence — at turn end the prompt box matches no rule, and the scan is skipped outright while the authority is live. Only a different detected agent or an observed process exit contradicts the hook; suppressed sources keep the stricter presence guard.No protection is weakened: a stale frame still cannot resurrect a finished gate (
visible_working_overrides_idle_hook/detected_working_overrides_idle_hookare unchanged and still guarded byauthority.state == Idle), process exit and agent conflict still release the authority immediately.Tests
New:
a_turn_end_repaint_alone_does_not_retire_a_hook_gate— one repaint burst then silence never retires.a_blocked_closing_block_report_survives_unavailable_screen_detection— blocked report withdetected_agent = Noneand a working fallback stays Blocked (failed before the fix:left: Working, right: Blocked).a_blocked_closing_block_report_ends_when_a_new_turn_retires_it— genuine retirement still returns the pane to Working.a_blocked_closing_block_report_yields_to_a_different_detected_agent,..._yields_to_the_agent_process_exiting— the two real contradictions still win.Updated:
continuing_output_retires_a_hook_gate_after_the_grace_periodandoutput_before_the_first_detection_tick_starts_the_grace_periodnow advance the content seq across the window, matching the new contract.cargo test --bin herdr: 3456 passed, 3 failed — all pre-existing environment flakes unrelated to this diff (app::api::plugins::tests::manifest_action_invoke_injects_plugin_paths,server::headless::tests::render_and_stream_sends_terminal_frame_for_terminal_ansi_client,workspace::tests::generated_workspace_ids_are_short_base32_handles).cargo fmt --checkandcargo clippy --bin herdr --all-featuresclean.