Repository navigation
fix(bin): stop capturing supervision engine notes as captain messages - #53
Merged
Merged
Conversation
…weep bin/fm-supervision-host.sh runs a headless branch engine beside main in the same per-home transcript directory. Its working notes (stale wake, seq numbers, MAIN, pane ids) were being swept and shown to the reader as chat. The sweep now identifies that engine's transcript by session identity - the session id state/.supervision-host-engine records for it - excludes it from directory sweeps and payload naming, and forgets it from the remembered named transcripts so a home already carrying the stale state self-heals on its next run.
…d by captain-message capture
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
Reply to the captured message "The stale wake was for jt-metal-process-explained, which is on a declared pause. It is holding for..." : "what the fuck why the fuck u use a language and way of communicateing which is hard to understand."
Reply to the captured message "I reported this stale wake as a silent routine outcome (seq 61), so it won't open a turn for MAIN...": "what the fuck"
Context: since this home took the upstream merge (PR 52), a background supervision session runs beside the main firstmate session in the same Claude project directory. Its internal working notes (stale wake, seq numbers, MAIN, pane ids) are being recorded in data/captain-messages.jsonl with source "transcript" and shown in the command center as messages to the reader, sometimes with a wrong project label (project "oc" on a JewelTrek task). Only the main firstmate session's messages are meant for the reader.
What Changed
bin/fm-captain-message-sweep.pynow drops any transcript whose file name matches a supervision engine session id before sweeping. The ids come fromstate/.supervision-host-engine-sessionsplus thesession=line instate/.supervision-host-engine, so the engine's working notes are no longer recorded indata/captain-messages.jsonlor shown in the command center.bin/fm-supervision-host.shappends each engine session id tostate/.supervision-host-engine-sessionsbefore that conversation's turn starts and never removes it, so a conversation stays excluded after a failed turn or after the host rotates to a new one.docs/command-center.md,docs/supervision-host.mdand the operational-home-layout skill describe the new state file and the skip.Risk Assessment
✅ Low: The change is a small additive filter in the sweep plus one idempotent append in the host before the engine's only launch site, and tracing new, resumed, rotated and failed-turn conversations shows each engine transcript is excluded from its first byte while main-session transcripts are still read.
Testing
Stood up a disposable lab home with a real Claude primary on its private tmux socket, parked the real supervision host under it, and let it run two real engine turns (sonnet, then haiku after a model change forced a new conversation). Swept the lab with the target code and the real command center, swept the same real transcripts with the base commit for comparison, captured API output and before/after screenshots, and ran the four existing tests that cover this change. All five scenarios driven live passed and the base commit reproduced the reported leak. Two scenarios are untested live: a really failing engine turn (stub-engine test only) and the real Stop-hook path.
Evidence: Command center /api/messages, target vs base, before and after rotation
Source: Command center /api/messages, target vs base, before and after rotation
Evidence: Sweep during the first engine turn (no engine record yet)
Source: Sweep during the first engine turn (no engine record yet)
engine process alive: yes; engine record present: no sweep rc=0 capture: transcripts=1 new=2 rows: 2, both session 5e77d2cd (main): "The fix landed on main." and "Captain, the supervision host is parked..."Evidence: Rotation and removed-record sweeps, target vs base
Source: Rotation and removed-record sweeps, target vs base
Evidence: Base-commit sweep reproducing the leak
Source: Base-commit sweep reproducing the leak
Evidence: Real host log: two handled engine turns, and the id listed before the first turn's record
Source: Real host log: two handled engine turns, and the id listed before the first turn's record
Evidence: Engine record after turn 1
Source: Engine record after turn 1
Evidence: The four targeted existing tests (non-live; stub engine)
Source: The four targeted existing tests (non-live; stub engine)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-captain-message-sweep.py:761- The fix only excludes the ONE session id currently in state/.supervision-host-engine, and it deletes that transcript's cursor entry (cursor["files"].pop). As soon as the record names a different session or is removed, the previous engine transcript is an ordinary unknown file again and is read from offset 0, so every working note in it since the floor is recorded in one burst. bin/fm-supervision-host.sh does this routinely: rotation after ROTATE_TURNS=20 turns, a main-session key change, an engine/model change, andrm -f "$ENGINE_RECORD"on any failed turn (lines 988, 997). Reproduced in a temp home: recordsession=old-> old.jsonl skipped; record rewritten tosession=new-> next directory sweep appended "stale wake seq 61 old engine note" with source "transcript". This is exactly the behavior the intent says must stop ("Only the main firstmate session's messages are meant for the reader"), delayed by up to 20 turns. Remedy inside the sweep: make the exclusion sticky instead of popping - keep the cursor entry marked as an engine transcript (e.g.{"engine": true}, cleared ofnamed) and drop every such path fromtargetson every run, so a transcript once identified as the engine's is never read after the record moves on. Extend the new test with a record that rotates to a second session id.bin/fm-captain-message-sweep.py:182- The session id is written to state/.supervision-host-engine only AFTER a turn that succeeded (write_engine_recordat bin/fm-supervision-host.sh:987). A new engine conversation (--session-id <fresh uuid>, chosen at line 735) is therefore unidentified for its whole first turn, and permanently if that turn fails (no receipt, unacked rows, timeout, error ->rm -f "$ENGINE_RECORD"), so its transcript is swept by the command-center backstop like main's. Reproduced: record absent or still naming the previous session, new.jsonl in the transcript directory -> "stale wake seq 62 NEW engine note" recorded. Failed turns are the ones most likely to carry confusing notes. The sweep cannot close this alone; the host has to make the session id known before the engine starts (for example an append-only list of engine session ids written in handle_wake beforefm_supervision_engine_turn, which the sweep reads instead of the singlesession=line). That adds new durable state and touches the upstream-owned host script, so the remedy, not the defect, is what needs authorization.bin/fm-captain-message-sweep.py:764- The self-heal only forgets the cursor entry; engine rows already written to data/captain-messages.jsonl (the "stale wake ... seq 61 ... MAIN" messages the intent quotes, including the ones labelled project "oc") are left in the log and the command center keeps showing them. The intent describes them as "being recorded ... and shown"; decide whether already-recorded engine rows should be removed or hidden (rows carrysession, so they can be matched to the engine's session id), or whether stopping new ones is enough.bin/fm-captain-message-sweep.py:882- Simplification: theFM_SUPERVISION_ACTOR == "branch"early return in main() is a second, parallel definition of "this is the engine" that no intent requirement needs. The engine runs with--safe-mode, which loads none of the home's hooks (docs/supervision-host.md:355), so the Stop hook never fires this script from the engine; the leak came from the command-center's directory sweep, which runs without that variable. No test exercises the guard. Recommend removing it and keeping session identity as the single rule.🔧 Fix applied.
1 warning still open:
bin/fm-captain-message-sweep.py:774- Simplification: the cursor purge in sweep() (lines 774-779: collectremembered, delete those entries,write_cursor, recomputenamed) is not needed by any intent requirement. Line 773 already drops every engine transcript fromtargets, including ones the cursor remembers as named, so nothing is ever read from them whether or not their cursor entry survives. The purge only changes the reportednamedcount, and it covers a state that intended usage does not produce: an engine transcript can only becomenamedthrough a Stop-hook payload, and the engine runs with--safe-mode, which loads no hooks. Recommend removing the purge and the two test sections that exist only for it (the payload naming the engine's transcript, and the hand-injectednamedcursor entry) in tests/command-center.test.sh.🔧 Fix applied.
1 info still open:
bin/fm-supervision-host.sh:145- The header's "STATE (all under state/, owned here)" inventory lists every host-owned state file but not the new state/.supervision-host-engine-sessions (append-only, one line per engine conversation, never removed, read by bin/fm-captain-message-sweep.py). Add it to that list so the file's owner and its one outside reader are recorded where the others are.✅ **Test** - passed
✅ No issues found.
bin/fm-lab-home.sh create "$LAB"+tmux -L fm-lab new-session ... -e FM_HOME="$LAB" claude(real primary, two real turns)FM_HOME=$LAB bin/fm-afk-contract.sh enter --words ...then primary ranFM_SUPERVISION_HOST_PRIMARY=claude bin/fm-supervision-host.sh parkin the backgroundAppendedworking [at=...]: step oneto$LAB/state/demo.status-> real engine turn 1 (claude -p ... --session-id eabfa2ac...), host loghandledReadstate/.supervision-host-engine-sessionswhile turn 1 was still running andstate/.supervision-host-enginedid not existCLAUDE_CONFIG_DIR=$LAB/cc python3 bin/fm-captain-message-sweep.py --home $LABduring turn 1, after turn 1, after rotation, and with the engine record moved awayecho 'claude haiku' > $LAB/config/supervision-host+ second status append -> real engine turn 2 on a new conversation (160c7546...)python3 bin/command-center.py --port 18765 --home $LAB->curl localhost:18765/api/messagesand page screenshotsBase commit 5e730b3 (git archive 5e730b3 bin) sweep and command center on port 18766 over the same real transcriptsOnlytest_the_supervision_engines_own_transcript_is_never_capturedandtest_a_transcript_the_payload_names_is_captured_wherever_it_livesfromtests/command-center.test.shOnlytest_away_wake_is_handled_on_the_engine_and_never_reaches_mainandtest_attended_latch_keeps_closes_on_main_and_records_recovery_off_mainfromtests/fm-supervision-host.test.sh✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.