fix: native CLI menu commands also skip the unreachable Stop-sentinel wait#19
Merged
Merged
Conversation
#1's client-side-command fixes ended up covering two non-overlapping lists: #16 (INSTANT_COMMANDS) skips the Stop-sentinel wait for no-menu commands (/rename, /model, ...), while #14 (qmonitor) drives the on-screen menu for /theme, /rewind, /memory, /plugin install — but nothing told the CAPTURE tape that THOSE turns also never fire a Stop hook. A live tmux probe confirmed selecting a /theme option returns to an empty prompt with zero Stop hook event, exactly like the #16 commands, so gen_capture_tape.py was still emitting Wait+Screen@120s /VHS_TURN_DONE_N/ for them — a sentinel that will never print, timing out the full turn-timeout even though qmonitor was already correctly answering the menu underneath. Adds NATIVE_MENU_COMMANDS (/theme, /rewind, /memory, /plugin install only — other /plugin subcommands don't show the scope-picker menu) matched via a _match_command_table helper shared with INSTANT_COMMANDS, and skips the sentinel Wait for these turns too, marking turn_plan["instant"] = True (the exact same flag/shape #16 already uses, so panel.py's existing `not t.get("instant")` filter and any other downstream consumer keep working unchanged). The settle Sleep is mode-dependent: under --robust/--tmux a real qmonitor.py is driving the menu (poll -> dwell 2.5s -> keystrokes at 0.8s/key -> confirm poll), so the tape sleeps NATIVE_MENU_SETTLE (10s — sized from qmonitor's own defaults with headroom, well short of its 60-90s worst-case ceilings) before moving on. In STANDARD (non-tmux) mode there is no qmonitor at all, so a /theme menu never gets navigated regardless of sleep length — that usage isn't supported by this fix — but we still must not hang on an unreachable sentinel, so it falls back to the short INSTANT_SETTLE there instead. Closes #17. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EwptawkV4dcGhoGefzxgV
htlin222
added a commit
that referenced
this pull request
Jul 5, 2026
… scripted timing (#20) * fix: detect_anchors recovers merged consecutive-instant-turn submissions using known scripted timing Two client-side-only turns back-to-back (e.g. "/context" then "/fast on") have no real thinking-gap between them, so the input-band pixel signal never dips long enough to split them -- they read as ONE continuous group instead of two, undercounting detected prompt submissions and raising in author.py (#18). The fix adds a targeted recovery for the len(groups) < n case: when the turns.json plan already flags a run of >=2 consecutive turns as "instant" (written by gen_capture_tape.py's render(), consumed via plan["turns"]) and the shortfall is EXACTLY explained by that run collapsing into one detected group, split the merged group back apart using the deterministic scripted timing (INSTANT_SETTLE + pre_enter + each turn's type_dur) instead of re-detecting split points from pixels -- there is no pixel signal left to re-detect from. If the shortfall isn't fully explained this way, it still raises: this is a recovery for a known, deterministic failure mode, not a blanket excuse to swallow a genuine detection miss. Regression tests added in tests/test_detect_anchors.py reproduce the exact 3-vs-4 mismatch from the issue with a synthetic merged input-band signal, and confirm the existing "shortfall not explained" case still raises. Closes #18. * fix: consecutive-instant recovery uses each turn's actual settle duration, not a hardcoded constant The just-landed #17/#19 fix (native CLI menu turns, e.g. "/theme") gives those turns the SAME turn_plan["instant"] = True flag/shape as the no-menu INSTANT_COMMANDS from #16, but they can use a LONGER settle (NATIVE_MENU_SETTLE under --tmux) than the no-menu INSTANT_SETTLE. My detect_anchors.py recovery (previous commit) assumed every "instant"-flagged turn used the module's INSTANT_SETTLE constant -- correct for #18's own "/context" + "/fast on" repro, but silently wrong (not raising, just mis-timed) for a merged run that includes a native-menu turn. gen_capture_tape.py now records the settle it actually used per-turn in turn_plan["settle"]; detect_anchors.py's split reads that value (turns[idx].get("settle", INSTANT_SETTLE)) instead of assuming one constant, falling back to the old default for capture.json plans predating this field. Added a regression test with a mixed run (a native-menu-settle turn followed by a plain-instant turn) proving the recovery uses the per-turn value.
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.
Closes #17.
The gap
#1's two client-side-command fixes ended up covering non-overlapping lists:
INSTANT_COMMANDSingen_capture_tape.py): no-menu commands (/rename,/model,/context, ...) — skips the Stop-sentinel wait.qmonitor.py): native on-screen menus (/theme,/rewind,/memory,/plugin install) — drives the menu via passive pane polling.Neither told
gen_capture_tape.pythat the menu commands also never fire a Stop hook once an option is picked (confirmed via a live tmux probe for/theme). So the capture tape kept emittingWait+Screen@120s /VHS_TURN_DONE_N/for those turns — a sentinel that never prints — timing out the full turn-timeout even though qmonitor was already correctly answering the menu underneath.Fix
NATIVE_MENU_COMMANDS(/theme,/rewind,/memory,/plugin install— onlyinstall, since other/pluginsubcommands don't show the scope-picker menu) and_is_native_menu_command, sharing a_match_command_tablehelper with the existing_is_instant_command/INSTANT_COMMANDS.Wait+Screen@.../VHS_TURN_DONE_N/and setturn_plan["instant"] = True— the same flag/shape#16already uses, sopanel.py's existingnot t.get("instant")filter (and any other downstream consumer, e.g. the concurrent Back-to-back instant-command turns collapse into fewer detected prompt-submissions in detect_anchors.py, failing the author stage #18 fix) keeps working without learning a second key.Sleepis mode-dependent:--robust/--tmux, a realqmonitor.pyis driving the menu (poll → dwell 2.5s → keystrokes at 0.8s/key → confirm poll), so the tape sleepsNATIVE_MENU_SETTLE = 10.0s— sized fromqmonitor's own defaults with headroom, well short of its 60–90s worst-case ceilings (sizing to those would defeat the point of not hanging).--tmux) mode there is noqmonitorat all, so a/thememenu never gets navigated regardless of sleep length — that usage isn't supported by this fix, and is a pre-existing limitation, not a regression. But the Stop hook still never fires either way, so it still must not hang on an unreachable sentinel — it falls back to the shortINSTANT_SETTLEthere instead.render()already receives atmuxparam, which is what selects between the two.Testing
Per this repo's "every new failure mode becomes a regression test" ethos: added 3 tests to
tests/test_capture_tape.py(native-menu command detection incl./plugin installvs/plugin list/uninstall; sentinel-skip +NATIVE_MENU_SETTLEunder--tmux; sentinel-skip + fallback toINSTANT_SETTLEwithout--tmux) — all three failed before the fix (AttributeErroron_is_native_menu_command, and the pre-fix behavior asserted against). Full suite: 125 → 128 passed, 0 failed.Test plan
pytest tests/— 128 passed, 0 failed (was 125 before this change)