Repository navigation
fix(bin): report wake drain presentation failures on stdout - #34
Merged
rub-a-dub-dub merged 7 commits intoSep 27, 2026
Merged
Conversation
…annotation notices
…hold every cursor
…n architecture.md
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
Captain's words, 2026-09-26: Can we dispatch the two Firstmate PRs on Codex? - authorizing this queued item. Earlier, at PR 28 on 2026-09-23, the captain said ship now, which deferred these cases here so that PR stopped growing one path per review round.
Redesign, as one piece, how bin/fm-wake-drain.sh reports its own failures. Known cases from the PR 28 review runs:
Goal: one consistent contract - every drain either presents the computed sections or states plainly on stdout what it could not compute and why, and never mislabels a complete presentation as incomplete.
What Changed
print_status_presentationinbin/fm-wake-drain.shnow routes every failure that leaves a drain with nothing to present — lock acquisition, fleet snapshot read, and the annotation pass — through a single stdout notice that names the failed operation, instead of the lock case writing only to stderr and returning 1; such a drain skips the acknowledge and commit passes so no presentation cursor advances. A separateprint_status_receipt_failure_noticecovers the case where all prepared section bytes reached stdout butstatus_commit_presentation_snapshotfailed, so a complete presentation is no longer relabeledSTATUS PRESENTATION INCOMPLETE, andprint_status_sectionspasses a per-failure reason into the incomplete notice.status_open_decisions_incrementalinbin/fm-classify-lib.shdrops thetrusted_openreplay path: an ident, stat, size, or span-read failure now returns 1 without printing the persisted open-decision set (leaving it untouched on disk for a later recovery call), rather than returning rc 0 with a possibly stale non-empty set.fm_wake_unread_eventsinbin/fm-wake-lib.shreturns 2 for a span it could not read versus 1 for a span with nothing unread, andfm_wake_print_annotationsreturns 1 when a live, readable status file's cursor read fails or its span read returns 2 — replacing the|| continuethat dropped a live row's annotation silently — while a missing, unreadable, or symlinked status path is still skipped without being called a failure.docs/architecture.mdrecords the resulting contract, andtests/fm-wake-queue.test.shplustests/fm-wake-drain-open-decisions.test.shadd cases for the receipt-failure label, the stale fold set, and the annotation cursor/span failures holding every cursor.Risk Assessment
Testing
I read the diff to pin the four intent cases, then validated each one end-to-end by driving the real bin/fm-wake-drain.sh over crafted state fixtures with the same injected failure against both the base build (d32fe93) and the target build (8618dcd), capturing the captain-facing stdout as before/after CLI transcripts: a receipt-only failure now keeps its computed sections and reports a distinct RECEIPT FAILED notice instead of labelling them incomplete; a non-124 lock failure now speaks on stdout instead of stderr alone; a stale non-empty open-decision set is no longer printed as authoritative; and an annotation cursor failure yields exactly one generic notice, moves no presentation cursor, and the following drain replays the annotation that never printed. On the automated side I ran every test in tests/fm-wake-drain-open-decisions.test.sh, the new and adjacent annotation and presentation-lock tests in tests/fm-wake-queue.test.sh, and full clean passes of the three neighbouring drain suites plus the two classify suites that exercise the changed fold - all pass. I also ran the eight new or reworded tests against a base-commit checkout, where each fails with its own intended assertion, confirming they are genuine regression tests. One environmental problem surfaced and is documented rather than fixed: this host intermittently SIGSEGVs LC_ALL=C /usr/bin/stat (~5% of forks under the current load), which fails individual drains and makes whole-file suite runs flake at random tests - it reproduces identically at the base commit, so it predates the change; I worked around it by retrying per test.
Evidence: Intent case 1 - receipt-only failure no longer mislabels a complete presentation (base vs target drain stdout)
Source: Intent case 1 - receipt-only failure no longer mislabels a complete presentation (base vs target drain stdout)
Evidence: Intent case 2 - lock-acquire failure now reported on stdout (base vs target, stdout and stderr shown separately)
Source: Intent case 2 - lock-acquire failure now reported on stdout (base vs target, stdout and stderr shown separately)
BEFORE (base d32fe93) stdout: <wake row only> stderr: wake drain: status presentation lock could not be acquired safely AFTER (HEAD 8618dcd) stdout: <wake row> + STATUS PRESENTATION INCOMPLETE: status presentation lock could not be acquired safely; no status annotations or fleet-wide status sections were computed this drain, nothing was marked as seen and no presentation cursor advanced, so every unread status line is still unread.Evidence: Intent case 3 - a stale non-empty open-decision set is no longer printed as authoritative
Source: Intent case 3 - a stale non-empty open-decision set is no longer printed as authoritative
BEFORE: OPEN DECISIONS … firstmate-reconcile-fork-with-upstream [key=old-choice] needs-decision: choose the old route (already resolved in the log; the new [key=new-choice] decision is hidden) AFTER: STATUS PRESENTATION INCOMPLETE: unread status, outcome backstop, OPEN DECISIONS, and record divergence could not be fully computed this drain (a status log, cursor, or prepared-output write could not be completed); …Evidence: Intent case 4 - annotation failure is never silent and no cursor advances (includes the next drain for both builds)
Source: Intent case 4 - annotation failure is never silent and no cursor advances (includes the next drain for both builds)
BEFORE: drain prints no notice and no annotation; the NEXT drain shows nothing - 'working: live row must not disappear silently' is lost. AFTER: drain prints one STATUS PRESENTATION INCOMPLETE: a supplemental status annotation could not be computed …; the NEXT drain prints 'wake annotation: unread wake-EVENT since last drain, not current state: task.status: working: live row must not disappear silently'.Evidence: Targeted test results, per test, plus the same new tests failing at the base commit
Source: Targeted test results, per test, plus the same new tests failing at the base commit
Evidence: Host flake write-up: /usr/bin/stat SIGSEGV reproducer and proof it predates the change
Source: Host flake write-up: /usr/bin/stat SIGSEGV reproducer and proof it predates the change
$ for i in $(seq 1 300); do bash -c 's=$(LC_ALL=C /usr/bin/stat -f "%z" /tmp/probe2.status 2>/dev/null); echo $?'; done | sort | uniq -c 284 0 16 139Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-wake-drain.sh:414- For a task the annotation pass held, the UNREAD STATUS header's promise is false.print_unread_status_sectionreceives only$snapshot(bin/fm-wake-drain.sh:646 passes$annotation_heldtoprint_status_sectionsbutscan_unread_surface_snapshotat bin/fm-classify-lib.sh:1663 has no knowledge of it), so it prints the held task's unreadnote:lines under 'UNREAD STATUS (new since last drain, not re-printed after this presentation)' whilestatus_acknowledge_presented_snapshot(bin/fm-classify-lib.sh:1417-1436) sets hold=true, skips the span scan, and forcesendpoint=$offset- the cursor does not move, so the very next drain re-prints those same lines. The change's own fixture proves the sequence:test_annotation_cursor_failure_is_reported_on_stdout(tests/fm-wake-queue.test.sh:1868) primes the cursor, appendsworking: live row must not disappear silentlyplusnote: the captain reads this one through the fleet-wide section, fails the annotation's firstcatof.status-presentation-cursor(read at bin/fm-classify-lib.sh:1123), and then asserts thenote:line IS on stdout (tests/fm-wake-queue.test.sh:1922) while asserting the span is retried on the next drain. So the note is printed under a 'not re-printed' header and is guaranteed to be re-printed. No data is lost - the direction is duplication - but a captain-facing section states a guarantee this drain then breaks, which is exactly the mislabeling the intent's 'never mislabels' goal targets. Needs the author's call because every remedy is a user-facing behavior choice: narrowest form is to pass the held list intoprint_unread_status_section/scan_unread_surface_snapshotand omit held tasks from that section for the drain (the STATUS ANNOTATION INCOMPLETE notice already tells the captain those bytes are coming back); alternatives are softening the header for any drain that held a cursor, or holding only the non-unread-surface part of the span.bin/fm-wake-drain.sh:627- The STATUS ANNOTATION INCOMPLETE notice promises 'every status line those annotations would have carried stays unread - retry on the next drain', but the held set is in-memory for one drain only (FM_WAKE_ANNOTATION_HELDat bin/fm-wake-lib.sh:2247, read at bin/fm-wake-drain.sh:628), and the retry depends on the wake row still being queued. Concrete ordinary sequence, using the change's own fixture shape: drain N holds tasktask(cursor read fails), prints the notice, and leaves the cursor at the previous offset; the drain then prints WAKE_ACK_REQUIRED on stderr, so the next thing the caller does is--ack-through <seq>, which removes the row and exits at bin/fm-wake-drain.sh:801 without ever callingprint_status_presentation; the following drain takes the empty-queue path (bin/fm-wake-drain.sh:803-820) and callsprint_status_presentationwith no rows, so the annotation pass never runs,annotation_heldis empty, andstatus_acknowledge_presented_snapshotapplies the plain fleet rule - the span [held offset, endpoint) containsnote: the captain reads this one through the fleet-wide section, whichstatus_line_is_unread_surfaceaccepts, so safe=true and the cursor jumps to the endpoint, past theworking:line whose only surface was the annotation. That line is then never annotated and the promised retry never happens. The pre-existing fleet rule already accepts dropping routine lines once an unread-surface line in the same span is presented, so the defect introduced here is the notice's claim, not the drop. Classified ask-user because the smallest honest remedy is a choice between extending the change with durable state (persisting the held set across drains so the retry is real) and narrowing the notice's wording to what the drain actually guarantees - the remedy, not the defect, is what needs authorization.bin/fm-wake-drain.sh:635- The prior fix round introducedif [ "$annotation_rc" -eq 0 ] || [ -n "$annotation_held" ]to gate computingfully_presented. The|| [ -n "$annotation_held" ]disjunct is the part no intent requirement needs, and it keeps a gap open:fm_wake_print_annotationshas a failure return that names no task while held is already non-empty -printf '%s\n' "$line" || return 1at bin/fm-wake-lib.sh:2407 aborts the manifest loop. With task A already inFM_WAKE_ANNOTATION_HELD(cursor or span read failure) and task B's annotation write then failing,annotation_heldis non-empty sofully_presentedis computed from the raw manifest as everydirecttask - including B, whose annotation write failed, and every task after B that the aborted loop never reached.status_acknowledge_presented_snapshotthen sets safe=true for those tasks and advances their cursors past spans the annotation never printed, which is precisely the loss the hold mechanism exists to prevent and makes the notice's 'stays unread' claim false. Reachability is a double fault (a transient read failure plus a stdout write failure with stdout on a different filesystem than $STATE, since SIGPIPE kills the drain outright and an ENOSPC $STATE fails the earlier mktemp), so I am not claiming this is common. The point is that the narrower gateif [ "$annotation_rc" -eq 0 ]satisfies the intent on its own and closes the gap structurally: on any annotation failure no task is claimed fully presented, every cursor falls back to the unread-surface rule, and held tasks are still frozen by the hold branch. The only cost is that a fully annotated sibling task may replay its annotation on the next drain - duplication, not loss. Recommend removing the disjunct rather than adding a third repair to this machinery.🔧 Fix: omit held unread lines, narrow annotation notice and gate
3 issues (2 warnings, 1 info) still open:
bin/fm-wake-drain.sh:647- When the fleet snapshot read fails, the annotation pass is skipped entirely and no STATUS ANNOTATION INCOMPLETE notice is printed, so absent annotations read as "nothing unread" under the contract this change establishes. Concrete sequence: task T's presentation cursor is at O andworking: rebasing onto upstreamhas been appended since O; a directsignal:row for T is queued. Concurrently task M is torn down, so_fm_open_decisions_file_identfails mid-loop instatus_presentation_snapshot(bin/fm-classify-lib.sh:1035) and it returns 1. The drain prints T's raw wake row, then onlySTATUS PRESENTATION INCOMPLETE: status snapshot could not be read.(bin/fm-wake-drain.sh:647), setssnapshot=andrc=1; the[ "$rc" -eq 0 ] && [ -n "$rows" ]gate at bin/fm-wake-drain.sh:650 then skipsfm_wake_print_annotationsoutright. Stdout carries nowake annotation:line and noSTATUS ANNOTATION INCOMPLETE. This change made those two the complete set of outcomes for the annotation pass - the lock branch at bin/fm-wake-drain.sh:635 spells out "no status annotations or fleet-wide status sections were computed this drain", and the annotation branch at bin/fm-wake-drain.sh:655 names its own - so the snapshot branch is the one place that goes silent about annotations. The captain reads T as having nothing unread while aworking:line whose only surface is that annotation went unpresented, which is the intent's "never mislabels" goal. Remedy is a user-facing wording/behavior choice: callprint_annotation_incomplete_notice ''on this branch when$rowsis non-empty, or extend the snapshot notice to state that annotations were not computed either.bin/fm-wake-drain.sh:669- The fix round's comments and its new test assert an invariant the code does not have. bin/fm-wake-drain.sh:669 states "Every task the annotation pass could not compute is held, so its presentation cursor does not advance" and bin/fm-wake-drain.sh:661 states "the cost is a replayed annotation, never a dropped span". Neither holds for the two failure paths that name no task:fm_wake_print_annotations's manifest awk (bin/fm-wake-lib.sh:2333,|| return 1) and its per-lineprintf '%s\n' "$line" || return 1(bin/fm-wake-lib.sh:2408). Concrete sequence: task T's span since its cursor holdsworking: rebasing onto upstreamfollowed bynote: answered the captain, with a direct signal row queued. The manifest awk fails - exactly the faulttest_unattributed_annotation_failure_holds_every_cursorinjects.FM_WAKE_ANNOTATION_HELDis empty, soannotation_held=''andfully_presented='';print_status_sectionsstill runs (bin/fm-wake-drain.sh:674) andstatus_acknowledge_presented_snapshottakes thehold=falsebranch (bin/fm-classify-lib.sh:1417), findsnote:is an unread surface, sets safe=true, and commits T's cursor at the snapshot endpoint - past theworking:line the annotation never printed. That line has no other surface and is dropped permanently. The new test only exercises aworking:-only span, which the pre-existing fleet unread-surface rule holds on its own, so it cannot detect the mixed-span gap its pass message ("an annotation failure that names no task holds every presentation cursor") claims to cover; it is a valid regression test for the narrowedfully_presentedgate, but not for the stated invariant. Needs the author's call because the two remedies differ in kind: correct the comments and the test's claim to what the fleet rule actually guarantees, or hold every manifest task when the failure names none - the latter extends the hold machinery rather than correcting it, and the prior round's chosen remedy (dropping the|| [ -n "$annotation_held" ]disjunct) deliberately accepted the fleet-rule fallback.bin/fm-wake-drain.sh:580- The named-hold notice tells the reader each held task's status log "is readable at its path", but one of the two hold triggers is precisely a failed read of that log. A task entersFM_WAKE_ANNOTATION_HELDeither from the cursor-read failure at bin/fm-wake-lib.sh:2369 (the status log itself is fine - only the shared.status-presentation-cursormanifest failed) or from the rc-2 span-read failure at bin/fm-wake-lib.sh:2392. rc 2 originates infm_wake_unread_events's perlsysopen(... O_RDONLY | O_NOFOLLOW) or exit 1(bin/fm-wake-lib.sh:2270), reached only after the pre-guard at bin/fm-wake-lib.sh:2364 already confirmed-f,-rand not-a-symlink, so rc 2 there means the file became unopenable or an I/O error hit between those two points. bin/fm-wake-drain.sh:580 then prints... each status log is readable at its path: /state/<task>.status, pointing the captain at a file the drain just failed to read - in the one code path whose stated purpose is to never mislabel. Smallest honest remedy is wording: give the path without asserting readability ("their status logs are at: ..."), which reads correctly for the cursor-failure case too.🔧 Fix: collapse presentation failures into one notice, hold every cursor
2 issues (1 warning, 1 info) still open:
bin/fm-wake-drain.sh:569- The one generic notice can claim no annotations were computed while some are already on stdout above it.fm_wake_print_annotationsprints each task's annotation lines as it walks the manifest (bin/fm-wake-lib.sh:2408) and only accumulatesincomplete=1for the task that failed (bin/fm-wake-lib.sh:2393), returning 1 at the end. Concrete sequence: tasksalphaandbravoboth have primed presentation cursors and a queued directsignal:row.status_presentation_snapshotsucceeds. alpha annotates cleanly, sowake annotation: unread wake-EVENT since last drain, not current state: alpha.status: working: ...is already on stdout. bravo.status is rotated/recreated (shorter) between the snapshot and bravo's span read, sofm_wake_unread_events's perl guardexit 1 unless ... $end <= $sizefails (bin/fm-wake-lib.sh:2284) -> rc 2 ->incomplete=1-> return 1.print_status_presentationtakes the reason branch at bin/fm-wake-drain.sh:640 and prints, directly below alpha's annotation:STATUS PRESENTATION INCOMPLETE: a supplemental status annotation could not be computed; no status annotations or fleet-wide status sections were computed this drain, nothing was marked as seen and no presentation cursor advanced, so every unread status line is still unread.The second and third clauses are true (print_status_sections is skipped, so no cursor moves), but "no status annotations ... were computed this drain" is contradicted by the visible line above it - the same mislabel class the intent forbids ("never mislabels a complete presentation as incomplete"), reintroduced at the annotation level rather than the section level. The failure mode the comment at bin/fm-wake-lib.sh:2387 names ("a file that disappears, rotates, or becomes unreadable after the snapshot") is exactly this path, so it is a real sequence, not a hypothetical. This needs the author's call because the single generic wording was the captain's own explicit instruction for this round; the narrowest honest remedy is wording only - keep one notice with no per-task naming, but state that SOME annotations could not be computed and that any annotation printed above may be partial, rather than that none were computed.bin/fm-wake-drain.sh:336- One computation failure inside the same presentation pass still resolves to silence with no notice:status_snapshot_latest_event "$STATE/$task.status" "$endpoint" "$ident" || continue. That helper returns 1 both for a genuine read failure (its_fm_status_read_spanat bin/fm-classify-lib.sh:1078, or its inner perl at :1091) and for the documented benign deferrals (endpoint 0, size/ident changed mid-read, latest line crossing the 64 KiB bound), so the|| continuecannot tell "could not read this task's latest event" from "not applicable this drain". The task is then absent from STATUS OUTCOME BACKSTOP with no STATUS OUTCOME BACKSTOP SKIPPED line, which under this change's contract reads as "computed, nothing uncovered". Nothing is lost: the task's backstop receipt is not advanced, so the next drain retries, and the benign deferral is by far the common case (any log that grew since the snapshot takes it). Recording this as an accepted tradeoff rather than a defect: separating a real I/O failure from the documented deferral would mean threading a distinct return code through status_snapshot_latest_event and its callers, which extends the change rather than correcting it, and the intent's four enumerated cases do not name this path.bin/fm-classify-lib.sh:866- Host-level flake, not a product defect: on this machineLC_ALL=C /usr/bin/statprints the correct value and then exits 139 (SIGSEGV) in ~5% of forks under the current load (Darwin 27.0.0, load avg 14-25, 43 concurrent agent processes). Since_fm_status_file_sizeand_fm_open_decisions_file_ident(bin/fm-classify-lib.sh) run that call for every status file,status_presentation_snapshotfails ~7% of drains (measured: 8 failures / 120 calls over 4 static status files) and the drain correctly printsSTATUS PRESENTATION INCOMPLETE: status snapshot could not be read. The result is that any whole-file run of tests/fm-wake-drain-open-decisions.test.sh or tests/fm-wake-queue.test.sh fails at a random test. This predates the change - 4 runs of the suite at base d32fe93 produced 3 random failures and 1 clean pass - and it also aborts tests/lib.sh at source time (FM_TEST_OWNER_IDENTITYcomes fromLC_ALL=C psin fm_pid_identity), which surfaces asfm_test_tmproot: command not found. I worked around it by running every relevant test individually with retries; all pass. No code change is warranted here, but local full-suite runs will stay unreliable until the machine's load drops or the OS-level stat crash is resolved.Before/after end-to-end drain transcripts for intent case 1 (fakemvfailing the.status-presentation-cursorreceipt write): base prints sections +STATUS PRESENTATION INCOMPLETE, target prints sections +STATUS PRESENTATION RECEIPT FAILEDand no INCOMPLETEBefore/after transcripts for intent case 2 (malformed.status-presentation-lock,FM_STATUS_PRESENTATION_LOCK_TIMEOUT=1): base leaves stdout silent below the wake row with only a stderr line, target printsSTATUS PRESENTATION INCOMPLETE: status presentation lock could not be acquired safely…on stdoutBefore/after transcripts for intent case 3 (fold cursor rewound to a stale non-empty open set,FM_STATUS_SPAN_READERforced to exit 1): base prints the already-resolved[key=old-choice]under OPEN DECISIONS and hides[key=new-choice], target prints no decisions and states what it could not computeBefore/after transcripts for intent case 4 (fakecatfailing the shared.status-presentation-cursorread once): base drops the annotation silently and theworking:line is gone from the next drain, target prints exactly one notice and the next drain replayswake annotation: … working: live row must not disappear silentlybash tests/fm-wake-drain-open-decisions.test.sh- all 16 tests exercised individually (retried per test for the host stat flake), including the newtest_trusted_nonempty_fold_cursor_read_failure_is_not_authoritativeandtest_receipt_failure_does_not_relabel_printed_sections_incompletebash tests/fm-wake-queue.test.sh- new teststest_annotation_cursor_failure_is_reported_on_stdout,test_annotation_span_read_failure_is_reported_and_retried,test_unattributed_annotation_failure_holds_every_cursor,test_annotation_failure_holds_sibling_cursors_too,test_snapshot_failure_reports_the_uncomputed_annotations, the rewordedtest_malformed_presentation_lock_reports_acquire_failure, plus adjacenttest_historical_annotation_skips_announced_status,test_structural_signal_enrichment_preserves_raw_rows,test_enrichment_preserves_all_unread_lines_and_status_file_failures,test_slow_annotation_does_not_block_append_and_deleted_file_fails_open,test_self_held_lock_reclaims_instead_of_deadlocking,test_subshell_lock_ownership_without_bashpid,test_bounded_lock_handoff_after_contention,test_live_presentation_holder_is_deadlined_without_weakening_ackbash tests/fm-wake-drain-unread-status.test.sh(13 tests),bash tests/fm-wake-drain-outcome-backstop.test.sh(18 tests),bash tests/fm-wake-drain-open-decisions-cursor.test.sh(7 tests) - whole-file clean passes over the same presentation machinerybash tests/fm-classify-decision-key.test.sh(25 tests) andbash tests/fm-classify-corr-token.test.sh(12 tests) - whole-file clean passes over the changedstatus_open_decisions_incrementalfoldRegression check: the same 8 new/updated tests run against a base-commit checkout of d32fe93 - each fails there with its own intended assertion (e.g.not ok - the annotation cursor failure owed exactly one notice, got 0) and passes at 8618dcdHost-flake isolation:for i in $(seq 1 300); do bash -c 's=$(LC_ALL=C /usr/bin/stat -f "%z" f 2>/dev/null); echo $?'; done | sort | uniq -c-> 16/300 exits 139 (SIGSEGV);status_presentation_snapshotover 4 static status files -> 8 failures / 120 calls; 4 base-commit runs of tests/fm-wake-drain-open-decisions.test.sh -> 3 random failures, 1 cleanAGENTS.md:189- AGENTS.md states twice (line 189 and line 425) that UNREAD STATUS lines "are not re-printed after that presentation", stated absolutely. This change makes the exception a named user-facing outcome: on a receipt-commit failure the drain now prints STATUS PRESENTATION RECEIPT FAILED, which says the fully printed sections "may repeat on the next drain". I deliberately did not hedge either AGENTS.md line: the possibility already existed at the base commit (sections printed, receipt commit failed), so this change did not make the claim newly wrong, the failure mode is duplication rather than loss, the drain announces it on stdout where it happens, and the instruction's load-bearing purpose - read those lines this turn, they are the only surface for them - is unchanged. Recording it as a judgment call in case the captain would rather AGENTS.md carry the caveat; doing so would cost two lines in the always-loaded surface that every session pays for.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.