@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 +1 -1
- package/src/launch/index.ts +24 -1
- package/src/launch/readyGateModel.test.ts +72 -0
- package/src/launch/readyGateModel.ts +99 -0
- package/src/shadow/gate.test.ts +49 -12
- package/src/shadow/gate.ts +41 -17
package/package.json
CHANGED
package/src/launch/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/shadow/gate.test.ts
CHANGED
|
@@ -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 (
|
|
149
|
-
|
|
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('
|
|
152
|
-
g.sample(1, mkSample('
|
|
153
|
-
expect(g.summary().coverage.readyResumeCaught['codex']).
|
|
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
|
-
|
|
179
|
+
|
|
180
|
+
test('gone→back + history + DIVERGENCE at first ready (model missed) → NOT counted + dirty (behind)', () => {
|
|
156
181
|
const g = new GateTracker()
|
|
157
|
-
g
|
|
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
|
|
186
|
+
expect(s.verdicts.readyGate.behind).toBeGreaterThanOrEqual(1) // the splash-blind bug → dirty → ineligible
|
|
162
187
|
})
|
|
163
|
-
|
|
188
|
+
|
|
189
|
+
test('counted exactly ONCE per life (dedup: one resume = one center++)', () => {
|
|
164
190
|
const g = new GateTracker()
|
|
165
|
-
g
|
|
166
|
-
g.sample(
|
|
167
|
-
expect(g.summary().coverage.readyResumeCaught['codex']).
|
|
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 = {
|
package/src/shadow/gate.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
127
|
-
//
|
|
128
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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 {
|