Skip to content

fix: absorb turn-end wakes on bounded pane churn - #2877

Open
karotkriss wants to merge 18 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-2374-turnend-absorb
Open

fix: absorb turn-end wakes on bounded pane churn#2877
karotkriss wants to merge 18 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-2374-turnend-absorb

Conversation

@karotkriss

@karotkriss karotkriss commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Intent

Fix issue #2374 in kunchenguid/firstmate: turn-end wakes are never absorbed for a harness whose semantic busy state has no verified source (codex is the concrete case), so a primary running N codex crewmates is woken once per worker turn, indefinitely, with nothing to act on. Each wake costs a full supervisor turn and real tokens, scaling with worker count. The required and binding fix shape is the issue's own middle option: widen positive-work evidence to include pane-content churn since the previous poll, which is harness-independent and already computed on the staleness path, while preserving absorb-only-on-positive-evidence semantics. Explicitly NOT the issue's third option of bounding the surfacing rate per task, because that would suppress genuinely stopped-worker wakes, and explicitly NOT giving codex a fabricated semantic busy source, because harness adapters must earn trust through verification. The predicate lives in bin/fm-watch.sh rather than bin/fm-classify-lib.sh because it consumes the state/.hash-* marker format that bin/fm-watch.sh declares it alone owns.

This round is rework accepted from maintainer review of the existing PR 2877. Two review requirements are now part of the accepted intent and supersede the original "no new configuration knob, no new state file" constraint for exactly these two points and nothing else:

  1. OPT-IN, not a default-behavior change. The project's vision rule is that new capability arrives as opt-in, and the reviewers held this PR as class=default-behavior because it widened the default absorb with no flag. The widening is now gated on the home creating config/turnend-churn-absorb. With the flag absent - which is every existing home - signal_turnend_panes_churned returns on its first line and triage is byte-for-byte the pre-change behavior. The rationale for keeping it opt-in rather than defaulting it on is that the other two proofs read a verdict the harness itself vouches for while this one infers execution from rendered bytes, which is a weaker claim and therefore a home's choice to make. The flag is local and gitignored, and deliberately NOT inherited by secondmate homes, because it is a home-local supervision-noise preference and a mate runs its own crew mix.

  2. Pane churn must not be able to perpetually mute the stale-recovery path. Reviewers observed that the churn absorb and the staleness backbone read the same pane, so one continuously-rendering pane could silence both. That concern is correct, though one detail of the stated mechanism is not: state/.stale- stores the hash the backbone has ALREADY CLASSIFIED, so it is a dedupe record and not the timer; clearing it makes a later stale render more likely to surface, not less. The real mute is upstream of both paths - a pane that renders continuously (a terminal clock, an animated prompt, a status bar, or a harness that leaves a background renderer alive after its agent yields) never produces two consecutive identical hashes, so .count- never reaches 2 and the staleness backbone never classifies it at all, which is already true on main. What this change added was a second path that also stayed quiet on the same evidence, leaving a worker that had genuinely stopped behind such a renderer with no path left to surface. The fix is therefore a bound on the deferral rather than a change to the dedupe record: one endpoint's bare turn-ends may ride churn evidence for at most FM_TURNEND_CHURN_ABSORB_SECS (default 900), tracked per window in state/.churn-since-*, after which the wake surfaces and the window restarts, so a perpetually churning pane produces at most one turn-end wake per window instead of either one per turn or none ever. The bound is evaluated BEFORE any .stale- state is touched, so a wake that surfaces there leaves the staleness backbone's own classification untouched. The .stale- clear is deliberately kept for its original reason: a later stopped render whose bytes happen to match an earlier classified stale hash must surface through ordinary staleness rather than inherit the earlier interval's wedge timer.

Both requirements are covered by behavioral tests colocated in tests/fm-watch-triage.test.sh: the same churning fixture that absorbs with the flag surfaces and queues without it and opens no deferral window, and a spent deferral window surfaces, queues, and restarts. The four existing safety guards (a .status file anywhere in the batch, a secondmate, a malformed prior hash, an ambiguous marker key) now run with the flag enabled so they keep proving their specific guard rather than passing vacuously on the disabled path.

Every negative outcome of the predicate still returns 1 so absence of evidence surfaces exactly as before: the flag being absent, a status file in the batch, a secondmate, an unresolvable task, no recorded endpoint, an ambiguous marker key, a missing or malformed prior hash, a capture that fails or returns empty, an exhausted deferral bound, and an unchanged pane.

Binding constraints, unchanged from the original task. This is firstmate's own shared tracked material, so .agents/skills/firstmate-coding-guidelines applies: the knowledge-placement decision tree, one owner per contract with cross-references rather than restatements, one full sentence per line in tracked Markdown, plain dash and never an em dash, never an agent name as commit co-author, bin/*.sh shellcheck-clean through bin/fm-lint.sh which is the single owner of the lint definition, and tests colocated in tests/ extending the existing runner and exercising behavior through the executable interface rather than asserting implementation source bytes. docs/architecture.md owns the watcher triage contract and docs/configuration.md owns the operator-facing flag and environment variable, with AGENTS.md carrying only the one-line layout entries. Prefer quality, simplicity, robustness, scalability and long-term maintainability over development speed, and the smallest diff in the right place rather than a patch that papers over a symptom.

Delivery: update the EXISTING pull request 2877 against kunchenguid/firstmate in place, pushed to the karotkriss fork remote, never to the clone's default branch, and never opening a duplicate PR. The body must cite "Fixes #2374" and nothing else. The pipeline attestation must bind to the final head, so no commits are pushed after the stamp.

Known out of scope, deliberately excluded and reported separately rather than folded in: tests/fm-watch-triage.test.sh contains pre-existing load-sensitive flakes (test_nonterminal_stale_repairs_missing_or_corrupt_timer and test_paused_authoritative_working_preserves_wedge_timer both use a 3-second wait_numeric_file budget measured from watcher process spawn), proven by a 6-round interleaved A/B against pristine origin/main to flake independently of this change. Those belong to their own task, and this PR must close #2374 only.

Decisions accepted during the review of this rework, now part of the binding intent. A reviewer reading only the diff would not know these were chosen deliberately rather than missed:

  • Positive evidence is composed PER REFERENCED TASK from one predicate-local metadata snapshot, so a batch mixing an authoritatively-working task on a static pane with an unknown-verdict task on a churning pane is absorbed, because every task in it has eligible evidence. The ambiguity guard counts metadata RECORDS by derived key in that snapshot, not deduplicated window values, so two tasks recording the same endpoint target refuse instead of letting one absorb on marker evidence that is not uniquely attributable to it.
  • The batch-level status-file and secondmate rejections are evaluated BEFORE any per-task evidence decision, and this ordering is absolute. Neither an authoritative provably-working verdict nor churn evidence may skip them. A wake naming any .status file anywhere in the batch keeps the strict authoritative proof, and a kind=secondmate task's status signal is never absorbed.
  • The config/turnend-churn-absorb presence check is the first statement of the function body, ahead of the argument count check, filename parsing, deduplication, and snapshot construction, so an existing home returns having done no parsing, no metadata reads, and no captures.
  • A .hash-* marker is validated against its exact bytes, so a digest carrying trailing whitespace is malformed and surfaces.
  • The deferral bound is compared numerically, never lexicographically.
  • DELIBERATELY DECLINED, with captain approval, and NOT a defect: the snapshot evaluation performs a linear scan per referenced task rather than an indexed lookup. This repository targets stock macOS Bash 3.2.57, enforced by the macos-stock-bash CI job, and therefore has no associative arrays, using none anywhere in bin/ or tests/. N here is one coalesced turn-end batch, realistically one to three tasks, and each referenced task already spends a pane-capture subprocess that dominates the comparisons by orders of magnitude. An index-based lookup is the recorded upgrade path if a batch ever grows large enough to matter. Do not re-raise this as a finding and do not restructure for it.

What Changed

  • Add opt-in pane-churn evidence for absorbing bare turn-end wakes when each referenced task is either provably working or has a uniquely attributable pane that changed since the previous poll.
  • Bound churn-only deferral per endpoint with FM_TURNEND_CHURN_ABSORB_SECS, while preserving fail-closed surfacing for expired windows, unchanged panes, secondmates, status signals, and invalid or ambiguous state.
  • Document the home-local setting and expand executable watcher triage coverage for absorption, deferral, and safety guards.

Risk Assessment

✅ Low: Captain, the change is opt-in, fail-closed, bounded per endpoint, and backed by behavioral coverage for its material safety boundaries.

Testing

The focused watcher triage runner passed, and direct process output plus persisted queue and marker state demonstrated opt-in absorption, default-off surfacing without pane capture, and bounded resurfacing after deadline expiry.

Evidence: End-to-end watcher transcript

Source: End-to-end watcher transcript

Enabled: absorbed, no queue entry, churn window opened. Disabled: surfaced and queued without pane capture. Deadline spent: surfaced and queued, churn window cleared.

SCENARIO=enabled
watcher_alive=true
watcher_stdout=
queue_record=none
pane_capture_count=1
churn_window=open
triage_log=[2026-08-23T21:57:05-0400] absorbed benign signal: /tmp/turnend-churn-evidence.ceU39N/opt_in/state/codexworker.turn-ended

SCENARIO=disabled
watcher_alive=false
watcher_stdout=signal: /tmp/turnend-churn-evidence.ceU39N/default_off/state/codexworker.turn-ended
queue_record=signal:signal: /tmp/turnend-churn-evidence.ceU39N/default_off/state/codexworker.turn-ended
pane_capture_count=0
churn_window=absent
triage_log=none

SCENARIO=expired
watcher_alive=false
watcher_stdout=signal: /tmp/turnend-churn-evidence.ceU39N/deadline_spent/state/codexworker.turn-ended
queue_record=signal:signal: /tmp/turnend-churn-evidence.ceU39N/deadline_spent/state/codexworker.turn-ended
pane_capture_count=1
churn_window=absent
triage_log=none

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • git status --short and targeted branch diff inspection
  • bash tests/fm-watch-triage.test.sh
  • Hermetic bin/fm-watch.sh execution using tests/wake-helpers.sh for enabled, disabled, and expired churn-window scenarios
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (6): Last reviewed commit: "no-mistakes(document): Clarify pane-chur..." | Re-trigger Greptile

Comment thread bin/fm-watch.sh Outdated
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Scheduled 11:10am PT 8/23 pass. VISION.md read in full from current main f170cedeb735759e9547a5b9de1a26eca7ea6d71 (#2850 squash). Issue #2374 is ready-for-pr; that is a queue label, not a merge vote. No captain comment authorizing a merge. First inspection of this PR.

VISION (inspected signal_turnend_panes_churned in bin/fm-watch.sh, the absorb composition with signal_crew_provably_working, docs/architecture.md third-evidence sentence, fail-closed tests in tests/fm-watch-triage.test.sh). Per-rule: peace of mind aligns (Codex turn-end noise currently costs a full supervisor turn per worker turn); scripts own the mechanics aligns (byte-hash compare, no vendor pixels, no fabricated busy verdict); honest interface mixed — absorb defers to the staleness backbone rather than swallowing, still-pane / malformed-hash / secondmate / colliding-key all still surface, but a pane that keeps rendering (clock, spinner, heartbeat) would churn every poll and keep the turn-end silent by default; new capability as opt-in does not align — this widens default absorb with no flag. Author's own writeup calls it "the minimum default-behavior change that fixes the reported symptom."

Class: default-behavior. Default-behavior is never auto-eligible. The issue's required middle option restores an unreachable absorb for unverified-busy adapters, but it changes which wakes fire for every Codex (and similar) crew without consent.

Security: none. No workflow-file / secret / injection risk. Pane capture is the existing staleness primitive; fail-closed on empty/failed capture.

Overlap / HOLD: not a standing spawn/teardown/herdr hold. Same-file overlap with open #2701 / #2884 / #2809 / #2796 on bin/fm-watch.sh, #2884 on bin/fm-classify-lib.sh, #2701 / #2884 / #2845 on tests/fm-watch-triage.test.sh, and #2622 (spawn-freshen hold) on docs/architecture.md. Not treated as a covering-PR hold for #2374; the class alone blocks auto-eligibility.

CI / NM: HEAD 47617a1147dded91eb1c2ee7cfca2207ee6dd7a0. MERGEABLE / CLEAN, ahead 8 / behind 0. Body no-mistakes-pipeline-attestation:v1 names f6f15e06c69bb72f9dd38afe3224e73205f676dc, not THIS HEAD — the later no-mistakes: apply CI fixes commit moved HEAD without a matching attestation. NM red despite GitHub Require no-mistakes SUCCESS (run 32652354993). CI run 32652354998 all SUCCESS. Greptile SUCCESS — not a gate. Do not @greptileai.

Workflows: already approved (CI completed SUCCESS on this HEAD). Run IDs: 32652354998 (CI), 32652354993 (Require no-mistakes). No pending first-time-fork approval.

Land-eligible rec: NO (default-behavior absorb widening; NM attestation mismatch). Captain-flag NOW: no.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

The firstmate's triage is accurate. Two clear blockers:

  1. Default-behavior change without captain consent — the signal_turnend_panes_churned widening fires for all Codex (and similar unverified-busy) workers by default. The firstmate's "honest interface mixed" flag is well-placed: a pane with a live clock, spinner, or heartbeat renders churn on every poll, so its turn-ends are silently deferred indefinitely unless the captain explicitly permits that tradeoff. That's a new default, not a restored one.

  2. NM attestation mismatch — the pipeline block in the PR body attests f6f15e06c69bb72f9dd38afe3224e73205f676dc but HEAD is 47617a1147dded91eb1c2ee7cfca2207ee6dd7a0 (the "apply CI fixes" commit). The attestation does not cover the landed state.

Both blockers are independent and either alone is sufficient to hold eligibility. The fix itself is coherent and the fail-closed properties are real, but it cannot advance without:

  • A captain consent comment authorizing the default-behavior widening (specifically acknowledging the continuous-renderer case), and
  • A fresh NM pipeline run whose attestation SHA matches this HEAD.

@karotkriss
karotkriss force-pushed the fm/fm-2374-turnend-absorb branch from 47617a1 to 5d8c3e4 Compare August 23, 2026 19:44
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Scheduled 3:10pm PT 8/23 pass. VISION.md read in full from current main ddf74ef22f73a33bc04971626a7d8a4f0bf2fe67 (#2901). Reconfirmed: VISION.md last changed 76355e20b4f4 (2026-08-11, #2147); unchanged since last-known 197afbb79f8b (#2870). Issue #2374 remains ready-for-pr; that is a queue label, not a merge vote. No captain comment authorizing a merge. Full re-inspect of THIS HEAD after 19:46Z activity (force-push/rebase at 19:44Z; Greptile re-review).

VISION (inspected signal_turnend_panes_churned in bin/fm-watch.sh, the absorb composition with signal_crew_provably_working, docs/architecture.md third-evidence sentence, fail-closed tests in tests/fm-watch-triage.test.sh). Per-rule: peace of mind aligns (Codex turn-end noise currently costs a full supervisor turn per worker turn); scripts own the mechanics aligns (byte-hash compare, no vendor pixels, no fabricated busy verdict); honest interface mixed — absorb defers to the staleness backbone rather than swallowing, still-pane / malformed-hash / secondmate / colliding-key all still surface, but a pane that keeps rendering (clock, spinner, heartbeat) would churn every poll and keep the turn-end silent by default; new capability as opt-in does not align — this widens default absorb with no flag. Author's own writeup still calls it "the minimum default-behavior change that fixes the reported symptom."

Class: default-behavior. Default-behavior is never auto-eligible. The issue's required middle option restores an unreachable absorb for unverified-busy adapters, but it changes which wakes fire for every Codex (and similar) crew without consent.

Security: none. No workflow-file / secret / injection risk. Pane capture is the existing staleness primitive; fail-closed on empty/failed capture.

Overlap / HOLD: not a standing spawn/teardown/herdr hold. Same-file overlap with open #2701 / #2809 / #2796 / #2320 / #2882 / #2867 on bin/fm-watch.sh, #2836 / #2867 / #2801 on bin/fm-classify-lib.sh, #2867 on tests/fm-watch-triage.test.sh, and #2622 (spawn-freshen hold) on docs/architecture.md. #2884 (listed last pass) is now CLOSED. Not treated as a covering-PR hold for #2374; the class alone blocks auto-eligibility.

CI / NM: HEAD 5d8c3e4917ddf8a925d65a5710168dd47acec196. MERGEABLE / CLEAN, ahead 8 / behind 2 vs current main. Body no-mistakes-pipeline-attestation:v1 still names f6f15e06c69bb72f9dd38afe3224e73205f676dc, not THIS HEAD — the later no-mistakes: apply CI fixes commit (and the 19:44Z rebase) moved HEAD without a matching attestation. NM red despite GitHub Require no-mistakes SUCCESS. CI run 32662209266 all SUCCESS. Greptile SUCCESS — not a gate. Do not @greptileai.

Workflows: already approved (CI completed SUCCESS on this HEAD). Run IDs: 32662209266 (CI), 32662209251 (Require no-mistakes). No pending first-time-fork approval.

Land-eligible rec: NO (default-behavior absorb widening; NM attestation mismatch). Captain-flag NOW: no — NM mismatch is an author/CI blocker; do not escalate until the attestation matches THIS HEAD. Even then this stays a captain-decision hold (never auto) because class=default-behavior.

Waiting on the author for a HEAD-matching no-mistakes attestation. Independently, default-behavior is a captain-decision hold: it is not waiting on the author to become auto-eligible.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Triage receipt confirmed and recorded at this HEAD.

On the NM attestation mismatch: The body still attests f6f15e06c69bb72f9dd38afe3224e73205f676dc; HEAD is 5d8c3e4917ddf8a925d65a5710168dd47acec196. That is an author-side blocker independent of the captain hold, and the CI run noted (32662209266) succeeded against a SHA that did not produce the current diff. The captain should not see this as attestation coverage for the post-rebase commits.

On honest interface: mixed: The concern is structurally correct and worth making explicit for the captain. signal_turnend_panes_churned also clears state/.stale-$key, so for a pane that churns continuously — terminal clock, animated prompt, tmux status bar, any heartbeat emitted by the harness or shell — the staleness backbone's own surface path is also reset each cycle. Neither the absorb fallback nor the staleness path escalates. The PR description says "a crew that has stopped renders nothing further," which is true for a fully exited agent, but false for any harness that leaves a background renderer alive after the agent's turn ends. Codex specifically: if its pane retains a clock or spinner after the agent yields, the turn-end is absorbed indefinitely and no stale wake fires, because the pane never goes static. This is a narrower version of the captain-relevant default-behavior question: it is not just "no opt-in knob" but "the absorb path and its fallback recovery path share the same continuously-churning pane as their evidence, so one background renderer closes both."

Captain-decision hold stands: default-behavior widening with no flag, and the architecture-level question above, are both captain decisions. The NM attestation mismatch is an independent author blocker. Neither unblocks automatically.

@karotkriss
karotkriss force-pushed the fm/fm-2374-turnend-absorb branch from 5d8c3e4 to de5edc0 Compare August 24, 2026 00:53
@karotkriss karotkriss changed the title fix(bin): absorb turn-end wakes on pane churn fix: optionally absorb turn-end wakes on bounded pane churn Aug 24, 2026
Comment thread bin/fm-watch.sh
The watcher's "absorb a benign turn-end when the crew is provably working"
triage was structurally unreachable for any harness whose semantic busy state
has no verified source. crew_absorb_class only reports working for an actively
running no-mistakes step or an exact busy verdict, and bin/fm-crew-state.sh can
only answer unknown for such an adapter, so codex crewmates surfaced a signal
wake at every turn boundary with nothing to act on - a full supervisor drain,
inspect and acknowledge turn per worker turn, scaling with the number of workers
in flight and drowning the wakes that matter in identical noise.

Widen the proof rather than bound the wake rate. A wake carrying only bare
turn-ended markers is now also benign when the task's pane content changed since
the previous poll, compared against the same state/.hash-* marker the staleness
backbone already records and already trusts as liveness. That evidence claims no
harness semantics, so it fabricates no busy verdict an adapter has not earned,
and it needs no adapter cooperation.

Absorb stays evidence-driven in both directions. A wake naming any status file
keeps the strict proof, every captain-relevant verb still surfaces immediately,
and an unresolvable task, a missing prior hash, a failed or empty capture, or an
unchanged pane all surface exactly as before. The absorb defers rather than
swallows: a crew that has stopped renders nothing further, so its now-static pane
surfaces through the staleness backbone within a poll or two. Bounding the
surfacing rate instead would have suppressed genuinely stopped workers.

The derivation lives with the .hash-* marker format in bin/fm-watch.sh, which
owns it, and costs one bounded capture reached only for a no-verb turn-end whose
crew is not already provably working.
Make the pane-churn form of positive work evidence opt-in per home and
bound how long it may defer one endpoint's bare turn-ends.

Absorbing a bare turn-end on pane churn is now reached only when the home
creates config/turnend-churn-absorb. The other two proofs read a verdict
the harness itself vouches for, while this one infers execution from
rendered bytes, so widening the absorb is a home's choice rather than a
default every fleet inherits. With the flag absent the predicate returns
on its first line and triage is unchanged.

Churn and pane staleness read the same pane, so neither can be the
other's only backstop. A pane that renders continuously never presents
the two consecutive identical hashes the staleness backbone needs, so an
unbounded churn absorb left a worker that had genuinely stopped behind
such a renderer with no path to surface at all. One endpoint's turn-ends
may now ride churn evidence for at most FM_TURNEND_CHURN_ABSORB_SECS,
tracked in state/.churn-since-*, after which the wake surfaces and the
window restarts. The bound is evaluated before any .stale- state is
touched, so a wake that surfaces there leaves the staleness backbone's
own classification alone.

Covers both with behavioral tests: the same churning fixture that absorbs
with the flag surfaces and queues without it, and a spent deferral window
surfaces and restarts. The four existing safety guards now run with the
flag enabled so they keep proving their specific guard.
@karotkriss
karotkriss force-pushed the fm/fm-2374-turnend-absorb branch from c83e976 to 8c2feee Compare August 24, 2026 01:59
@karotkriss karotkriss changed the title fix: optionally absorb turn-end wakes on bounded pane churn fix: absorb turn-end wakes on bounded pane churn Aug 24, 2026
@karotkriss

Copy link
Copy Markdown
Contributor Author

Both blockers are addressed in this push, and the pipeline attestation now binds to this HEAD.

1. Shared-evidence flaw: the churn absorb is now bounded

The concern is real and it is fixed, but one detail in the diagnosis is worth correcting because it changes what the fix has to be.

state/.stale-<key> is not the staleness timer. It stores the hash the backbone has already classified, so it is a dedupe record: clearing it makes a later stale render more likely to surface, not less. The timer is .count-<key> (consecutive identical hashes) with .stale-since-<key>.

The actual mute is upstream of both. A pane that renders continuously - a terminal clock, an animated prompt, a status bar, or a harness that leaves a background renderer alive after its agent yields - never produces two consecutive identical hashes, so .count-<key> never reaches 2 and the staleness backbone never classifies it at all. That is true on main today, with or without this change. What this change added was a second path that also stayed quiet on the same evidence, so a worker that had genuinely stopped behind such a renderer had no path left to surface at all. Churn and staleness read the same pane, so neither can be the other's only backstop.

The fix is therefore a bound on the deferral rather than a change to the dedupe record:

  • One endpoint's bare turn-ends may ride churn evidence for at most FM_TURNEND_CHURN_ABSORB_SECS (default 900), tracked per window in state/.churn-since-*.
  • When that window is spent the turn-end surfaces and the window restarts, so a perpetually churning pane produces at most one turn-end wake per window instead of either "every turn" or "never".
  • The bound is evaluated before any .stale-* state is touched, so a wake that surfaces here leaves the staleness backbone's own classification untouched.

Covered by test_turn_ended_churn_absorb_bounded, which drives a pane that churns every poll with a spent deferral window and asserts the turn-end surfaces, is queued, and restarts the window. The .stale-<key> clear is kept, with its original reason: a later stopped render whose bytes happen to match an earlier classified stale hash should surface through ordinary staleness rather than inherit the earlier interval's wedge timer, which test_turn_ended_churn_resets_prior_stale_classification pins.

2. Default-behavior change: the widening is now opt-in

The absorb widening no longer changes any home's behavior by default. It is gated on the presence of config/turnend-churn-absorb:

  • Flag absent, which is every existing home: signal_turnend_panes_churned returns 1 on its first line and triage is byte-for-byte the pre-change behavior.
  • Flag present: the bounded churn evidence above applies.

The rationale for keeping it opt-in rather than defaulting it on is stated in the code: the other two proofs read a verdict the harness itself vouches for, while this one infers execution from rendered bytes, which is a weaker claim and therefore a home's choice to make. The flag is local and gitignored, and deliberately not inherited by secondmate homes, since it is a home-local supervision-noise preference and a mate runs its own crew mix. Documented under docs/configuration.md "Turn-end pane-churn absorb", with the triage contract still owned by docs/architecture.md.

test_turn_ended_churn_absorb_off_by_default pins the default: the same churning fixture that absorbs with the flag surfaces and queues without it, and opens no deferral window. The four existing safety guards (status file in the batch, secondmate, malformed prior hash, ambiguous marker key) now run with the flag enabled so they keep proving their specific guard rather than passing vacuously on the disabled path.

3. Attestation

This HEAD is the product of one clean no-mistakes round taken after the rework, with no commits pushed after the stamp, so the no-mistakes-pipeline-attestation:v1 head_sha in the body matches the HEAD under review.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Scheduled 7:10pm PT 8/23 pass. VISION.md read in full from current main 7b88520c055408a18f1476ecce08be60b2885fc9 (#2858). Issue #2374 remains ready-for-pr; that is a queue label, not a merge vote. No captain comment authorizing a merge. Full re-inspect of THIS HEAD after the post-3:10pm rework (opt-in gate + matching attestation). Last pass was HEAD 5d8c3e49 with attestation f6f15e06 mismatch and class=default-behavior.

VISION (inspected signal_turnend_panes_churned in bin/fm-watch.sh including the first-line config/turnend-churn-absorb presence check, the absorb composition ! signal_crew_provably_working && ! signal_turnend_panes_churned, .churn-since- bound, fail-closed tests including test_turn_ended_churn_absorb_off_by_default, and docs/architecture.md / docs/configuration.md flag contract). Per-rule:

  • One captain, one interface: aligns (opt-in only; flag absent is unchanged triage; absorbed wakes stay below-deck machinery).
  • Authority is explicit and never inferred: aligns (new absorb ships as a local presence flag, not inherited, default-off).
  • Scripts own the mechanics: aligns (byte-hash pane compare, no vendor pixels, no fabricated busy verdict).
  • A restart is a non-event: aligns (deferral lives in durable .churn-since-*; exhausted bound surfaces).
  • Delegation with a spine: aligns (fail-closed on status files, secondmates, ambiguous keys, malformed hashes).
  • The fleet outlives any vendor: aligns (harness-independent pane bytes; does not invent a Codex busy source).
  • Scope: aligns (watcher/docs/tests; validation stays in no-mistakes/CI).

Class this HEAD: opt-in. Last pass's default-behavior hold on the absorb itself is lifted: [ -e "$CONFIG/turnend-churn-absorb" ] || return 1 is the first statement of the predicate, and the default-off test proves the pre-change path. That is not a captain product decision anymore.

Security: no workflow/secret/exfil surface. Watcher-internal markers only.

CI: all required checks green on THIS HEAD (Behavior portable/serial, Herdr, Lint, Repo invariants, macOS Bash, coverage guard, Greptile). PR must be raised via no-mistakes pass. Body no-mistakes-pipeline-attestation:v1 head_sha=8c2feee2a823bfae5fadea312c92c561b986257e matches HEAD 8c2feee2. MERGEABLE/CLEAN, ahead 18 / behind 0.

Still not land-eligible. File overlap is a coordinator hold, not waiting on the author:

Not waiting on the author. NM matches and the absorb is now opt-in. Do not rebase: overlap would still need a captain/coordinator exception. Not flagging Firstmate: the remaining hold is file overlap, not a product decision.

Land-eligible: NO. Captain-flag NOW: no.

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.

Turn-end wakes are never absorbed for harnesses without a verified busy source

2 participants