@agfpd/iapeer 0.2.62 → 0.2.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.62",
3
+ "version": "0.2.64",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,7 @@ import { CODEX_BEARER_ENV_VAR, CODEX_DUMMY_BEARER, type Runtime } from '../core/
21
21
  import { readLaunchEnv } from '../storage/index.ts'
22
22
  import { canaryChannel, dismissCanary, ensureServerCanary, exitLogPath, signalCanaryClean } from './canary.ts'
23
23
  import { hardenCmdLogDir, prepareCmdLogDir } from './cmdlog.ts'
24
+ import { logReadyGateCrossCheck, readyGatePtyFlipEnabled, readyGateViewport } from './readyGateModel.ts'
24
25
  import { appendLifecycleEvent } from '../lifecycle/eventlog.ts'
25
26
  import { claudeAdapter } from './adapters/claude.ts'
26
27
  import { codexAdapter } from './adapters/codex.ts'
@@ -385,7 +386,29 @@ export const launch: LaunchFn = async (
385
386
  tmux(sock, 'send-keys', '-t', identity, ...dialogKeys)
386
387
  continue
387
388
  }
388
- if (adapter.isInputReady(pane)) {
389
+ // Ф1 READY-GATE SOURCE FLIP (per-peer flag, DARK by default). When the peer's flip flag is set,
390
+ // run the SAME isInputReady predicate on the pty-MODEL viewport (validated 0-divergence vs
391
+ // capture-pane by the burn-in gate, incl. the codex resume-with-history splash-off-screen center)
392
+ // instead of the capture-pane scrape, with an UNCONDITIONAL capture-pane fallback. This is the
393
+ // ONLY changed input — dialog detection (above) and delivery + delivery-confirm (below + transport)
394
+ // are untouched. Flag OFF → readyView === pane (byte-identical to today).
395
+ let readyView = pane
396
+ if (readyGatePtyFlipEnabled(cfg.logDir, identity)) {
397
+ const geo = tmux(sock, 'display', '-p', '-t', identity, '#{pane_width}x#{pane_height}')
398
+ const [c, r] = geo.ok ? geo.out.trim().split('x').map(Number) : [0, 0]
399
+ readyView = await readyGateViewport({ logDir: cfg.logDir, identity, fallback: pane, cols: c ?? 0, rows: r ?? 0 })
400
+ // SERVING-path cross-check (post-flip soak gate): the model verdict is AUTHORITATIVE (readyView);
401
+ // record what capture-pane WOULD have said, to prove 0 model-vs-capture divergence live in the
402
+ // serving path. Observability only — never affects the ready decision.
403
+ logReadyGateCrossCheck(cfg.exitLogDir, {
404
+ identity,
405
+ modelReady: adapter.isInputReady(readyView),
406
+ captureReady: adapter.isInputReady(pane),
407
+ geom: `${c ?? 0}x${r ?? 0}`,
408
+ nowISO: new Date().toISOString(),
409
+ })
410
+ }
411
+ if (adapter.isInputReady(readyView)) {
389
412
  if (!hasMessage) return ready(identity) // bare bring-up — session up, no message
390
413
  // Boot delivery = load-buffer → paste-buffer -p → Enter (Ф-#8b hardening):
391
414
  // the SAME mechanism warm delivery uses (transport.deliverViaTmux), replacing
@@ -0,0 +1,72 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { logReadyGateCrossCheck, readyGatePtyFlipEnabled, readyGateViewport } from './readyGateModel.ts'
6
+
7
+ // The Ф1 ready-gate source swap is DARK by default and falls back to capture-pane UNCONDITIONALLY,
8
+ // so the flip can never make readiness worse than tmux today. These prove exactly that contract.
9
+ const tmps: string[] = []
10
+ const mkLogDir = (): string => {
11
+ const d = mkdtempSync(join(tmpdir(), 'iapeer-rgm-'))
12
+ tmps.push(d)
13
+ return d
14
+ }
15
+ afterEach(() => {
16
+ while (tmps.length) rmSync(tmps.pop()!, { recursive: true, force: true })
17
+ })
18
+ const FALLBACK = '<<capture-pane-fallback>>'
19
+
20
+ describe('readyGatePtyFlipEnabled (per-peer flip flag, canary-first)', () => {
21
+ test('absent marker → off; present marker → on', () => {
22
+ const d = mkLogDir()
23
+ expect(readyGatePtyFlipEnabled(d, 'codex-x')).toBe(false)
24
+ writeFileSync(join(d, 'codex-x.pty-readygate'), '')
25
+ expect(readyGatePtyFlipEnabled(d, 'codex-x')).toBe(true)
26
+ })
27
+ })
28
+
29
+ describe('readyGateViewport (Ф1 viewport-source swap, tmux-fallback)', () => {
30
+ test('flag OFF → the capture-pane fallback VERBATIM (byte-identical to today, no model built)', async () => {
31
+ const d = mkLogDir()
32
+ expect(await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 80, rows: 24 })).toBe(FALLBACK)
33
+ })
34
+
35
+ test('flag ON + a pane-log → the MODEL viewport (source swapped), not the fallback', async () => {
36
+ const d = mkLogDir()
37
+ writeFileSync(join(d, 'codex-x.pty-readygate'), '')
38
+ writeFileSync(join(d, 'codex-x.log'), '\x1b[24;1H› ready prompt here') // render a codex '›' prompt row
39
+ const v = await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 80, rows: 24 })
40
+ expect(v).not.toBe(FALLBACK) // the source was swapped to the model
41
+ expect(v).toContain('› ready prompt here') // the model rendered the prompt the predicate keys on
42
+ })
43
+
44
+ test('flag ON but the pane-log is MISSING → fallback (never worse than tmux)', async () => {
45
+ const d = mkLogDir()
46
+ writeFileSync(join(d, 'codex-x.pty-readygate'), '')
47
+ expect(await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 80, rows: 24 })).toBe(FALLBACK)
48
+ })
49
+
50
+ test('flag ON but geometry is unknown (0x0) → fallback', async () => {
51
+ const d = mkLogDir()
52
+ writeFileSync(join(d, 'codex-x.pty-readygate'), '')
53
+ writeFileSync(join(d, 'codex-x.log'), '\x1b[24;1H› ready')
54
+ expect(await readyGateViewport({ logDir: d, identity: 'codex-x', fallback: FALLBACK, cols: 0, rows: 0 })).toBe(FALLBACK)
55
+ })
56
+ })
57
+
58
+ describe('logReadyGateCrossCheck (serving-path soak observability)', () => {
59
+ test('appends a record with diverged computed; agreement → false, mismatch → true', () => {
60
+ const d = mkLogDir()
61
+ logReadyGateCrossCheck(d, { identity: 'codex-x', modelReady: true, captureReady: false, geom: '220x50', nowISO: '2026-06-14T00:00:00Z' })
62
+ logReadyGateCrossCheck(d, { identity: 'codex-x', modelReady: true, captureReady: true, geom: '220x50', nowISO: '2026-06-14T00:00:05Z' })
63
+ const lines = readFileSync(join(d, 'readygate-flip.jsonl'), 'utf8').trim().split('\n').map(l => JSON.parse(l))
64
+ expect(lines[0].diverged).toBe(true) // model ahead of capture
65
+ expect(lines[0].modelReady).toBe(true)
66
+ expect(lines[0].captureReady).toBe(false)
67
+ expect(lines[1].diverged).toBe(false) // agreement
68
+ })
69
+ test('no event-log dir → no-op (never blocks the ready-gate)', () => {
70
+ expect(() => logReadyGateCrossCheck(undefined, { identity: 'x', modelReady: true, captureReady: true, geom: '1x1', nowISO: 't' })).not.toThrow()
71
+ })
72
+ })
@@ -0,0 +1,99 @@
1
+ // Ф1 READY-GATE viewport-source FLIP (cutover Block 3) — DARK / flag-gated, canary-first.
2
+ //
3
+ // The cold-wake ready-gate (launch index.ts step 6) decides "is the input surface ready?" by
4
+ // running adapter.isInputReady on a VIEWPORT. Today that viewport is a `tmux capture-pane` scrape.
5
+ // This is the FIRST migration mutation: swap the viewport SOURCE to the pty-MODEL (an @xterm/headless
6
+ // emulation of the daemon's pane-log — the SAME raw child bytes tmux renders), keeping the SAME
7
+ // production predicate (adapter.isInputReady) and an UNCONDITIONAL capture-pane fallback.
8
+ //
9
+ // Why this is low-radius (boris's frame): it is a viewport-SOURCE swap, NOT a predicate change.
10
+ // The model verdict was validated 0-divergence vs capture-pane on the readiness predicate by the
11
+ // burn-in gate (incl. the codex resume-with-history splash-off-screen center cell) before any peer
12
+ // is flipped. It touches ONLY the isInputReady INPUT — dialog detection upstream and the delivery +
13
+ // delivery-confirm downstream (submitIntoTui's submit-landed + deliverViaTmux's transcript-mtime
14
+ // advance, the "real model advancement" final-arbiter) are untouched, so the canonical
15
+ // harmless-false-glyph property is preserved by construction.
16
+ //
17
+ // FLAG: a per-peer marker `<logDir>/<identity>.pty-readygate`. Absent → capture-pane (byte-identical
18
+ // to today). Present → the model viewport. Per-peer so the first flip is a single canary with the
19
+ // whole fleet still on tmux; dynamic (touch/rm, no daemon restart). @xterm is dynamic-imported ONLY
20
+ // when the flag is set, so the launch hot path stays @xterm-free with the flip off.
21
+
22
+ import { appendFileSync, closeSync, existsSync, fstatSync, openSync, readSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+
25
+ const SEED_BYTES = 4 * 1024 * 1024
26
+
27
+ /**
28
+ * SERVING-path cross-check log (the post-flip soak gate). When a peer is flipped, the MODEL verdict
29
+ * is AUTHORITATIVE in the cold-wake ready-gate; this records, per evaluation, what capture-pane WOULD
30
+ * have said — so a soak can prove 0 model-vs-capture divergence IN THE SERVING PATH (the read-only
31
+ * burn-in measures continuously but never drove the actual ready decision). Append-only jsonl under
32
+ * the event-log dir. Best-effort: a log hiccup never affects the ready decision. No-op without a dir.
33
+ */
34
+ export function logReadyGateCrossCheck(
35
+ eventLogDir: string | undefined,
36
+ rec: { identity: string; modelReady: boolean; captureReady: boolean; geom: string; nowISO: string },
37
+ ): void {
38
+ if (!eventLogDir) return
39
+ try {
40
+ appendFileSync(
41
+ join(eventLogDir, 'readygate-flip.jsonl'),
42
+ JSON.stringify({
43
+ ts: rec.nowISO,
44
+ identity: rec.identity,
45
+ modelReady: rec.modelReady,
46
+ captureReady: rec.captureReady,
47
+ diverged: rec.modelReady !== rec.captureReady,
48
+ geom: rec.geom,
49
+ }) + '\n',
50
+ )
51
+ } catch {
52
+ /* observability only — never block the ready-gate */
53
+ }
54
+ }
55
+
56
+ /** Per-peer ready-gate flip flag: the marker `<logDir>/<identity>.pty-readygate`. */
57
+ export function readyGatePtyFlipEnabled(logDir: string, identity: string): boolean {
58
+ return existsSync(`${logDir}/${identity}.pty-readygate`)
59
+ }
60
+
61
+ export interface ReadyGateViewportOpts {
62
+ logDir: string
63
+ identity: string
64
+ /** The capture-pane scrape — returned verbatim when the flag is off OR any model build fails. */
65
+ fallback: string
66
+ cols: number
67
+ rows: number
68
+ }
69
+
70
+ /**
71
+ * Resolve the ready-gate viewport. Flag OFF → `fallback` (capture-pane) verbatim — zero work, the
72
+ * caller never even reaches here unless it pre-checked the flag, but this stays safe regardless.
73
+ * Flag ON → the pty-model viewport as plain text. ANY failure (unreadable/short log, @xterm hiccup)
74
+ * → `fallback`: the flip can never make readiness WORSE than capture-pane today (tmux is the floor).
75
+ */
76
+ export async function readyGateViewport(opts: ReadyGateViewportOpts): Promise<string> {
77
+ if (!readyGatePtyFlipEnabled(opts.logDir, opts.identity)) return opts.fallback
78
+ if (!(opts.cols > 0 && opts.rows > 0)) return opts.fallback
79
+ const log = `${opts.logDir}/${opts.identity}.log`
80
+ if (!existsSync(log)) return opts.fallback
81
+ try {
82
+ const { Terminal } = await import('@xterm/headless')
83
+ const { modelToPlainText } = await import('../shadow/index.ts')
84
+ const fd = openSync(log, 'r')
85
+ try {
86
+ const sz = fstatSync(fd).size
87
+ const off = Math.max(0, sz - SEED_BYTES)
88
+ const buf = Buffer.alloc(sz - off)
89
+ readSync(fd, buf, 0, buf.length, off)
90
+ const term = new Terminal({ cols: opts.cols, rows: opts.rows, allowProposedApi: true, scrollback: 3000 })
91
+ await new Promise<void>(res => term.write(buf, () => res()))
92
+ return modelToPlainText(term, opts.cols, opts.rows)
93
+ } finally {
94
+ closeSync(fd)
95
+ }
96
+ } catch {
97
+ return opts.fallback // tmux-fallback is unconditional — never worse than today
98
+ }
99
+ }
@@ -145,26 +145,62 @@ describe('GateTracker — coverage matrix (clean counts only over traversed cell
145
145
  })
146
146
  })
147
147
 
148
- describe('GateTracker — center cell (codex×wake-resume×history readiness caught by the model)', () => {
149
- test('a history-bearing peer whose readiness the model catches at the tmux transition → counted', () => {
148
+ describe('GateTracker — robust center cell (gone→back + history + first-ready parity; cadence-proof)', () => {
149
+ // Drive a peer through a gone→back cycle: birth (not-ready), then die. The caller's next sample
150
+ // is the re-enroll (a resume/restart), where the center is evaluated at the first ready sample.
151
+ const goneBack = (g: GateTracker, peer: string, rt: string): void => {
152
+ g.sample(0, mkSample(peer, rt, { hasHistory: true, readyGate: { pty: false, tmux: false } }))
153
+ g.peerGone(peer)
154
+ }
155
+
156
+ test('warm-continuous peer (never gone→back) → NOT counted, even ready-with-history (warm-exclusion)', () => {
150
157
  const g = new GateTracker()
151
- g.sample(0, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: false } }))
152
- g.sample(1, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } })) // ready, model agreed
153
- expect(g.summary().coverage.readyResumeCaught['codex']).toBe(1)
158
+ g.sample(0, mkSample('w', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: false } }))
159
+ g.sample(1, mkSample('w', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } }))
160
+ expect(g.summary().coverage.readyResumeCaught['codex']).toBeUndefined()
161
+ })
162
+
163
+ test('gone→back + history + first-ready parity → counted (robust, even first-sight-already-ready)', () => {
164
+ const g = new GateTracker()
165
+ goneBack(g, 'c', 'codex')
166
+ g.sample(1, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } })) // re-enroll already-ready (fast resume)
167
+ const cov = g.summary().coverage
168
+ expect(cov.readyResumeCaught['codex']).toBe(1)
169
+ expect(cov.readyResumeCaughtViaTransition['codex']).toBeUndefined() // window not observed (fast) — still counted
170
+ })
171
+
172
+ test('gone→back WITHOUT history → NOT counted (a fresh restart, not a resume-with-history)', () => {
173
+ const g = new GateTracker()
174
+ g.sample(0, mkSample('c', 'codex', { hasHistory: false, readyGate: { pty: false, tmux: false } }))
175
+ g.peerGone('c')
176
+ g.sample(1, mkSample('c', 'codex', { hasHistory: false, readyGate: { pty: true, tmux: true } }))
177
+ expect(g.summary().coverage.readyResumeCaught['codex']).toBeUndefined()
154
178
  })
155
- test('model MISSES readiness at the transition → NOT counted, and it is a pty-behind (dirty)', () => {
179
+
180
+ test('gone→back + history + DIVERGENCE at first ready (model missed) → NOT counted + dirty (behind)', () => {
156
181
  const g = new GateTracker()
157
- g.sample(0, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: false } }))
182
+ goneBack(g, 'c', 'codex')
158
183
  g.sample(1, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: true } })) // tmux ready, model NOT
159
184
  const s = g.summary()
160
185
  expect(s.coverage.readyResumeCaught['codex']).toBeUndefined()
161
- expect(s.verdicts.readyGate.behind).toBeGreaterThanOrEqual(1) // the exact splash-blind bug
186
+ expect(s.verdicts.readyGate.behind).toBeGreaterThanOrEqual(1) // the splash-blind bug → dirty → ineligible
162
187
  })
163
- test('a FRESH (no-history) peer ready transition is NOT a center-cell hit', () => {
188
+
189
+ test('counted exactly ONCE per life (dedup: one resume = one center++)', () => {
164
190
  const g = new GateTracker()
165
- g.sample(0, mkSample('f', 'codex', { hasHistory: false, readyGate: { pty: false, tmux: false } }))
166
- g.sample(1, mkSample('f', 'codex', { hasHistory: false, readyGate: { pty: true, tmux: true } }))
167
- expect(g.summary().coverage.readyResumeCaught['codex']).toBeUndefined()
191
+ goneBack(g, 'c', 'codex')
192
+ for (let t = 1; t <= 4; t++) g.sample(t, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } }))
193
+ expect(g.summary().coverage.readyResumeCaught['codex']).toBe(1)
194
+ })
195
+
196
+ test('observed not-ready→ready window → counted AND flagged caughtViaTransition (diagnostic only)', () => {
197
+ const g = new GateTracker()
198
+ goneBack(g, 'c', 'codex')
199
+ g.sample(1, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: false, tmux: false } })) // re-enroll not-ready
200
+ g.sample(2, mkSample('c', 'codex', { hasHistory: true, readyGate: { pty: true, tmux: true } })) // becomes ready
201
+ const cov = g.summary().coverage
202
+ expect(cov.readyResumeCaught['codex']).toBe(1)
203
+ expect(cov.readyResumeCaughtViaTransition['codex']).toBe(1) // window was observed
168
204
  })
169
205
  })
170
206
 
@@ -200,6 +236,7 @@ describe('evaluateReadyGateFlipEligibility (boris guardrail: the eligibility log
200
236
  restarts: over.restarts ?? {},
201
237
  historySamples: {},
202
238
  readyResumeCaught: over.center ?? {},
239
+ readyResumeCaughtViaTransition: {},
203
240
  },
204
241
  })
205
242
  const COMPLETE = {
@@ -96,7 +96,12 @@ interface PeerState {
96
96
  /** History present at first sight ⇒ this peer woke WITH resume-history (vs a fresh empty birth)
97
97
  * — the population the center cell (codex×wake-resume×history) is measured on. */
98
98
  bornWithHistory: boolean
99
- /** Previous tmux ready value, to detect the false/none→true readiness transition. */
99
+ /** This peer reappeared after we saw it die (it was in goneOnce) — a gone→back resume/restart,
100
+ * NOT a warm-continuous peer. The center cell's warm-exclusion discriminator. */
101
+ wasGoneReenroll: boolean
102
+ /** The center cell is evaluated exactly ONCE per life (first ready sample) — dedup guard. */
103
+ centerEvaluated: boolean
104
+ /** Previous tmux ready value, to annotate whether the not-ready→ready window was observed. */
100
105
  prevReadyTmux: boolean | null
101
106
  open: Record<Verdict, OpenAhead | null>
102
107
  }
@@ -123,10 +128,13 @@ export class GateTracker {
123
128
  private births = new Map<string, number>() // `${runtime}/${fresh|wake-resume}` → count
124
129
  private restarts = new Map<string, number>() // runtime → count
125
130
  private historySamples = new Map<string, number>() // runtime → count
126
- // CENTER cell: a wake-resume-with-history peer whose readiness the MODEL caught at the tmux
127
- // ready transition (rgTmux false/none→true with rgPty agreeing) — the codex splash-off-screen
128
- // case the ready-gate flip exists for. A behind at that transition instead lands in readyGate.behind.
131
+ // CENTER cell (robust, first-sight): a gone→back-with-history peer whose readiness the MODEL
132
+ // matches tmux on at its first ready sample — the codex splash-off-screen case the ready-gate flip
133
+ // exists for. A model MISS there lands in readyGate.behind (dirty). PRIMARY eligibility counter.
129
134
  private readyResumeCaught = new Map<string, number>() // runtime → count
135
+ // Diagnostic ONLY (not a threshold path): the subset of center hits where the not-ready→ready
136
+ // window was actually observed (prev===false) — "we caught the window", a stronger evidence note.
137
+ private readyResumeCaughtViaTransition = new Map<string, number>() // runtime → count
130
138
  private runtimesSeen = new Set<string>()
131
139
 
132
140
  constructor(cfg: GateConfig = {}) {
@@ -156,10 +164,13 @@ export class GateTracker {
156
164
  this.runtimesSeen.add(s.runtime)
157
165
  let ps = this.peers.get(s.peer)
158
166
  if (!ps) {
159
- // First sight this lifetime → a birth (or a restart if we saw it die before).
167
+ // First sight this lifetime → a birth. A peer we saw DIE before (in goneOnce) reappearing is
168
+ // a gone→back cycle (a resume/restart, NOT a warm-continuous peer) — the discriminator the
169
+ // robust center cell keys on.
160
170
  const birthType = s.hasHistory ? 'wake-resume' : 'fresh'
161
171
  this.inc(this.births, `${s.runtime}/${birthType}`)
162
- if (this.goneOnce.has(s.peer)) {
172
+ const reenrolled = this.goneOnce.has(s.peer)
173
+ if (reenrolled) {
163
174
  this.inc(this.restarts, s.runtime)
164
175
  this.goneOnce.delete(s.peer)
165
176
  }
@@ -169,6 +180,8 @@ export class GateTracker {
169
180
  readyStreak: 0,
170
181
  bootDone: false,
171
182
  bornWithHistory: s.hasHistory,
183
+ wasGoneReenroll: reenrolled,
184
+ centerEvaluated: false,
172
185
  prevReadyTmux: null,
173
186
  open: { occupancy: null, readyGate: null, liveness: null },
174
187
  }
@@ -184,17 +197,23 @@ export class GateTracker {
184
197
  }
185
198
  const bootWindow = !ps.bootDone
186
199
 
187
- // Center-cell signal: at the readiness transition (rgTmux false/none→true) of a
188
- // wake-resume-with-history peer, did the MODEL catch readiness too (rgPty=true)? That is the
189
- // codex splash-off-screen case the ready-gate flip exists for. (A model MISS at this transition
190
- // is a pty-behind → readyGate.behind → dirty → ineligible, which is exactly right.)
200
+ // CENTER cell — cadence-ROBUST (first-sight, the SAME invariant births/restarts use), so a fast
201
+ // resume can't slip through a transition-polling gap. Counted ONCE per life (dedup: one resume =
202
+ // one center++) at the FIRST ready sample of a peer that (a) is a gone→back re-enroll (resume/
203
+ // restart, NOT warm-continuous — the warm-exclusion discriminator: a warm peer never entered
204
+ // goneOnce; also stricter than a transition, which a fresh peer's first boot would trip) and
205
+ // (b) re-enrolled WITH history (a resume-with-history). Parity there (rgPty===rgTmux===true) on a
206
+ // splash-off-screen frame validates the source swap; a model MISS is a pty-behind (the splash-blind
207
+ // bug) → readyGate.behind → dirty → ineligible. The observed not-ready→ready transition is kept as a
208
+ // DIAGNOSTIC annotation only ("we caught the window" — stronger evidence), NOT a second path to threshold.
191
209
  if (s.readyGate.tmux !== null) {
192
- // becameReady requires an OBSERVED not-ready→ready transition (prev === false), NOT
193
- // first-sight-already-ready (prev === null): the center cell must capture a REAL resume
194
- // splash→ready the observer witnessed (boris's representativeness guardrail), not a warm
195
- // peer that was already up when the observer attached.
196
- const becameReady = s.readyGate.tmux === true && ps.prevReadyTmux === false
197
- if (becameReady && ps.bornWithHistory && s.readyGate.pty === true) this.inc(this.readyResumeCaught, s.runtime)
210
+ if (ps.wasGoneReenroll && ps.bornWithHistory && !ps.centerEvaluated && s.readyGate.tmux === true) {
211
+ ps.centerEvaluated = true // evaluate exactly once, at the first ready sample
212
+ if (s.readyGate.pty === true) {
213
+ this.inc(this.readyResumeCaught, s.runtime)
214
+ if (ps.prevReadyTmux === false) this.inc(this.readyResumeCaughtViaTransition, s.runtime) // diagnostic: window caught
215
+ }
216
+ }
198
217
  ps.prevReadyTmux = s.readyGate.tmux
199
218
  }
200
219
 
@@ -307,6 +326,7 @@ export class GateTracker {
307
326
  restarts: Object.fromEntries([...this.restarts].sort()),
308
327
  historySamples: Object.fromEntries([...this.historySamples].sort()),
309
328
  readyResumeCaught: Object.fromEntries([...this.readyResumeCaught].sort()),
329
+ readyResumeCaughtViaTransition: Object.fromEntries([...this.readyResumeCaughtViaTransition].sort()),
310
330
  },
311
331
  }
312
332
  }
@@ -354,8 +374,12 @@ export interface GateCoverage {
354
374
  births: Record<string, number>
355
375
  restarts: Record<string, number>
356
376
  historySamples: Record<string, number>
357
- /** runtime → count of wake-resume-with-history readiness transitions the MODEL caught (center cell). */
377
+ /** runtime → center-cell count: gone→back-with-history peers whose first-ready model verdict
378
+ * matched tmux (robust, first-sight). The PRIMARY ready-gate-flip eligibility signal. */
358
379
  readyResumeCaught: Record<string, number>
380
+ /** runtime → DIAGNOSTIC subset of readyResumeCaught where the not-ready→ready window was observed
381
+ * (transition caught). Stronger evidence; NOT a separate threshold path. */
382
+ readyResumeCaughtViaTransition: Record<string, number>
359
383
  }
360
384
 
361
385
  export interface GateSummary {