Repository navigation
fix: retire finished Lavish board listeners - #78
Merged
Merged
Conversation
A listener lives as long as its Lavish session, and a choice answer never ends the session, so every board ever armed kept its resident processes. Add fm-procevent-lavish.sh sweep: it retires a listener whose board file or session is gone, or whose board has been idle past FM_BOARD_LISTENER_IDLE_HOURS with no open captain call, and keeps worker-owned, standing, queued-feedback and unacknowledged boards. A board rebuild runs the sweep.
…oard symlink identities
…redundant predicate cache
…e fm-backlog-read-bound.test.sh required incidental diagnostic word order, despite verify correctly rejecting a timed-out read. Removed wording-only assertions across hold, direct verify, reconcile-requests, and migrated-prefix verify; now check bounded completion, failure status, prohibited output/side effects, and no further backend reads after timeout. Updated verification documentation; product code is unchanged. Verification passed: the complete bounded-backlog behavior suite, bash -n, and shellcheck -x. Temporary fixtures were cleaned up. The full CI shard was not rerun
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.
Intent
I would like all bugs to be fixed so tomorrow can be focused entirely to Vernant and not problems preventing Vernant from getting built. A recommended architecture should come with proof.
Context for this change: Lavish board listeners were never retired. Each armed Lavish review board keeps a resident listener (runner, owner guard, poll wrapper, node poll) for as long as its Lavish session lives, and a captain's choice answer does not end the session. 25 listeners were live on the host; 15 of them belonged to boards answered 8-13 days earlier, and 2 pointed at board files that no longer exist, so reconcile relaunched them forever. The change retires listeners whose boards have ended or are older than their work. Hard requirement: never retire a listener whose board still has an open captain card.
What the change does: adds
bin/fm-procevent-lavish.sh sweep [--dry-run], run automatically at the end of each goodbin/fm-bearings-board.sh build. It retires a listener when the board file is gone, Lavish has no session for it, or the session status is ended; otherwise only when the board has been idle for FM_BOARD_LISTENER_IDLE_HOURS (default 48, range 1..8760) and no card key on the board names an open captain call (checked with fm-captain-hold.sh open; exit 0 open or exit 2 cannot-tell both keep). It always keeps the standing Bearings board, worker-owned boards, boards with queued feedback or status feedback, boards with an unacknowledged captured result, and every board when the Lavish store cannot be read. Every uncertain read fails toward keeping the listener. It never ends the Lavish session, andarmbrings a listener back. Design was compared against a manual one-off retire, a generic check in every reconcile cycle, and a self-expiring poll timer; the sweep was chosen because it stops regrowth at lower process cost. Proof is in data/fm-board-listener-retire/proof.md.Known deliberate limits: a card key held only in a secondmate's backlog is invisible to the main backlog (the idle window is the second safeguard); the watchdog's own 9 execs per tick belong to the separate fm-supervision-fork-budget work and are not changed here; retiring the live fleet's listeners is not part of this change. Tests: two behavioral tests in tests/fm-captain-hold-lifecycle.test.sh fail on the old code (unknown command: sweep) and pass now. tests/fm-procevent.test.sh has one host-only failure (setsid missing on macOS) in code this change does not touch.
What Changed
fm-procevent-lavish.sh sweep [--dry-run]and invoke it after successful Bearings builds to retire eligible missing, ended, sessionless, or idle board listeners. Configure the idle window withFM_BOARD_LISTENER_IDLE_HOURS(default 48).Risk Assessment
✅ Low: The retirement path conservatively preserves uncertain evidence, the latest fix distinguishes binding absence from read failures, and no new material defects were substantiated within the authorized scope.
Testing
Live CLI checks verified retirement, process shutdown and revival, automatic build sweeping, protected cards and feedback, activity windows, and fail-toward-keeping errors. All eight focused regression selectors completed across two commands after an initial timeout; a pre-existing quoted-path build limitation required an ordinary-path fixture. Product transcripts, persisted session state, and captured feedback were saved. Successful Beads routing remains untested live, but its unavailable consumer kept safely. No rendered UI changed; no screenshots were needed.
Evidence: Live product command transcript
Source: Live product command transcript
Evidence: Real captured feedback preserved by retirement refusal
Source: Real captured feedback preserved by retirement refusal
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
docs/verification/process-event-sources.md- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
🔧 **Review** - 2 issues found → auto-fixed (3) ✅
bin/fm-procevent-lavish.sh:1120- The sweep can retire supported JavaScript-generated decks with open captain cards. Main's fix: route legacy Lavish captain answers safely #63 includes .live-lavish-validation/ordinary.html:10-28, which creates forms dynamically and submits row.data.question without any static data-lavish-question attribute. Arm accepts this valid script. With an unanswered captain-held question, an idle open session, and no pending capture, the changed hunknext unless exists $attr{"data-lavish-question"}discovers no keys; sweep therefore retires the listener. This contradicts "never retire a listener whose board still has an open captain card." Related changed sites: bin/fm-procevent-lavish.sh:1113 (static-only discovery), :1151 (empty discovery serialized as '-'), :1163 (sentinel skipped), :1276 (shared guard for ended, sessionless, and idle boards), and :1284 (retirement). Establish an exhaustive card-key set at sweep_facts, or classify unsupported/dynamic card discovery as unknown and keep the listener; zero static matches must not prove there are no cards.bin/fm-procevent-lavish.sh:1167- The sweep checks a card's raw key rather than the task its answer would reach. Concrete supported sequence: bind an ordinary board to origin sample-scout, retain its legacy card key third-choice, and leave sample-scout-decision-third-choice captain-held while the board becomes idle. The changed hunkfm-captain-hold.sh open "$key" --distinguish-absentreturns 3 for third-choice, allowing retirement although the answer intake resolves that key to the open derived task. This routing is documented in docs/captain-hold-lifecycle.md:468 and exercised at tests/fm-captain-hold-lifecycle.test.sh:2333-2345. Migrated Beads identities have the same mismatch through bin/fm-captain-hold.sh:739-755. This contradicts "never retire a listener whose board still has an open captain card." Related changed sites: bin/fm-procevent-lavish.sh:1164 and :1170 (cache keyed only by raw key), :1174 (absence authorizes retirement), and :1276 (caller supplies no source binding). Reuse the captain-hold owner's exact/legacy/migrated resolution precedence in a read-only bound-key predicate, keeping unresolved evidence; cache by resolved task or binding plus key.🔧 Fix applied.
4 issues (2 errors, 2 warnings) still open:
bin/fm-procevent-lavish.sh:1120- The sweep can retire supported JavaScript-generated decks with open captain cards. Main's fix: route legacy Lavish captain answers safely #63 includes .live-lavish-validation/ordinary.html:10-28, which creates forms dynamically and submits row.data.question without any static data-lavish-question attribute. Arm accepts this valid script. With an unanswered captain-held question, an idle open session, and no pending capture, the changed hunknext unless exists $attr{"data-lavish-question"}discovers no keys; sweep therefore retires the listener. This contradicts "never retire a listener whose board still has an open captain card." Related changed sites: bin/fm-procevent-lavish.sh:1113 (static-only discovery), :1151 (empty discovery serialized as '-'), :1163 (sentinel skipped), :1276 (shared guard for ended, sessionless, and idle boards), and :1284 (retirement). Establish an exhaustive card-key set at sweep_facts, or classify unsupported/dynamic card discovery as unknown and keep the listener; zero static matches must not prove there are no cards.bin/fm-procevent-lavish.sh:1167- The sweep checks a card's raw key rather than the task its answer would reach. Concrete supported sequence: bind an ordinary board to origin sample-scout, retain its legacy card key third-choice, and leave sample-scout-decision-third-choice captain-held while the board becomes idle. The changed hunkfm-captain-hold.sh open "$key" --distinguish-absentreturns 3 for third-choice, allowing retirement although the answer intake resolves that key to the open derived task. This routing is documented in docs/captain-hold-lifecycle.md:468 and exercised at tests/fm-captain-hold-lifecycle.test.sh:2333-2345. Migrated Beads identities have the same mismatch through bin/fm-captain-hold.sh:739-755. This contradicts "never retire a listener whose board still has an open captain card." Related changed sites: bin/fm-procevent-lavish.sh:1164 and :1170 (cache keyed only by raw key), :1174 (absence authorizes retirement), and :1276 (caller supplies no source binding). Reuse the captain-hold owner's exact/legacy/migrated resolution precedence in a read-only bound-key predicate, keeping unresolved evidence; cache by resolved task or binding plus key.tests/fm-captain-hold-lifecycle.test.sh:4885- This run's Round 1 R13 fix leaves the recent-board-activity fixture unable to detect a regression.freshhas no static card keys, so it now remains registered because discovery is unknown even if sweep stops including the board's mtime in activity. Related sites: tests/fm-captain-hold-lifecycle.test.sh:4942 (session without updated_at), :4971 (recent-touch assertion), and :4985 (registration assertion). Give this fixture the existing resolved closed-card key, leaving recent board mtime as its only keep condition; verify the active-window reason so unknown discovery cannot satisfy the assertion.bin/fm-procevent-lavish.sh:1175- Simplification: Round 1's R14 fix changes the cache identity to source:key, but each source is visited once and its card keys are already deduplicated at :1137. Consequently, this cache cannot hit during an ordinary sweep and adds matching and accumulation without saving any predicate calls. No intent requirement needs this unused component. Remove the cache branches at :1175-1186 and initialization at :1196, directly retaining the predicate's exit-1 check. Also remove the caching claim at docs/configuration.md:2450.🔧 Fix applied.
1 error still open:
bin/fm-captain-hold.sh:2054- Round 1's R14 fix leaves binding-lookup uncertainty behind. Concrete sequence: bind an idle static board keyed third-choice to origin; its Beads graph contains a closed migration-note row for third-choice and an open migration-note row for origin-decision-third-choice. With readable binding evidence, resolution is ambiguous and keeps the listener. Remove search permission from state/decision-bindings while leaving the backlog, registry and inbox accessible. read_binding's existence check at bin/fm-captain-hold.sh:1253 returns false for EACCES and succeeds with an empty origin. The new hunkorigin=$(read_binding "$source") || return 2accepts that result; migration discovery considers only the closed raw-key row, and open-bound returns 1, permitting retirement. This contradicts "Every uncertain read fails toward keeping the listener." Related sites: bin/fm-captain-hold.sh:673 (origin-derived migration candidate), :753 (legacy candidate), :2055-2056 (resolution and closed verdict); bin/fm-procevent-lavish.sh:1175 (predicate consumption), :1274/:1276/:1279 (sessionless, ended and idle paths), :1281 (shared card guard), and :1289 (retirement). At the shared binding-read boundary, distinguish confirmed absence from lookup errors; preserve ordinary unbound behavior but return uncertainty for EACCES and other non-absence failures.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bash tests/fm-captain-hold-lifecycle.test.sh test_sweep_retires_only_finished_board_listeners test_sweep_keeps_recently_handled_boards_in_literal_home_paths test_sweep_keeps_unknown_inbox_evidence test_sweep_keeps_unknown_card_discovery test_sweep_resolves_bound_captain_keys test_sweep_resolves_migrated_captain_keys test_sweep_stops_a_live_listener_of_a_finished_board test_sweep_preserves_rearmed_registration_generations— the first four selectors passed before the 600-second command timeout.bash tests/fm-captain-hold-lifecycle.test.sh test_sweep_resolves_bound_captain_keys test_sweep_resolves_migrated_captain_keys test_sweep_stops_a_live_listener_of_a_finished_board test_sweep_preserves_rearmed_registration_generations— remaining selectors passed with a longer command bound.Started reallavish-axi server --port 53341 --verbosewith workspace-local state, loopback binding, and browser opening disabled; drove the public Firstmate CLIs against disposable homes.Ranfm-procevent-lavish.sh arm,sweep --dry-run,sweep, andfm-procevent.sh reconcile; observed exact resident poll processes, registration removal, unchanged Lavish session-store checksum, and revival througharm.Ranfm-bearings-board.sh buildagainst a disposable payload. An initial literal-path attempt hit the unchanged quoted-session-listing limitation; an ordinary-path fixture successfully exercised automatic retirement and standing-board protection.Created exact and legacy captain holds throughfm-captain-hold.sh, exercised idle, ended, and sessionless boards, recorded answers, and verified the subsequent retirement transitions.Submitted feedback to the real Lavish prompts API; inspected durable captured results and exercisedhandledand identity-conditional retirement.Exercised malformed session state, inbox mode 0300, an inbox-file stat EACCES, unreadable board contents, and decision-binding lookup EACCES; verified logged keep decisions and locked retirement refusals.Exercised the default idle window, a one-hour override, accepted endpoints 1 and 8760, and rejected values 0, 8761, and invalid text.Downloaded and SHA256-verified workspace-local Beads 1.3.1, initialized a real embedded-Dolt graph, and attempted Beads operations through tasks-axi 0.2.6. The consumer rejected that backend; the actual sweep retained its listener.Stopped the isolated Lavish server and private tmux server, verified no owned resident polls remained, and removed all disposable fixtures and downloaded tools.✅ **Document** - passed
✅ No issues found.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.