Skip to content

Fix ctrl+g/ctrl+b excursion re-arm to track needs-input identity, not count (BUG-069) - #909

Merged
anutron merged 2 commits into
masterfrom
argus/ctrlg-excursion-rearm-fix
Jul 25, 2026
Merged

Fix ctrl+g/ctrl+b excursion re-arm to track needs-input identity, not count (BUG-069)#909
anutron merged 2 commits into
masterfrom
argus/ctrlg-excursion-rearm-fix

Conversation

@anutron

@anutron anutron commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • The ctrl+g/ctrl+b "problem-child excursion" (Rework ctrl+g into a problem-child excursion with ctrl+b manual restore #906/Tighten ctrl+g/ctrl+b help-overlay wording #908) re-arm path (excursion == nil after a restore, needs-input count still >=1) froze a fresh snapshot on the very next rebuild regardless of whether anything genuinely new had happened. A stale, never-resolved needs-input role (e.g. an unrelated stuck (?)) kept the count >=1 indefinitely, so every ctrl+b re-armed and froze immediately — silently discarding any fold/selection change the operator made afterward until the next explicit restore.
  • Confirmed against two back-to-back live dogfood repros (verbatim in the originating task): ctrl+b replayed a stale, much-earlier layout instead of the operator's actual prior position, and in the second (clean, controlled) test replayed the FIRST test's world instead of the second test's own manual navigation.
  • Fix: Rail.armedNeedsInputIDs (Model.needsInputRoleIDs, hasNewNeedsInputID) tracks the needs-input set by role identity, not count. While no excursion is held, the baseline keeps refreshing to the current set on every rebuild for as long as it stays a subset of the last capture, and only freezes the instant a genuinely new, distinct role id appears — so the eventual freeze reflects the operator's latest position, never a stale one. The baseline updates unconditionally even while an excursion is frozen, so a role that merely folds into an already-open excursion doesn't look "new" again after a later discharge (would otherwise spuriously re-freeze).
  • Separately confirmed (not fixed — narrow, pre-existing, shared with BUG-002's persisted-selection mechanism): currentRef() has no identity encoding for a cursor resting on an Archive-expando or Freelance-header fold row, so restoreCursor silently no-ops in that specific case. Documented as a known follow-up candidate, not fixed here since the clean repro is fully explained by the identity-tracking bug alone.
  • Updated openspec/specs/hera-view/spec.md's excursion requirement/scenarios (same-feature bug fix, no new change folder per project convention) and context/knowledge/gotchas/hera-view.md to correct the previously-wrong "intentional, not a bug" description of the count-edge re-arm.

Test plan

  • New regression tests in internal/tui/hera/excursion_test.go: TestRail_ExcursionSnapshot_StaleItemDoesNotFreezeAwayManualNavigation (fails against pre-fix code, confirmed live), _DoesNotReArmAfterRestoreWithoutNewEntrant, _EntrantAbsorbedWhileFrozenDoesNotReFreezeAfterDischarge, TestModel_NeedsInputRoleIDs, TestRail_CurrentRef_ZeroOnArchiveExpandoRow (documents the separate confirmed-but-unfixed gap)
  • go build ./..., go vet ./..., make fmt-check, make lint-pr (golangci-lint v2.11.4, 0 new issues)
  • make test-cover-gate (race suite + 88% coverage floor) — green at 88.7%
  • openspec validate hera-view --strict — valid
  • make vuln reports 3 pre-existing stdlib CVEs (GO-2026-5856/5039/5037, unrelated to this change) — CI runs this step with continue-on-error: true per .github/workflows/ci.yml

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

anutron and others added 2 commits July 24, 2026 01:13
… count (BUG-069)

The re-arm path (excursion==nil after a restore, needs-input count still >=1)
froze a fresh snapshot on the very next rebuild regardless of whether anything
new had actually happened. A stale, never-resolved needs-input role (e.g. an
unrelated stuck (?)) kept the count >=1 indefinitely, so every ctrl+b re-armed
and froze immediately, silently discarding any fold/selection change the
operator made afterward until the next explicit restore — confirmed against
two back-to-back live dogfood repros where ctrl+b replayed a stale, much
earlier layout instead of the operator's actual prior position.

Rail.armedNeedsInputIDs (Model.needsInputRoleIDs, hasNewNeedsInputID) now
tracks the needs-input set by role identity: while no excursion is held, the
baseline keeps refreshing to the current set on every rebuild for as long as
it stays a subset of the last capture, and only freezes the instant a
genuinely new, distinct role id appears — so the eventual freeze reflects the
operator's latest position, not a stale one. The baseline updates
unconditionally even while frozen, so a role that merely folds into an
already-open excursion doesn't look "new" again after a later discharge.

Separately confirmed (not fixed, narrow + pre-existing): currentRef() has no
identity encoding for a cursor resting on an Archive-expando or
Freelance-header fold row, so restoreCursor silently no-ops in that specific
case — shared with the BUG-002 persisted-selection mechanism, out of scope
here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
noteExcursionTransition's fresh-interruption arm fired unconditionally
on a Rail's first-ever SetModel call, even when rows didn't exist yet
(right after a TUI launch/relaunch). If a stale needs-input role
already predated the launch, this froze a bogus snapshot: selRef=0
(no rows to resolve a cursor against) and fold state from whatever
the PREVIOUS session had persisted to disk, not this session's
navigation. Live repro: ctrl+b left both panes empty and flipped fold
state to a stale prior-session layout instead of the operator's actual
position.

Fix: skip arming entirely (seed the baseline only) on the literal
first-ever rebuild. Normal arm/re-arm rules apply from the second
rebuild onward; ctrl+g's own EnsureExcursionArmed still arms from the
operator's live position if nothing new interrupts them first.
@github-actions

Copy link
Copy Markdown

Merging this branch changes the coverage (1 decrease, 1 increase)

Impacted Packages Coverage Δ 🤖
github.com/drn/argus/internal/tui 82.69% (-0.03%) 👎
github.com/drn/argus/internal/tui/hera 92.12% (+0.07%) 👍

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/drn/argus/internal/tui/hera/model.go 96.52% (+0.13%) 345 (+12) 333 (+12) 12 👍
github.com/drn/argus/internal/tui/hera/rail.go 92.56% (+0.07%) 874 (+8) 809 (+8) 65 👍

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/drn/argus/internal/tui/ctrlg_test.go
  • github.com/drn/argus/internal/tui/hera/excursion_test.go

@anutron

anutron commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Update: found and fixed a second, related bug (BUG-070) while dogfood-testing the BUG-069 fix above.

Live repro: relaunching the TUI while a stale, never-resolved needs-input role already existed (predating the launch) caused ctrl+gctrl+b to leave both panes empty and flip unrelated orchestrators' fold state to a stale prior-session layout, discarding the operator's actual navigation.

Root cause: Rail.noteExcursionTransition's fresh-interruption arm fired unconditionally on the rail's very first-ever SetModel call — before buildRows had run even once. At that instant currentRef() has no rows to resolve (→ selRef==0, a silent no-op on restore) and the fold-state snapshot reflects whatever the previous session's SetStateStore load persisted, not anything from the current session.

Fix: noteExcursionTransition now early-returns (seeding the needs-input baseline only, no capture) on the literal first-ever rebuild. Normal arm/re-arm rules resume from the second rebuild on; ctrl+g's own EnsureExcursionArmed still arms from the operator's live position if nothing new interrupts them before their first press.

Added regression tests (TestRail_ExcursionSnapshot_ColdStartDoesNotCaptureBogusSnapshot / _ColdStartWithNoPriorNeedsInputArmsNormally), updated openspec/specs/hera-view/spec.md and context/knowledge/gotchas/hera-view.md. make pre-pr clean. Confirmed fixed live via dogfood re-test (relaunch → repeat the ctrl+g/ctrl+b sequence → restores to the actual post-navigation position). CI green on the new commit.

@anutron
anutron merged commit 49c150e into master Jul 25, 2026
1 check passed
@anutron
anutron deleted the argus/ctrlg-excursion-rearm-fix branch July 25, 2026 06:13
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.

1 participant