@agfpd/iapeer 0.4.32 → 0.4.33
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 +4 -22
- package/src/launch/adapters/transcriptTail.ts +67 -0
- package/src/launch/ptyHost.ts +1 -1
- package/src/lifecycle/index.ts +8 -1
- package/src/lifecycle/queue.ts +29 -2
- package/src/storage/rotatelog.ts +0 -0
- package/src/supervisor/index.ts +16 -6
- package/src/transport/index.ts +42 -24
package/package.json
CHANGED
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
|
|
38
38
|
import { homedir } from 'os'
|
|
39
39
|
import { join } from 'path'
|
|
40
|
-
import {
|
|
41
|
-
import { lastTimestampedEntryMs } from './transcriptTail.ts'
|
|
40
|
+
import { readdirSync, realpathSync, statSync } from 'fs'
|
|
41
|
+
import { codexSessionCwd, lastTimestampedEntryMs } from './transcriptTail.ts'
|
|
42
42
|
import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from '../types.ts'
|
|
43
43
|
|
|
44
44
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -75,26 +75,8 @@ function codexHooksReviewActive(pane: string): boolean {
|
|
|
75
75
|
// Session activity proxy + resume preflight (watcher / spawner port)
|
|
76
76
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* `session_meta.payload.cwd` (watcher.codexSessionCwd, 200-212). null when the
|
|
81
|
-
* file is missing, unreadable, not a session_meta record, or carries no cwd.
|
|
82
|
-
*/
|
|
83
|
-
function codexSessionCwd(file: string): string | null {
|
|
84
|
-
try {
|
|
85
|
-
const firstLine = readFileSync(file, 'utf8').split(/\r?\n/, 1)[0]
|
|
86
|
-
if (!firstLine) return null
|
|
87
|
-
const entry = JSON.parse(firstLine) as {
|
|
88
|
-
type?: unknown
|
|
89
|
-
payload?: { cwd?: unknown }
|
|
90
|
-
}
|
|
91
|
-
return entry.type === 'session_meta' && typeof entry.payload?.cwd === 'string'
|
|
92
|
-
? entry.payload.cwd
|
|
93
|
-
: null
|
|
94
|
-
} catch {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
97
|
-
}
|
|
78
|
+
// codexSessionCwd (bounded first-line read + path-memoized) is shared from transcriptTail.ts —
|
|
79
|
+
// the old full-file readFileSync here read tens of MB per session file on every hot-path scan.
|
|
98
80
|
|
|
99
81
|
/** realpath a path so a symlinked cwd compares equal to the dir codex recorded;
|
|
100
82
|
* a stale/missing path falls through to the original string (the canonicalPath
|
|
@@ -30,6 +30,73 @@ import { openSync, readSync, closeSync, statSync } from 'fs'
|
|
|
30
30
|
* the newline-bounded segments after it. Reaching byte 0 makes every segment complete, including the
|
|
31
31
|
* first — and terminates the growth.
|
|
32
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* The FIRST newline-terminated line of a file, read with an ADAPTIVE growing window — NEVER the whole
|
|
35
|
+
* file. A codex `session_meta` first line carries the full instruction payload (measured 29–73 KB on the
|
|
36
|
+
* live host), so a fixed small chunk would miss the terminating `\n` and return a truncated line; the
|
|
37
|
+
* window doubles (capped at file size) until a `\n` is seen or the whole file is read. Returns null when
|
|
38
|
+
* the file is unreadable/empty. Cheap for the common case (one read covers it), bounded otherwise.
|
|
39
|
+
*/
|
|
40
|
+
export function readFirstLine(path: string, chunkBytes = 65536): string | null {
|
|
41
|
+
let fd: number
|
|
42
|
+
try {
|
|
43
|
+
fd = openSync(path, 'r')
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const size = statSync(path).size
|
|
49
|
+
if (size <= 0) return null
|
|
50
|
+
let window = Math.max(chunkBytes, 1)
|
|
51
|
+
for (;;) {
|
|
52
|
+
const len = Math.min(window, size)
|
|
53
|
+
const buf = Buffer.allocUnsafe(len)
|
|
54
|
+
let read = 0
|
|
55
|
+
while (read < len) {
|
|
56
|
+
const n = readSync(fd, buf, read, len - read, read)
|
|
57
|
+
if (n <= 0) break
|
|
58
|
+
read += n
|
|
59
|
+
}
|
|
60
|
+
const text = buf.subarray(0, read).toString('utf8')
|
|
61
|
+
const nl = text.indexOf('\n')
|
|
62
|
+
if (nl >= 0) return text.slice(0, nl).replace(/\r$/, '')
|
|
63
|
+
if (len >= size) return text.replace(/\r$/, '') // whole file, no trailing newline → the single line
|
|
64
|
+
window *= 2
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
} finally {
|
|
69
|
+
closeSync(fd)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Memoized path→cwd. session_meta is written ONCE at session start (immutable) and session-file paths
|
|
74
|
+
* are unique, so caching a DEFINITIVE parse is sound. Only the unreadable/empty case (readFirstLine
|
|
75
|
+
* null) is NOT cached — that can become readable. */
|
|
76
|
+
const codexCwdCache = new Map<string, string | null>()
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The cwd a codex session was opened in — the `session_meta.payload.cwd` of the FIRST jsonl line — read
|
|
80
|
+
* via the bounded readFirstLine and memoized by path. Replaces the old full-file readFileSync that ran
|
|
81
|
+
* per-file on every hot-path scan (superviseTick, ready-gate, delivery baseline). Returns null when the
|
|
82
|
+
* file is missing/unreadable or carries no session_meta cwd.
|
|
83
|
+
*/
|
|
84
|
+
export function codexSessionCwd(file: string): string | null {
|
|
85
|
+
const cached = codexCwdCache.get(file)
|
|
86
|
+
if (cached !== undefined) return cached
|
|
87
|
+
const firstLine = readFirstLine(file)
|
|
88
|
+
if (firstLine === null) return null // unreadable/empty — do NOT cache (may become readable)
|
|
89
|
+
let cwd: string | null = null
|
|
90
|
+
try {
|
|
91
|
+
const entry = JSON.parse(firstLine) as { type?: unknown; payload?: { cwd?: unknown } }
|
|
92
|
+
if (entry.type === 'session_meta' && typeof entry.payload?.cwd === 'string') cwd = entry.payload.cwd
|
|
93
|
+
} catch {
|
|
94
|
+
cwd = null
|
|
95
|
+
}
|
|
96
|
+
codexCwdCache.set(file, cwd)
|
|
97
|
+
return cwd
|
|
98
|
+
}
|
|
99
|
+
|
|
33
100
|
export function lastTimestampedEntryMs(path: string, tailBytes = 65536): number | null {
|
|
34
101
|
let fd: number
|
|
35
102
|
try {
|
package/src/launch/ptyHost.ts
CHANGED
|
@@ -121,7 +121,7 @@ export async function startPtyHost(b: {
|
|
|
121
121
|
}): Promise<HostStartResult> {
|
|
122
122
|
try {
|
|
123
123
|
const { startSupervisorDaemon } = await import('../supervisor/index.ts')
|
|
124
|
-
const r = startSupervisorDaemon({
|
|
124
|
+
const r = await startSupervisorDaemon({
|
|
125
125
|
session: b.identity,
|
|
126
126
|
runtime: b.runtime,
|
|
127
127
|
runDir: hostRunDir(),
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -1445,7 +1445,14 @@ export async function drainEphemeralQueue(
|
|
|
1445
1445
|
)
|
|
1446
1446
|
const wake = deps.wakeFn ?? wakeOrSpawn
|
|
1447
1447
|
const result = await wake({ personality, runtime, topic: item.topic, task: item.task }, { cfg, env })
|
|
1448
|
-
|
|
1448
|
+
// Remove ONLY when THIS wake actually delivered the task. On the live-session fast
|
|
1449
|
+
// path (READY, taskDelivered:false) a CONCURRENT wake delivered ITS OWN task — ours
|
|
1450
|
+
// was NOT delivered. Deleting it here would silently drop a queued task (the loss the
|
|
1451
|
+
// contract forbids); leaving it at the head lets the next drain (after that session
|
|
1452
|
+
// reaps) deliver it into a fresh session — invariant "one task = one fresh session".
|
|
1453
|
+
if (result.status === 'READY' && result.taskDelivered !== false) {
|
|
1454
|
+
removeEphemeralTask(cfg, identity, item.seq)
|
|
1455
|
+
}
|
|
1449
1456
|
return result
|
|
1450
1457
|
}
|
|
1451
1458
|
|
package/src/lifecycle/queue.ts
CHANGED
|
@@ -43,6 +43,30 @@ export function ephemeralQueueDir(cfg: LifecycleConfig, identity: string): strin
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const SEQ_PAD = 6
|
|
46
|
+
const HWM_FILE = '.hwm'
|
|
47
|
+
|
|
48
|
+
/** High-water mark of seqs ever ISSUED for this queue — the monotonic floor that
|
|
49
|
+
* makes names never repeat, even after the queue drains empty (`max(existing)+1`
|
|
50
|
+
* alone resets to 1 once empty, so a concurrent drain still holding the OLD seq
|
|
51
|
+
* could delete a fresh task that got the reused name). 0 when absent/unreadable. */
|
|
52
|
+
function readHwm(dir: string): number {
|
|
53
|
+
try {
|
|
54
|
+
const n = parseInt(readFileSync(join(dir, HWM_FILE), 'utf8'), 10)
|
|
55
|
+
return Number.isFinite(n) && n >= 0 ? n : 0
|
|
56
|
+
} catch {
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Raise the high-water mark (monotonic; only ever rises). Best-effort — a lost
|
|
62
|
+
* bump merely risks a reuse after a FULL drain, the same rare window we harden. */
|
|
63
|
+
function bumpHwm(dir: string, seq: number): void {
|
|
64
|
+
try {
|
|
65
|
+
if (seq > readHwm(dir)) writeFileSync(join(dir, HWM_FILE), String(seq), { mode: 0o600 })
|
|
66
|
+
} catch {
|
|
67
|
+
/* best-effort */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
46
70
|
|
|
47
71
|
function listSeqs(dir: string): string[] {
|
|
48
72
|
let entries: string[]
|
|
@@ -79,16 +103,19 @@ export function enqueueEphemeralTask(
|
|
|
79
103
|
mode: 0o600,
|
|
80
104
|
})
|
|
81
105
|
try {
|
|
82
|
-
// Next seq = max
|
|
106
|
+
// Next seq = max(existing items, ever-issued high-water) + 1 — MONOTONIC: never
|
|
107
|
+
// reuse a name after the queue empties. On an EEXIST race, advance and retry.
|
|
83
108
|
let seq = (() => {
|
|
84
109
|
const seqs = listSeqs(dir)
|
|
85
|
-
|
|
110
|
+
const maxExisting = seqs.length ? parseInt(seqs[seqs.length - 1]!, 10) : 0
|
|
111
|
+
return Math.max(maxExisting, readHwm(dir)) + 1
|
|
86
112
|
})()
|
|
87
113
|
// Bounded retry: a competitor can win a name at most once per its own enqueue.
|
|
88
114
|
for (let attempt = 0; attempt < 1000; attempt++, seq++) {
|
|
89
115
|
const target = join(dir, String(seq).padStart(SEQ_PAD, '0'))
|
|
90
116
|
try {
|
|
91
117
|
linkSync(tmp, target) // atomic exclusive-create (EEXIST when taken)
|
|
118
|
+
bumpHwm(dir, seq) // record the issued name so it is never reused
|
|
92
119
|
return listSeqs(dir).length
|
|
93
120
|
} catch (e) {
|
|
94
121
|
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e
|
package/src/storage/rotatelog.ts
CHANGED
|
Binary file
|
package/src/supervisor/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// serving is productionized (a later block) the daemon-spawn argv is repointed at the installed
|
|
6
6
|
// binary; for now it self-spawns from source (bun + this file), which is what the tests exercise.
|
|
7
7
|
import { spawn as nodeSpawn } from 'node:child_process'
|
|
8
|
-
import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
import { runSupervisorClient, sessionAlive } from './client.ts'
|
|
11
11
|
import { defaultRunDir, logPath, pidPath, servePath, sockPath } from './paths.ts'
|
|
@@ -86,8 +86,10 @@ export function resolveDaemonSelfArgv(execPath: string, selfPath: string): strin
|
|
|
86
86
|
}
|
|
87
87
|
const selfArgv = (): string[] => resolveDaemonSelfArgv(process.execPath, fileURLToPath(import.meta.url))
|
|
88
88
|
|
|
89
|
-
/** Start a detached supervisor daemon and wait until its socket is live. Idempotent.
|
|
90
|
-
|
|
89
|
+
/** Start a detached supervisor daemon and wait until its socket is live. Idempotent.
|
|
90
|
+
* Async: the socket-wait yields the event loop (a sync busy-wait here froze the whole
|
|
91
|
+
* router daemon for the bring-up window on every warm wake). */
|
|
92
|
+
export async function startSupervisorDaemon(opts: StartDaemonOptions): Promise<StartDaemonResult> {
|
|
91
93
|
const { session, runtime, runDir } = opts
|
|
92
94
|
if (sessionAlive(runDir, session)) return { state: 'already-running' }
|
|
93
95
|
mkdirSync(runDir, { recursive: true })
|
|
@@ -111,13 +113,21 @@ export function startSupervisorDaemon(opts: StartDaemonOptions): StartDaemonResu
|
|
|
111
113
|
detached: true,
|
|
112
114
|
stdio: ['ignore', out, out],
|
|
113
115
|
})
|
|
116
|
+
// The child dup'd `out` at spawn — close the PARENT copy immediately. This function
|
|
117
|
+
// runs inside the always-on router daemon on EVERY warm wake; an unclosed fd here bled
|
|
118
|
+
// the daemon ~1 fd/wake toward EMFILE (live-observed) → eventual full routing outage.
|
|
119
|
+
try {
|
|
120
|
+
closeSync(out)
|
|
121
|
+
} catch {
|
|
122
|
+
/* */
|
|
123
|
+
}
|
|
114
124
|
child.unref()
|
|
115
125
|
const deadline = Date.now() + (opts.timeoutMs ?? 5000)
|
|
116
126
|
while (Date.now() < deadline) {
|
|
117
127
|
if (existsSync(sockPath(runDir, session)) && sessionAlive(runDir, session)) {
|
|
118
128
|
return { state: 'started', pid: Number(readFileSync(pidPath(runDir, session), 'utf8')) }
|
|
119
129
|
}
|
|
120
|
-
Bun.
|
|
130
|
+
await Bun.sleep(80) // yield the loop — never a sync block in the daemon's wake path
|
|
121
131
|
}
|
|
122
132
|
// Failed to come up — drop the serve-spec we wrote so it can't mislead a later read.
|
|
123
133
|
if (opts.serve)
|
|
@@ -192,7 +202,7 @@ export async function runSupervisorCli(argv: string[]): Promise<number> {
|
|
|
192
202
|
return 0 // unreachable
|
|
193
203
|
}
|
|
194
204
|
case 'start': {
|
|
195
|
-
const r = startSupervisorDaemon({ session: a!, runtime: b || 'claude', runDir })
|
|
205
|
+
const r = await startSupervisorDaemon({ session: a!, runtime: b || 'claude', runDir })
|
|
196
206
|
console.log(`${r.state}${r.pid ? ` pid=${r.pid}` : ''}${r.detail ? ` — ${r.detail}` : ''}: ${a}`)
|
|
197
207
|
return r.state === 'failed' ? 1 : 0
|
|
198
208
|
}
|
|
@@ -200,7 +210,7 @@ export async function runSupervisorCli(argv: string[]): Promise<number> {
|
|
|
200
210
|
await runSupervisorClient(runDir, a!)
|
|
201
211
|
return 0
|
|
202
212
|
case 'up': {
|
|
203
|
-
const r = startSupervisorDaemon({ session: a!, runtime: b || 'claude', runDir })
|
|
213
|
+
const r = await startSupervisorDaemon({ session: a!, runtime: b || 'claude', runDir })
|
|
204
214
|
if (r.state === 'failed') {
|
|
205
215
|
console.error(r.detail)
|
|
206
216
|
return 1
|
package/src/transport/index.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// runtime's adapter.deliveryMarkers. The poll/re-press logic is untouched; only the
|
|
14
14
|
// marker SOURCE moved, so the timing behaviour is identical.
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { closeSync, fstatSync, openSync, readSync, readdirSync, realpathSync, statSync } from 'fs'
|
|
17
17
|
import { join } from 'path'
|
|
18
18
|
import { homedir } from 'os'
|
|
19
19
|
import {
|
|
@@ -40,6 +40,7 @@ import type { ResolvedCaller } from '../identity/index.ts'
|
|
|
40
40
|
// reads them from getAdapter(target.runtime) for the tui submit path. One-way
|
|
41
41
|
// dependency: launch does NOT import transport, so no cycle.
|
|
42
42
|
import { getAdapter } from '../launch/index.ts'
|
|
43
|
+
import { codexSessionCwd } from '../launch/adapters/transcriptTail.ts'
|
|
43
44
|
import type { ControlCommand } from '../launch/types.ts'
|
|
44
45
|
// Spawn-flip Ф0b-2: warm-deliver is HOST-AWARE. hostSessionAlive (runtime-state host detect, an
|
|
45
46
|
// @xterm-free pid-file check) + deliverHosted (the pure-protocol socket leaf, Ф0a) come from
|
|
@@ -234,7 +235,11 @@ async function deliverViaHost(
|
|
|
234
235
|
// Baseline the transcript/session-jsonl file SIZES BEFORE delivery (per-runtime candidate files).
|
|
235
236
|
// The landed-confirm reads ONLY the bytes appended past this baseline, so a record carrying our
|
|
236
237
|
// envelope can only be one written in RESPONSE to this delivery — never a pre-existing copy.
|
|
237
|
-
|
|
238
|
+
// LAZY: only 'transcript'-confirm runtimes (claude) use the baseline; router (socket-ack) and
|
|
239
|
+
// socket-ack runtimes (codex) return before it — computing it for them recursively scanned all of
|
|
240
|
+
// ~/.codex/sessions (full-file read per session) on every delivery for nothing.
|
|
241
|
+
const usesBaseline = adapter.kind !== 'router' && adapter.deliveryConfirm !== 'socket-ack'
|
|
242
|
+
const baseline = cwd && usesBaseline ? compactDoneBaseline(target.runtime, cwd) : null
|
|
238
243
|
const send = seam.deliverHosted ?? deliverHosted
|
|
239
244
|
const sent = await send(target.address, envelope)
|
|
240
245
|
if (!sent.ok) {
|
|
@@ -374,14 +379,36 @@ function claudeTranscriptDirForCwd(cwd: string, env?: NodeJS.ProcessEnv): string
|
|
|
374
379
|
return join(envHome(env), '.claude', 'projects', slug)
|
|
375
380
|
}
|
|
376
381
|
|
|
377
|
-
|
|
382
|
+
// codexSessionCwd (bounded first-line read + path-memoized) is shared from launch/adapters/transcriptTail.ts —
|
|
383
|
+
// the old full-file readFileSync here scanned tens of MB per session file on every warm delivery baseline.
|
|
384
|
+
|
|
385
|
+
/** Read ONLY the bytes of `path` past `offset` (the appended tail), without loading the whole file.
|
|
386
|
+
* O(added bytes) instead of O(entire history) — the transcript files this scans grow to hundreds of MB.
|
|
387
|
+
* Returns '' on any fs error / empty tail. */
|
|
388
|
+
function readFileTailFrom(path: string, offset: number): string {
|
|
389
|
+
let fd: number
|
|
378
390
|
try {
|
|
379
|
-
|
|
380
|
-
if (!firstLine) return null
|
|
381
|
-
const entry = JSON.parse(firstLine) as { type?: unknown; payload?: { cwd?: unknown } }
|
|
382
|
-
return entry.type === 'session_meta' && typeof entry.payload?.cwd === 'string' ? entry.payload.cwd : null
|
|
391
|
+
fd = openSync(path, 'r')
|
|
383
392
|
} catch {
|
|
384
|
-
return
|
|
393
|
+
return ''
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const size = fstatSync(fd).size
|
|
397
|
+
const start = Math.min(Math.max(0, offset), size)
|
|
398
|
+
const len = size - start
|
|
399
|
+
if (len <= 0) return ''
|
|
400
|
+
const buf = Buffer.allocUnsafe(len)
|
|
401
|
+
let read = 0
|
|
402
|
+
while (read < len) {
|
|
403
|
+
const n = readSync(fd, buf, read, len - read, start + read)
|
|
404
|
+
if (n <= 0) break
|
|
405
|
+
read += n
|
|
406
|
+
}
|
|
407
|
+
return buf.subarray(0, read).toString('utf8')
|
|
408
|
+
} catch {
|
|
409
|
+
return ''
|
|
410
|
+
} finally {
|
|
411
|
+
closeSync(fd)
|
|
385
412
|
}
|
|
386
413
|
}
|
|
387
414
|
|
|
@@ -478,14 +505,8 @@ export function compactTranscriptHasDone(
|
|
|
478
505
|
}
|
|
479
506
|
|
|
480
507
|
for (const [path, offset] of offsets) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
buf = readFileSync(path)
|
|
484
|
-
} catch {
|
|
485
|
-
continue
|
|
486
|
-
}
|
|
487
|
-
const start = Math.min(Math.max(0, offset), buf.length)
|
|
488
|
-
const tail = buf.subarray(start).toString('utf8')
|
|
508
|
+
const tail = readFileTailFrom(path, offset) // tail-only read (see transcriptCarriesEnvelope)
|
|
509
|
+
if (!tail) continue
|
|
489
510
|
for (const line of tail.split(/\r?\n/)) {
|
|
490
511
|
if (!line.trim()) continue
|
|
491
512
|
let obj: unknown
|
|
@@ -537,14 +558,11 @@ export function transcriptCarriesEnvelope(baseline: CompactDoneBaseline, envelop
|
|
|
537
558
|
const needle = envelope.replace(/\r/g, '')
|
|
538
559
|
if (!needle) return false
|
|
539
560
|
for (const { path, size: offset } of baseline.files) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
const start = Math.min(Math.max(0, offset), buf.length)
|
|
547
|
-
const tail = buf.subarray(start).toString('utf8')
|
|
561
|
+
// Read ONLY the tail past the baseline offset — a full readFileSync here re-read the ENTIRE
|
|
562
|
+
// transcript (hundreds of MB for a long-running peer) on every ~100ms confirm-poll, stalling the
|
|
563
|
+
// single-threaded router event loop for the whole fleet.
|
|
564
|
+
const tail = readFileTailFrom(path, offset)
|
|
565
|
+
if (!tail) continue
|
|
548
566
|
for (const line of tail.split(/\r?\n/)) {
|
|
549
567
|
if (!line.trim()) continue
|
|
550
568
|
let obj: unknown
|