@agfpd/iapeer 0.4.29 → 0.4.31
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
|
@@ -136,6 +136,11 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
136
136
|
ghostTextSgr: ['2', '38;5;246'],
|
|
137
137
|
},
|
|
138
138
|
|
|
139
|
+
// claude logs an accepted paste PROMPTLY (sub-second: a queue-operation when busy,
|
|
140
|
+
// the user-turn when idle), so the message-specific transcript confirm is both cheap
|
|
141
|
+
// and a swallow-guard (the false-OK class). Keep it. (See RuntimeAdapter.deliveryConfirm.)
|
|
142
|
+
deliveryConfirm: 'transcript',
|
|
143
|
+
|
|
139
144
|
/**
|
|
140
145
|
* argv = claudeBin + headless flags + (system-prompt-file when set) +
|
|
141
146
|
* (--continue when resuming) + extraArgs.
|
|
@@ -177,6 +177,16 @@ export const codexAdapter: RuntimeAdapter = {
|
|
|
177
177
|
ghostTextSgr: ['2', '38;5;246'],
|
|
178
178
|
},
|
|
179
179
|
|
|
180
|
+
// codex writes NO prompt-acceptance record — its session-jsonl user-input appears only
|
|
181
|
+
// when the model TURN ingests the message (during a long turn, tens of seconds away;
|
|
182
|
+
// measured ~80s live 2026-06-25). But its input queue is DURABLE: a mid-turn submit is
|
|
183
|
+
// held and processed at the next turn boundary, never lost (verified live — an 80s turn
|
|
184
|
+
// still ingested + replied with the exact probe token, despite the send false-FAILing at
|
|
185
|
+
// the 8s grace). So the flushed socket-ack IS the delivery confirm; a transcript grace
|
|
186
|
+
// only false-FAILs a message that WILL be processed → wrong fallback escalation. A
|
|
187
|
+
// genuinely dead session still fails at the socket-ack. (See RuntimeAdapter.deliveryConfirm.)
|
|
188
|
+
deliveryConfirm: 'socket-ack',
|
|
189
|
+
|
|
180
190
|
/**
|
|
181
191
|
* argv = codexBin + (resume --last when spec.resume) + the TUI args +
|
|
182
192
|
* (doctrine via -c when systemPromptFile set) + bypass + extraArgs. Order is
|
|
@@ -11,12 +11,24 @@ import { openSync, readSync, closeSync, statSync } from 'fs'
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Content-timestamp (epoch ms) of the LAST jsonl entry carrying a `timestamp` field, read from the
|
|
14
|
-
* file's TAIL
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* file's TAIL. Metadata records without a `timestamp` (claude `mode` / `ai-title` / `bridge-session`)
|
|
15
|
+
* are skipped. Returns null only when the file is unreadable / empty, or genuinely carries NO
|
|
16
|
+
* timestamped entry anywhere (the caller then floors at wokeAt).
|
|
17
|
+
*
|
|
18
|
+
* ADAPTIVE WINDOW (the giant-tail fix). The reader starts at the cheap `tailBytes` window and GROWS it
|
|
19
|
+
* (doubling, capped at file size) until a complete timestamped entry is captured. A single transcript
|
|
20
|
+
* record can dwarf the default window — a browser peer appends base64 screenshots, observed up to
|
|
21
|
+
* 368 KB (5.6× the 64 KB default). When such a record is the tail, a FIXED window holds only a fragment
|
|
22
|
+
* of one line: no complete JSON → the old code returned null, the caller floored at wokeAt, and a LIVE
|
|
23
|
+
* working session was idle-reaped (incident: mrbrowser killed mid-task, last turn 7 s before the reap).
|
|
24
|
+
* Growing guarantees we eventually read the full last record (worst case: the entire file), so a present
|
|
25
|
+
* timestamp is NEVER missed. The default window already covers the common case in one read; growth only
|
|
26
|
+
* kicks in for oversized tail records, and even then is bounded (≈log2(fileSize/tailBytes) reads).
|
|
27
|
+
*
|
|
28
|
+
* A partial LEADING line is never trusted: once the window starts past byte 0, the first split segment
|
|
29
|
+
* may be the truncated tail of an earlier record (which could mis-parse as valid JSON), so we scan only
|
|
30
|
+
* the newline-bounded segments after it. Reaching byte 0 makes every segment complete, including the
|
|
31
|
+
* first — and terminates the growth.
|
|
20
32
|
*/
|
|
21
33
|
export function lastTimestampedEntryMs(path: string, tailBytes = 65536): number | null {
|
|
22
34
|
let fd: number
|
|
@@ -27,35 +39,45 @@ export function lastTimestampedEntryMs(path: string, tailBytes = 65536): number
|
|
|
27
39
|
}
|
|
28
40
|
try {
|
|
29
41
|
const size = statSync(path).size
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
42
|
-
const line = lines[i].trim()
|
|
43
|
-
if (!line) continue
|
|
44
|
-
let obj: unknown
|
|
45
|
-
try {
|
|
46
|
-
obj = JSON.parse(line)
|
|
47
|
-
} catch {
|
|
48
|
-
continue // partial leading line / non-json → skip
|
|
42
|
+
if (size <= 0) return null
|
|
43
|
+
let window = Math.max(tailBytes, 1)
|
|
44
|
+
for (;;) {
|
|
45
|
+
const start = Math.max(0, size - window)
|
|
46
|
+
const len = size - start
|
|
47
|
+
const buf = Buffer.allocUnsafe(len)
|
|
48
|
+
let read = 0
|
|
49
|
+
while (read < len) {
|
|
50
|
+
const n = readSync(fd, buf, read, len - read, start + read)
|
|
51
|
+
if (n <= 0) break
|
|
52
|
+
read += n
|
|
49
53
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const atFileStart = start === 0
|
|
55
|
+
const lines = buf.subarray(0, read).toString('utf8').split(/\r?\n/)
|
|
56
|
+
// Segment 0 is a complete record ONLY when the window reaches byte 0; otherwise it is the possibly
|
|
57
|
+
// truncated tail of an earlier record and must not be parsed (`floor` skips it).
|
|
58
|
+
const floor = atFileStart ? 0 : 1
|
|
59
|
+
for (let i = lines.length - 1; i >= floor; i--) {
|
|
60
|
+
const line = lines[i].trim()
|
|
61
|
+
if (!line) continue
|
|
62
|
+
let obj: unknown
|
|
63
|
+
try {
|
|
64
|
+
obj = JSON.parse(line)
|
|
65
|
+
} catch {
|
|
66
|
+
continue // metadata fragment / non-json → skip
|
|
67
|
+
}
|
|
68
|
+
const ts = (obj as { timestamp?: unknown })?.timestamp
|
|
69
|
+
if (typeof ts === 'string') {
|
|
70
|
+
const ms = Date.parse(ts)
|
|
71
|
+
if (!Number.isNaN(ms)) return ms
|
|
72
|
+
} else if (typeof ts === 'number' && Number.isFinite(ts)) {
|
|
73
|
+
return ts
|
|
74
|
+
}
|
|
56
75
|
}
|
|
76
|
+
// No complete timestamped entry in this window. If the whole file is read, there genuinely is none.
|
|
77
|
+
// Otherwise the tail record outgrows the window → grow and retry (terminates: window hits size → start 0).
|
|
78
|
+
if (atFileStart) return null
|
|
79
|
+
window *= 2
|
|
57
80
|
}
|
|
58
|
-
return null
|
|
59
81
|
} catch {
|
|
60
82
|
return null
|
|
61
83
|
} finally {
|
package/src/launch/types.ts
CHANGED
|
@@ -235,6 +235,32 @@ export interface RuntimeAdapter {
|
|
|
235
235
|
*/
|
|
236
236
|
deliveryMarkers: DeliveryMarkers
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* How a delivered envelope's LANDING is confirmed AFTER the socket-ack, in
|
|
240
|
+
* deliverViaHost (transport). Owned by the adapter because it is a property of
|
|
241
|
+
* HOW the runtime logs an accepted message, not transport policy. A router
|
|
242
|
+
* (kind:'router') is always socket-ack by structure and short-circuits before
|
|
243
|
+
* this field is read; it refines the TUI runtimes. Absent → 'transcript' (the
|
|
244
|
+
* strict default: a new/unknown runtime gets the swallow-guarding confirm, never
|
|
245
|
+
* a weaker one).
|
|
246
|
+
* - 'transcript' (claude): the runtime writes an acceptance record CARRYING the
|
|
247
|
+
* envelope PROMPTLY — sub-second: a queue-operation when busy, the user-turn
|
|
248
|
+
* when idle. So a message-specific transcript-carries-envelope check within a
|
|
249
|
+
* short grace BOTH proves landing AND catches a paste swallowed at a turn
|
|
250
|
+
* boundary (the false-OK class, incident 2026-06-23). Cheap and safe → keep it.
|
|
251
|
+
* - 'socket-ack' (codex): the runtime writes NO acceptance record — its
|
|
252
|
+
* session-jsonl user-input appears only when the model TURN INGESTS the
|
|
253
|
+
* message, which during a long turn is many tens of seconds away (measured ~80s
|
|
254
|
+
* live, 2026-06-25). But its input queue is DURABLE: a mid-turn submit is HELD
|
|
255
|
+
* and processed at the next turn boundary, never lost (verified live — an 80s
|
|
256
|
+
* turn still ingested + replied with the exact probe token). So the flushed
|
|
257
|
+
* socket-ack (bytes left us AND drained to the pty) IS the delivery confirm; a
|
|
258
|
+
* transcript grace here only ever FALSE-FAILs a message that WILL be processed,
|
|
259
|
+
* wrongly escalating to a fallback peer. A genuinely dead session still fails
|
|
260
|
+
* at the socket-ack (no socket / stalled flush) → really-dead still escalates.
|
|
261
|
+
*/
|
|
262
|
+
deliveryConfirm?: 'transcript' | 'socket-ack'
|
|
263
|
+
|
|
238
264
|
/**
|
|
239
265
|
* If set, launch REFUSES unless the peer's intelligence equals this value
|
|
240
266
|
* (fail-loud). telegram → 'natural' — it is a human channel; launching an
|
package/src/transport/index.ts
CHANGED
|
@@ -157,20 +157,19 @@ export function resolvePeerDeliveryTarget(
|
|
|
157
157
|
const LIVENESS_POLL_MS = 100
|
|
158
158
|
|
|
159
159
|
// Confirm grace — how long deliverViaHost polls for a transcript record CARRYING our envelope before
|
|
160
|
-
// declaring a false-FAIL.
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
function confirmGraceMs(runtime: string): number {
|
|
160
|
+
// declaring a false-FAIL. This applies ONLY to the 'transcript'-confirm runtimes (claude): claude
|
|
161
|
+
// writes the queue-operation (busy) / user-turn (idle) record PROMPTLY (sub-second), so a short grace
|
|
162
|
+
// suffices and the confirm doubles as a swallow-guard. codex is 'socket-ack' (adapter.deliveryConfirm)
|
|
163
|
+
// and never reaches this loop — its input queue is durable but it logs the user-input only at ingest
|
|
164
|
+
// (tens of seconds into a long turn), so a grace would only false-FAIL a message that WILL be processed.
|
|
165
|
+
// `IAP_HOST_LIVENESS_GRACE_MS` overrides (operator calibration; tests set 0 for an immediate
|
|
166
|
+
// deterministic fail). Prefer a false-FAIL (sender retries) over a false-OK (silent loss, forbidden).
|
|
167
|
+
const CONFIRM_GRACE_MS = 4000
|
|
168
|
+
function confirmGraceMs(): number {
|
|
170
169
|
const raw = process.env.IAP_HOST_LIVENESS_GRACE_MS
|
|
171
170
|
const n = raw === undefined ? NaN : Number(raw)
|
|
172
171
|
if (Number.isFinite(n) && n >= 0) return n
|
|
173
|
-
return
|
|
172
|
+
return CONFIRM_GRACE_MS
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
// Launchd-revive retry — a MISS on a launchd-managed target the daemon can't wake (H4)
|
|
@@ -204,13 +203,17 @@ export interface WarmDeliverSeam {
|
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
/**
|
|
207
|
-
* Warm-deliver `envelope` to a LIVE local target over its supervisor socket (pty-only).
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
206
|
+
* Warm-deliver `envelope` to a LIVE local target over its supervisor socket (pty-only). Confirmation is
|
|
207
|
+
* adapter-driven (RuntimeAdapter.deliveryConfirm):
|
|
208
|
+
* - 'transcript' (claude): CONFIRMED MESSAGE-SPECIFICALLY — by a NEW transcript record CARRYING this
|
|
209
|
+
* envelope appearing past the pre-deliver baseline, NOT the socket-ack (a flushed CR proves the
|
|
210
|
+
* bytes left us, not that the session accepted them) and NOT a bare mtime advance (the receiver's
|
|
211
|
+
* OWN concurrent turn bumps mtime even when our paste was swallowed — the false-OK class, forbidden).
|
|
212
|
+
* - 'socket-ack' (codex): confirmed by the socket-ack — its input queue is durable (a mid-turn submit
|
|
213
|
+
* is never lost) and it logs no prompt-acceptance record, so a transcript grace would only false-FAIL.
|
|
214
|
+
* - router (telegram/notifier): no transcript → confirmed by the socket-ack (liveness is structural
|
|
215
|
+
* via launchd KeepAlive).
|
|
216
|
+
* See `deliverViaHost`.
|
|
214
217
|
*/
|
|
215
218
|
export async function deliverWarm(
|
|
216
219
|
target: DeliveryTarget,
|
|
@@ -244,21 +247,32 @@ async function deliverViaHost(
|
|
|
244
247
|
// router's liveness is structural via launchd KeepAlive (not a model turn), so the ack IS the
|
|
245
248
|
// delivery-level confirm (parity with the legacy tmux router C-j path).
|
|
246
249
|
if (adapter.kind === 'router') return ok(undefined)
|
|
250
|
+
// 'socket-ack' TUI runtimes (codex): the input queue is DURABLE — a mid-turn submit is HELD and
|
|
251
|
+
// processed at the next turn boundary, however long that is (verified live 2026-06-25: an 80s turn
|
|
252
|
+
// still ingested + replied; the send false-FAILed at the 8s grace, the message was NOT lost). codex
|
|
253
|
+
// writes NO prompt-acceptance record (its user-input lands only at ingest), so a transcript grace
|
|
254
|
+
// can't tell "swallowed" from "queued-but-not-yet-ingested" within any useful window — it only
|
|
255
|
+
// false-FAILs a message that WILL be delivered, wrongly escalating to a fallback peer. The flushed
|
|
256
|
+
// socket-ack above IS the delivery confirm; a genuinely dead session already failed at deliverHosted
|
|
257
|
+
// (no socket / stalled flush) → really-dead still escalates. (claude stays on the strict
|
|
258
|
+
// message-specific confirm below — it logs acceptance sub-second, so that is cheap AND catches a
|
|
259
|
+
// swallowed paste, the false-OK class.) Driven by adapter.deliveryConfirm, not a hardcoded runtime.
|
|
260
|
+
if (adapter.deliveryConfirm === 'socket-ack') return ok(undefined)
|
|
247
261
|
// socket-ack ≠ landed. With no transcript (direct callers/tests without a cwd) we can only trust the
|
|
248
262
|
// flushed submit — confirmed-only.
|
|
249
263
|
if (!cwd || !baseline) return ok(undefined)
|
|
250
|
-
// MESSAGE-SPECIFIC landed-confirm — the false-OK killer
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
// The receiver's own assistant/tool turn never reproduces the
|
|
257
|
-
// it cannot forge a confirm. No such record within the grace →
|
|
258
|
-
// a false-OK (silent loss the contract forbids).
|
|
264
|
+
// MESSAGE-SPECIFIC landed-confirm — the false-OK killer (claude / any 'transcript'-confirm runtime;
|
|
265
|
+
// codex took the socket-ack short-circuit above). A bare transcript/pane-log mtime BUMP is NOT proof
|
|
266
|
+
// the session took THIS message: a receiver in an active turn advances both mtimes with its OWN
|
|
267
|
+
// rendering even when our paste was swallowed at the turn boundary (incident 2026-06-23: ok=true,
|
|
268
|
+
// message gone). The ONLY proof is a NEW transcript record CARRYING our envelope — for claude, a
|
|
269
|
+
// queue-operation `content` (busy → enqueued) or the user-turn message (idle), written sub-second so
|
|
270
|
+
// a short grace covers it. The receiver's own assistant/tool turn never reproduces the
|
|
271
|
+
// `<iap from-personality=…>` wrapper, so it cannot forge a confirm. No such record within the grace →
|
|
272
|
+
// false-FAIL (the sender retries) — NOT a false-OK (silent loss the contract forbids).
|
|
259
273
|
const confirm = seam.confirmLanded ?? ((payload: string) => transcriptCarriesEnvelope(baseline, payload))
|
|
260
274
|
const sleep = seam.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)))
|
|
261
|
-
const graceMs = confirmGraceMs(
|
|
275
|
+
const graceMs = confirmGraceMs()
|
|
262
276
|
const graceDeadline = monotonicMs() + graceMs
|
|
263
277
|
while (true) {
|
|
264
278
|
if (confirm(envelope)) return ok(undefined)
|