Skip to content

fix: native CLI menu commands also skip the unreachable Stop-sentinel wait#19

Merged
htlin222 merged 1 commit into
mainfrom
fix/native-menu-instant-sentinel
Jul 5, 2026
Merged

fix: native CLI menu commands also skip the unreachable Stop-sentinel wait#19
htlin222 merged 1 commit into
mainfrom
fix/native-menu-instant-sentinel

Conversation

@htlin222

@htlin222 htlin222 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Closes #17.

The gap

#1's two client-side-command fixes ended up covering non-overlapping lists:

Neither told gen_capture_tape.py that 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 emitting Wait+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

  • Added NATIVE_MENU_COMMANDS (/theme, /rewind, /memory, /plugin install — only install, since other /plugin subcommands don't show the scope-picker menu) and _is_native_menu_command, sharing a _match_command_table helper with the existing _is_instant_command/INSTANT_COMMANDS.
  • These turns now also skip Wait+Screen@.../VHS_TURN_DONE_N/ and set turn_plan["instant"] = True — the same flag/shape #16 already uses, so panel.py's existing not 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.
  • 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 = 10.0s — sized from qmonitor's own defaults with headroom, well short of its 60–90s worst-case ceilings (sizing to those would defeat the point of not hanging).
    • 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, 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 short INSTANT_SETTLE there instead. render() already receives a tmux param, 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 install vs /plugin list/uninstall; sentinel-skip + NATIVE_MENU_SETTLE under --tmux; sentinel-skip + fallback to INSTANT_SETTLE without --tmux) — all three failed before the fix (AttributeError on _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)
  • New regression tests fail on the pre-fix code, pass after

#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
htlin222 merged commit 2167f3f into main Jul 5, 2026
5 checks passed
@htlin222
htlin222 deleted the fix/native-menu-instant-sentinel branch July 5, 2026 07:03
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native CLI menus (/theme, /rewind, /memory, /plugin install) still hang the capture wait — gap between #1c and #1d fixes

1 participant