@agfpd/iapeer 0.2.87 → 0.2.88
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/supervisor/daemon.ts +67 -23
package/package.json
CHANGED
package/src/supervisor/daemon.ts
CHANGED
|
@@ -176,9 +176,11 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
176
176
|
// client leaves this set and live forwarding resumes — no gap, no duplication.
|
|
177
177
|
const initializing = new WeakSet<object>()
|
|
178
178
|
// MODEL-WRITE PARSE BARRIER: @xterm `write` is async (parsed on an internal queue). The attach snapshot
|
|
179
|
-
// must be built only AFTER in-flight writes have parsed
|
|
180
|
-
//
|
|
179
|
+
// must be built only AFTER in-flight writes have parsed AND none arrived during the wait, else it reflects
|
|
180
|
+
// a stale screen / drops bytes that landed while the client was gated. `writeChain` tracks the latest
|
|
181
|
+
// write's parse-callback; `writeSeq` bumps on every write so the drain can detect new arrivals mid-await.
|
|
181
182
|
let writeChain: Promise<void> = Promise.resolve()
|
|
183
|
+
let writeSeq = 0
|
|
182
184
|
// ATTACHED-CLIENT MARKER (spawn-flip Ф0b-3 slice 3c): keep `<runDir>/<session>.attached` in sync with
|
|
183
185
|
// clients.size — present iff ≥1 operator is attached. The warm-deliver path (a DIFFERENT process)
|
|
184
186
|
// reads it to gate the hosted busy-composer hold: hold ONLY when a human is attached (their unfinished
|
|
@@ -232,6 +234,7 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
232
234
|
/* best-effort */
|
|
233
235
|
}
|
|
234
236
|
const s = buf.toString('latin1')
|
|
237
|
+
writeSeq++ // a new write arrived (lets the attach drain detect bytes that land mid-await)
|
|
235
238
|
writeChain = new Promise<void>(resolve => xterm.write(buf, () => resolve())) // authoritative model (+ parse barrier)
|
|
236
239
|
for (const r of capabilityResponses(s)) child.terminal.write(r) // answer capability queries
|
|
237
240
|
const fwd = Buffer.from(stripQueries(s), 'latin1') // forward to clients sans queries (daemon is authoritative)
|
|
@@ -285,14 +288,29 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
285
288
|
* (BackpressureWriter) — a large snapshot MUST NOT drop past the socket buffer (port-dep #2). Called
|
|
286
289
|
* EXACTLY ONCE per client (its first FRAME_RESIZE). NO clears (\x1b[2J/3J): a fresh window needs none,
|
|
287
290
|
* and Apple Terminal scrolls 2J into scrollback + ignores 3J. */
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
// Await until the model has parsed ALL writes AND none arrived during the wait (stable). Awaiting a single
|
|
292
|
+
// write promise is not enough: while the client is gated (initializing, live-forward skipped) new child
|
|
293
|
+
// bytes still land in the model and bump writeSeq — loop until the sequence holds steady so the snapshot
|
|
294
|
+
// reflects every byte received so far (no stale screen, no dropped tail).
|
|
295
|
+
const awaitModelParsedStable = async (): Promise<void> => {
|
|
296
|
+
for (;;) {
|
|
297
|
+
const seq = writeSeq
|
|
298
|
+
try {
|
|
299
|
+
await writeChain
|
|
300
|
+
} catch {
|
|
301
|
+
/* a write parse-callback should not reject */
|
|
302
|
+
}
|
|
303
|
+
if (seq === writeSeq) return
|
|
293
304
|
}
|
|
294
|
-
|
|
295
|
-
|
|
305
|
+
}
|
|
306
|
+
// Build + send the one-time attach SNAPSHOT of the resolved model and LIFT the initializing gate —
|
|
307
|
+
// SYNCHRONOUS (no await), so the live data() handler cannot interleave between building the snapshot and
|
|
308
|
+
// resuming live-forward (single-threaded) → no gap, no double-forward. Called only from firstAttach, which
|
|
309
|
+
// owns the full order (drain → model-resize → THIS → child-resize). It is a SNAPSHOT of the resolved @xterm
|
|
310
|
+
// model (committed scrollback + viewport rows), NOT a raw-frame replay / SerializeAddon — both corrupt
|
|
311
|
+
// scrollback for claude's relative-cursor, no-scroll-region TUI. Scrollback rows are spinner-stripped, the
|
|
312
|
+
// viewport is verbatim. NO clears (\x1b[2J/3J): a fresh window needs none; Apple Terminal mishandles them.
|
|
313
|
+
const sendSnapshot = (s: { write(b: Buffer): number }): void => {
|
|
296
314
|
const rowsArr = modelAttachSnapshotRows(xterm, cols, rows)
|
|
297
315
|
if (rowsArr.length > 0) {
|
|
298
316
|
const cleaned = stripSpinnerScrollback(rowsArr.join('\r\n'), rows) // dechrome scrollback, viewport verbatim
|
|
@@ -302,8 +320,13 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
302
320
|
initializing.delete(s as object) // snapshot sent → resume live forwarding for this client
|
|
303
321
|
}
|
|
304
322
|
|
|
305
|
-
// Resize the model + child pty to (c, r). Shared by client
|
|
323
|
+
// Resize the model + child pty to (c, r). Shared by LATER client resizes and the last-detach restore.
|
|
324
|
+
// NO-OP when already at (c,r): a gratuitous resize SIGWINCHes the child → claude reflows its WHOLE
|
|
325
|
+
// transcript, and when that reflow is taller than the viewport the overflow scrolls into the model's
|
|
326
|
+
// scrollback as a DUPLICATE copy (the scrollback-pollution root). One real resize = one SIGWINCH; the
|
|
327
|
+
// old (r-1)→r "nudge" doubled every SIGWINCH and is removed for the same reason.
|
|
306
328
|
const applyGeometry = (c: number, r: number): void => {
|
|
329
|
+
if (c === cols && r === rows) return // unchanged → no SIGWINCH → no reflow → no scrollback pollution
|
|
307
330
|
cols = c
|
|
308
331
|
rows = r
|
|
309
332
|
try {
|
|
@@ -312,14 +335,37 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
312
335
|
} catch {
|
|
313
336
|
/* transient */
|
|
314
337
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
}
|
|
339
|
+
// FIRST attach for a client. Order matters for a clean frame0 (codex review):
|
|
340
|
+
// 1. DRAIN to stability — pending child bytes were generated under the OLD pty geometry; parsing them
|
|
341
|
+
// AFTER a model resize would rewrap them wrong, and the drain also captures bytes that land while the
|
|
342
|
+
// client is gated, so they make the snapshot rather than being lost.
|
|
343
|
+
// 2. MODEL resize to the client geometry (so the snapshot wraps at the client's width).
|
|
344
|
+
// 3. SNAPSHOT — build + send + lift gate, SYNCHRONOUS (no await), so no live byte interleaves.
|
|
345
|
+
// 4. CHILD resize → SIGWINCH → claude's live reflow, forwarded AFTER the snapshot (post-gate).
|
|
346
|
+
// Resizing the child FIRST would SIGWINCH claude into a reflow whose bytes pollute frame0.
|
|
347
|
+
const firstAttach = async (s: { write(b: Buffer): number }, c: number, r: number): Promise<void> => {
|
|
348
|
+
await awaitModelParsedStable()
|
|
349
|
+
const changed = c !== cols || r !== rows
|
|
350
|
+
if (changed) {
|
|
351
|
+
try {
|
|
352
|
+
xterm.resize(c, r) // MODEL only — not the child yet
|
|
353
|
+
} catch {
|
|
354
|
+
/* transient */
|
|
355
|
+
}
|
|
356
|
+
cols = c
|
|
357
|
+
rows = r
|
|
358
|
+
}
|
|
359
|
+
sendSnapshot(s) // synchronous: build + send + lift gate (no await before the child resize below)
|
|
360
|
+
// CHILD resize only when the size actually CHANGED. A reattach at the SAME geometry must NOT SIGWINCH the
|
|
361
|
+
// child — a gratuitous SIGWINCH makes claude re-render on-screen content, which (if taller than the
|
|
362
|
+
// viewport) scrolls a DUPLICATE copy into the model. Same-size reattach = pure snapshot, zero pollution.
|
|
363
|
+
if (changed) {
|
|
364
|
+
try {
|
|
365
|
+
child.terminal.resize(c, r) // SIGWINCH the child → live reflow (post-snapshot, forwarded live)
|
|
366
|
+
} catch {
|
|
367
|
+
/* transient */
|
|
368
|
+
}
|
|
323
369
|
}
|
|
324
370
|
}
|
|
325
371
|
// Restore the serve geometry when the last client detaches, so the unattached session never sticks at
|
|
@@ -334,14 +380,12 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
334
380
|
child.terminal.write(payload) // keystrokes → child
|
|
335
381
|
} else if (type === FRAME_RESIZE) {
|
|
336
382
|
const sz = parseSize(payload)
|
|
337
|
-
applyGeometry(sz.cols, sz.rows)
|
|
338
383
|
if (!painted.has(s as object)) {
|
|
339
384
|
painted.add(s as object)
|
|
340
|
-
void
|
|
385
|
+
void firstAttach(s, sz.cols, sz.rows) // split-resize: model → snapshot → gate-lift → child (clean frame0)
|
|
386
|
+
} else {
|
|
387
|
+
applyGeometry(sz.cols, sz.rows) // LATER resize: one real resize, no re-snapshot
|
|
341
388
|
}
|
|
342
|
-
// LATER resizes: nothing from the supervisor. applyGeometry already SIGWINCH'd the child → it
|
|
343
|
-
// redraws its own viewport (forwarded live) and the terminal reflows its own scrollback, exactly
|
|
344
|
-
// like a directly-launched claude. Re-dumping here would stack history in a no-\x1b[3J terminal.
|
|
345
389
|
}
|
|
346
390
|
}
|
|
347
391
|
|