@agfpd/iapeer 0.2.46 → 0.2.48
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
|
@@ -223,17 +223,28 @@ export const codexAdapter: RuntimeAdapter = {
|
|
|
223
223
|
},
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* Ready for the first message iff the codex TUI
|
|
227
|
-
*
|
|
228
|
-
* - pane includes 'OpenAI Codex' — the TUI splash/header is present.
|
|
226
|
+
* Ready for the first message iff the codex TUI composer is rendered and no
|
|
227
|
+
* startup screen is still up:
|
|
229
228
|
* - pane does NOT include 'Press enter to continue' — the update screen (and
|
|
230
|
-
* other "press enter" startup prompts) is gone
|
|
229
|
+
* other "press enter" startup prompts) is gone (belt-and-suspenders: the
|
|
230
|
+
* boot loop answers known dialogs via bootDialogKeys BEFORE asking this).
|
|
231
231
|
* - some line, trimStart, startsWith '› ' — the rendered input arrow at the
|
|
232
|
-
* start of the
|
|
233
|
-
* input arrow is the stable ready invariant — not any single tip.)
|
|
232
|
+
* start of the composer row. (Codex rotates its top-of-pane tip line, so
|
|
233
|
+
* the input arrow is the stable ready invariant — not any single tip.)
|
|
234
|
+
*
|
|
235
|
+
* DELIBERATELY no 'OpenAI Codex' splash check (the watcher.codexInputReady
|
|
236
|
+
* heritage requirement, removed 12.06): `resume --last` replays the prior
|
|
237
|
+
* transcript ABOVE the composer, and a history taller than the pane scrolls
|
|
238
|
+
* the splash box off the top of capture-pane — the predicate then stays false
|
|
239
|
+
* for the entire boot deadline while the composer sits ready, failing the wake
|
|
240
|
+
* `never-became-ready` and LOSING the triggering message (live incidents
|
|
241
|
+
* 11.06 19:21Z + 12.06 12:07Z, codex-linus). A replayed history message also
|
|
242
|
+
* renders with a leading '› ', so this can fire a frame before the composer
|
|
243
|
+
* activates — benign: the paste/Enter queue in the pty and land in the
|
|
244
|
+
* composer, and the ready-gate still verifies a real model turn (activity
|
|
245
|
+
* mtime advance) before declaring READY.
|
|
234
246
|
*/
|
|
235
247
|
isInputReady(pane: string): boolean {
|
|
236
|
-
if (!pane.includes('OpenAI Codex')) return false
|
|
237
248
|
if (pane.includes('Press enter to continue')) return false
|
|
238
249
|
return pane.split(/\r?\n/).some(line => line.trimStart().startsWith('› '))
|
|
239
250
|
},
|
package/src/launch/index.ts
CHANGED
|
@@ -114,6 +114,22 @@ function sleep(ms: number): Promise<void> {
|
|
|
114
114
|
function fail(identity: string, reason: string): LaunchResult {
|
|
115
115
|
return { status: 'FAILED', identity, process_address: identity, reason }
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The last two non-empty lines of a pane capture, flattened for embedding in a
|
|
120
|
+
* fail reason: quotes → ', whitespace runs collapsed, joined with ' ⏎ ', clipped
|
|
121
|
+
* to 160 chars. '' for an empty/blank capture (caller omits the clause). Exported
|
|
122
|
+
* for unit tests — the boot loop embeds this in `never-became-ready` so the
|
|
123
|
+
* delivery.log err field shows what the TUI was actually displaying.
|
|
124
|
+
*/
|
|
125
|
+
export function paneTail(pane: string): string {
|
|
126
|
+
const lines = pane
|
|
127
|
+
.split(/\r?\n/)
|
|
128
|
+
.map(l => l.replace(/\s+/g, ' ').trim())
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
if (lines.length === 0) return ''
|
|
131
|
+
return lines.slice(-2).join(' ⏎ ').replace(/"/g, "'").slice(0, 160)
|
|
132
|
+
}
|
|
117
133
|
function ready(identity: string): LaunchResult {
|
|
118
134
|
return { status: 'READY', identity, process_address: identity }
|
|
119
135
|
}
|
|
@@ -347,6 +363,7 @@ export const launch: LaunchFn = async (
|
|
|
347
363
|
const baselineMtime = adapter.newestActivityMtime(cwd) ?? 0
|
|
348
364
|
const bootIters = Math.max(1, Math.ceil(cfg.bootDeadlineSecs / 2))
|
|
349
365
|
let delivered = false
|
|
366
|
+
let lastPane = '' // the final capture — surfaced in the never-became-ready reason
|
|
350
367
|
for (let i = 0; i < bootIters && !delivered; i++) {
|
|
351
368
|
await sleep(2000)
|
|
352
369
|
if (!sessionAlive(sock, identity)) {
|
|
@@ -354,6 +371,7 @@ export const launch: LaunchFn = async (
|
|
|
354
371
|
}
|
|
355
372
|
const pane = capturePane(sock, identity)
|
|
356
373
|
if (!pane) continue
|
|
374
|
+
lastPane = pane
|
|
357
375
|
const dialogKeys = adapter.bootDialogKeys(pane)
|
|
358
376
|
if (dialogKeys) {
|
|
359
377
|
tmux(sock, 'send-keys', '-t', identity, ...dialogKeys)
|
|
@@ -390,7 +408,15 @@ export const launch: LaunchFn = async (
|
|
|
390
408
|
}
|
|
391
409
|
}
|
|
392
410
|
if (!delivered) {
|
|
393
|
-
|
|
411
|
+
// Surface WHAT the pane showed when the gate gave up — the 12.06 codex-resume
|
|
412
|
+
// incident (splash scrolled off, composer ready, predicate false for 240 s) was
|
|
413
|
+
// undiagnosable from the bare reason alone. Last two non-empty lines, quotes
|
|
414
|
+
// flattened (the reason lands inside a logfmt-quoted err field), clipped.
|
|
415
|
+
const tail = paneTail(lastPane)
|
|
416
|
+
return fail(
|
|
417
|
+
identity,
|
|
418
|
+
`never-became-ready (stuck at a startup prompt or boot hang)${tail ? `; pane tail: ${tail}` : ''}`,
|
|
419
|
+
)
|
|
394
420
|
}
|
|
395
421
|
|
|
396
422
|
// (7) READY-GATE — the activity proxy must strictly advance past baseline
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { exitCauseHook, exitLogPath, getAdapter, launch } from './index.ts'
|
|
2
|
+
import { exitCauseHook, exitLogPath, getAdapter, launch, paneTail } from './index.ts'
|
|
3
3
|
import { claudeAdapter } from './adapters/claude.ts'
|
|
4
4
|
import { codexAdapter } from './adapters/codex.ts'
|
|
5
5
|
import { telegramAdapter } from './adapters/telegram.ts'
|
|
@@ -74,6 +74,64 @@ describe('codexAdapter.buildArgv', () => {
|
|
|
74
74
|
})
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
+
describe('codexAdapter.isInputReady', () => {
|
|
78
|
+
test('fresh boot: splash + composer → true', () => {
|
|
79
|
+
const pane = [
|
|
80
|
+
'╭───────────────────────────────╮',
|
|
81
|
+
'│ >_ OpenAI Codex (v0.138.0) │',
|
|
82
|
+
'╰───────────────────────────────╯',
|
|
83
|
+
'› Explain this codebase',
|
|
84
|
+
'gpt-5.5 xhigh · ~/Peers/p',
|
|
85
|
+
].join('\n')
|
|
86
|
+
expect(codexAdapter.isInputReady(pane)).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('resume replay scrolled the splash off — composer alone is READY (12.06 incident)', () => {
|
|
90
|
+
// The live zombie pane shape: history tail + MCP noise + composer, NO
|
|
91
|
+
// 'OpenAI Codex' anywhere in the visible capture. The old predicate required
|
|
92
|
+
// the splash and spun false for the whole 240 s boot deadline.
|
|
93
|
+
const pane = [
|
|
94
|
+
' <message><![CDATA[Компакт прошел?]]></message>',
|
|
95
|
+
' </iap>',
|
|
96
|
+
'• Принял. Ответ пиру не слал: он явно написал, что больше ничего не нужно.',
|
|
97
|
+
'⚠ MCP startup incomplete (failed: iap)',
|
|
98
|
+
'› Explain this codebase',
|
|
99
|
+
'gpt-5.5 xhigh · ~/Peers/p',
|
|
100
|
+
].join('\n')
|
|
101
|
+
expect(pane.includes('OpenAI Codex')).toBe(false) // fixture honesty
|
|
102
|
+
expect(codexAdapter.isInputReady(pane)).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('update screen up → false even with a composer-looking line', () => {
|
|
106
|
+
const pane = ['Update available!', 'Press enter to continue', '› something'].join('\n')
|
|
107
|
+
expect(codexAdapter.isInputReady(pane)).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('no composer row yet (box only, model loading) → false', () => {
|
|
111
|
+
const pane = ['│ >_ OpenAI Codex (v0.138.0) │', '│ model: loading │'].join('\n')
|
|
112
|
+
expect(codexAdapter.isInputReady(pane)).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('bare › without trailing space (a stray glyph) → false', () => {
|
|
116
|
+
expect(codexAdapter.isInputReady('›\nnothing else')).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('paneTail (never-became-ready observability)', () => {
|
|
121
|
+
test('last two non-empty lines, joined, quotes flattened', () => {
|
|
122
|
+
const pane = 'first\n\n middle line \n⚠ MCP "failed"\n› composer\n\n'
|
|
123
|
+
expect(paneTail(pane)).toBe("⚠ MCP 'failed' ⏎ › composer")
|
|
124
|
+
})
|
|
125
|
+
test('single line and empty captures', () => {
|
|
126
|
+
expect(paneTail('only')).toBe('only')
|
|
127
|
+
expect(paneTail('')).toBe('')
|
|
128
|
+
expect(paneTail('\n \n')).toBe('')
|
|
129
|
+
})
|
|
130
|
+
test('clips to 160 chars', () => {
|
|
131
|
+
expect(paneTail('x'.repeat(300)).length).toBe(160)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
77
135
|
describe('telegramAdapter (router — no TUI surface)', () => {
|
|
78
136
|
test('router predicates are trivial', () => {
|
|
79
137
|
expect(telegramAdapter.bootDialogKeys('anything')).toBeNull()
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -1251,20 +1251,29 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1251
1251
|
* consumed BEFORE the relaunch so a relaunch failure does not loop on the same eager mark
|
|
1252
1252
|
* (it then fresh-wakes lazily on its next message — branch 3a — never lost). NB: a /new'd
|
|
1253
1253
|
* peer is expected to carry an initial_prompt; without one the first turn delivers nothing.
|
|
1254
|
+
*
|
|
1255
|
+
* Runtime is DELIBERATELY NOT pinned to the dead session's runtime: the relaunch passes
|
|
1256
|
+
* no runtime so resolveWakeRuntime falls through to the registry default (H5 — registry
|
|
1257
|
+
* is the only source of wake-runtime truth). Pinning broke the fleet-switch procedure
|
|
1258
|
+
* (`default-runtime <rt>` + `self-fresh`): the peer silently resurrected on the OLD
|
|
1259
|
+
* runtime, ignoring the just-flipped default (live incident 12.06, claude-iapeer). A
|
|
1260
|
+
* session deliberately living on a non-default runtime has explicit paths back (wake
|
|
1261
|
+
* with an explicit runtime); an owner /new returns the peer to its declared default.
|
|
1254
1262
|
*/
|
|
1255
1263
|
export async function processEagerRelaunches(
|
|
1256
1264
|
cfg: LifecycleConfig,
|
|
1257
1265
|
outcomes: SuperviseOutcome[],
|
|
1258
|
-
deps: WakeDeps = {},
|
|
1266
|
+
deps: WakeDeps & { wakeFn?: (args: WakeArgs, deps: WakeDeps) => Promise<WakeResult> } = {},
|
|
1259
1267
|
): Promise<WakeResult[]> {
|
|
1268
|
+
const wake = deps.wakeFn ?? wakeOrSpawn
|
|
1260
1269
|
const results: WakeResult[] = []
|
|
1261
1270
|
for (const o of outcomes) {
|
|
1262
|
-
if (o.action !== 'needs-eager-fresh' || !o.personality
|
|
1271
|
+
if (o.action !== 'needs-eager-fresh' || !o.personality) continue
|
|
1263
1272
|
clearNewEager(cfg, o.identity) // consume the eager mark — the relaunch is fresh by construction
|
|
1264
1273
|
try {
|
|
1265
1274
|
results.push(
|
|
1266
|
-
await
|
|
1267
|
-
{ personality: o.personality,
|
|
1275
|
+
await wake(
|
|
1276
|
+
{ personality: o.personality, task: '', resume: false },
|
|
1268
1277
|
{ cfg, env: deps.env },
|
|
1269
1278
|
),
|
|
1270
1279
|
)
|
|
@@ -621,6 +621,38 @@ describe('superviseTick death-cause accounting (TARGET)', () => {
|
|
|
621
621
|
}
|
|
622
622
|
})
|
|
623
623
|
|
|
624
|
+
test('eager relaunch does NOT pin the dead session runtime — re-resolves from the registry (H5 fleet-switch)', async () => {
|
|
625
|
+
// Live incident 12.06: `default-runtime codex` + `self-fresh` resurrected the peer
|
|
626
|
+
// on the OLD runtime — the relaunch passed the dead session's runtime EXPLICITLY,
|
|
627
|
+
// so resolveWakeRuntime never consulted the just-flipped registry default.
|
|
628
|
+
const { env, cfg, root, laDir } = deadSessionEnv('rt')
|
|
629
|
+
try {
|
|
630
|
+
setNewEager(cfg, 'claude-rt')
|
|
631
|
+
const outcomes = superviseTick(cfg, { env })
|
|
632
|
+
const o = outcomes.find(x => x.identity === 'claude-rt')
|
|
633
|
+
expect(o?.action).toBe('needs-eager-fresh')
|
|
634
|
+
const seen: Array<{ personality: string; runtime?: string; resume?: boolean; task: string }> = []
|
|
635
|
+
const { processEagerRelaunches } = await import('./index.ts')
|
|
636
|
+
await processEagerRelaunches(cfg, outcomes, {
|
|
637
|
+
env,
|
|
638
|
+
wakeFn: async args => {
|
|
639
|
+
seen.push({ personality: args.personality, runtime: args.runtime, resume: args.resume, task: args.task })
|
|
640
|
+
return { status: 'READY', woke: true }
|
|
641
|
+
},
|
|
642
|
+
})
|
|
643
|
+
expect(seen.length).toBe(1)
|
|
644
|
+
expect(seen[0].personality).toBe('rt')
|
|
645
|
+
expect(seen[0].runtime).toBeUndefined() // ← the fix: wakeOrSpawn re-resolves from the registry
|
|
646
|
+
expect(seen[0].resume).toBe(false)
|
|
647
|
+
expect(seen[0].task).toBe('')
|
|
648
|
+
// the mark was consumed by the relaunch
|
|
649
|
+
expect(hasNewEager(cfg, 'claude-rt')).toBe(false)
|
|
650
|
+
} finally {
|
|
651
|
+
rmSync(root, { recursive: true, force: true })
|
|
652
|
+
rmSync(laDir, { recursive: true, force: true })
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
|
|
624
656
|
test('a DEAD session with NO .new-eager → reaped-gone, NO .idle-reaped written (crash leaves no marker)', () => {
|
|
625
657
|
const { env, cfg, root, laDir } = deadSessionEnv('w')
|
|
626
658
|
try {
|