@agfpd/iapeer 0.2.46 → 0.2.47
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/adapters/codex.ts +18 -7
- package/src/launch/index.ts +27 -1
- package/src/launch/launch.test.ts +59 -1
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()
|