@agfpd/iapeer 0.2.84 → 0.2.86
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 +47 -28
package/package.json
CHANGED
package/src/supervisor/daemon.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// detached process, dynamic-imported from the launch path (supervisor/index.ts → daemon.ts), so
|
|
6
6
|
// @xterm never loads in the central router's delivery path nor in the launch/lifecycle process.
|
|
7
7
|
//
|
|
8
|
-
// Loads @xterm/headless
|
|
9
|
-
//
|
|
8
|
+
// Loads @xterm/headless as the authoritative model (boot-driver reads it; capability queries answered
|
|
9
|
+
// off it). The attach catch-up is NOT a model serialize — it REPLAYS the child's recent raw output ring
|
|
10
|
+
// (claude's own bytes → native scrollback, no duplication; see repaint). @xterm/headless bundles under
|
|
10
11
|
// `bun build --compile` (pure JS; the Bun-native pty has no native addon).
|
|
11
12
|
import { Terminal } from '@xterm/headless'
|
|
12
|
-
import { SerializeAddon } from '@xterm/addon-serialize'
|
|
13
13
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, unlinkSync, writeFileSync, writeSync } from 'node:fs'
|
|
14
14
|
import { dirname } from 'node:path'
|
|
15
15
|
import {
|
|
@@ -131,8 +131,6 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
131
131
|
const pid = pidPath(runDir, session)
|
|
132
132
|
|
|
133
133
|
const xterm = new Terminal({ cols, rows, allowProposedApi: true, scrollback: 5000 })
|
|
134
|
-
const ser = new SerializeAddon()
|
|
135
|
-
xterm.loadAddon(ser)
|
|
136
134
|
|
|
137
135
|
const bin = opts.bin ?? knownLauncher(runtime) ?? runtime
|
|
138
136
|
// SERVING SEAM (slice b): opts.argv (launch-composed, carries the system-prompt swap + PEER_START_ARGS)
|
|
@@ -163,12 +161,28 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
163
161
|
const clients = new Set<unknown>()
|
|
164
162
|
const writers = new WeakMap<object, BackpressureWriter>()
|
|
165
163
|
const framers = new WeakMap<object, (chunk: Uint8Array) => void>()
|
|
166
|
-
// FIRST-PAINT TRACKING (Bug B fix): a client gets the full scrollback
|
|
167
|
-
// FRAME_RESIZE (which runClient always sends on connect, at the real terminal size). Re-dumping
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
164
|
+
// FIRST-PAINT TRACKING (Bug B fix): a client gets the full scrollback catch-up EXACTLY ONCE — on its
|
|
165
|
+
// first FRAME_RESIZE (which runClient always sends on connect, at the real terminal size). Re-dumping
|
|
166
|
+
// on a later resize STACKS duplicate histories in a terminal that ignores \x1b[3J (Apple Terminal has
|
|
167
|
+
// NO E3 capability) → smeared/overlapping scroll-up history. After the catch-up the supervisor does
|
|
168
|
+
// NOTHING on resize — the child redraws its viewport (SIGWINCH) and the terminal reflows its own
|
|
169
|
+
// scrollback, like a directly-launched claude.
|
|
171
170
|
const painted = new WeakSet<object>()
|
|
171
|
+
// RAW REPLAY RING (Bug B catch-up): the last ~RAW_RING_MAX bytes of the child's RAW output, exactly as
|
|
172
|
+
// forwarded to live clients. The attach catch-up REPLAYS this verbatim so the client renders claude's
|
|
173
|
+
// OWN bytes — native scrollback, NO duplication. (The previous catch-up dumped the @xterm model via
|
|
174
|
+
// SerializeAddon, which captures claude's no-scroll-region status/tool-list redraws as DUPLICATE
|
|
175
|
+
// scrollback frames; a directly-launched claude has no such duplication — the dup was the model's, not
|
|
176
|
+
// claude's.) No clears in the replay: Apple Terminal scrolls \x1b[2J into scrollback and ignores
|
|
177
|
+
// \x1b[3J, so claude's own print+\n stream is the only clean primitive.
|
|
178
|
+
const RAW_RING_MAX = 1024 * 1024
|
|
179
|
+
const rawRing: Buffer[] = []
|
|
180
|
+
let rawRingBytes = 0
|
|
181
|
+
const ringAppend = (b: Buffer): void => {
|
|
182
|
+
rawRing.push(b)
|
|
183
|
+
rawRingBytes += b.length
|
|
184
|
+
while (rawRingBytes > RAW_RING_MAX && rawRing.length > 1) rawRingBytes -= rawRing.shift()!.length
|
|
185
|
+
}
|
|
172
186
|
// ATTACHED-CLIENT MARKER (spawn-flip Ф0b-3 slice 3c): keep `<runDir>/<session>.attached` in sync with
|
|
173
187
|
// clients.size — present iff ≥1 operator is attached. The warm-deliver path (a DIFFERENT process)
|
|
174
188
|
// reads it to gate the hosted busy-composer hold: hold ONLY when a human is attached (their unfinished
|
|
@@ -225,7 +239,10 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
225
239
|
xterm.write(buf) // authoritative model
|
|
226
240
|
for (const r of capabilityResponses(s)) child.terminal.write(r) // answer capability queries
|
|
227
241
|
const fwd = Buffer.from(stripQueries(s), 'latin1') // forward to clients sans queries (daemon is authoritative)
|
|
228
|
-
if (fwd.length)
|
|
242
|
+
if (fwd.length) {
|
|
243
|
+
ringAppend(fwd) // keep recent raw output for the attach catch-up (raw replay = native, no dup)
|
|
244
|
+
for (const c of [...clients]) sendClient(c as { write(b: Buffer): number }, frame(FRAME_DATA, fwd))
|
|
245
|
+
}
|
|
229
246
|
},
|
|
230
247
|
exit(_t?: unknown, exitCode?: number | null, signalCode?: number | null) {
|
|
231
248
|
// resilience-b exit-cause (spawn-flip Ф0b): record WHY the hosted child died into exits.log,
|
|
@@ -253,21 +270,22 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
253
270
|
} as unknown as Parameters<typeof Bun.spawn>[1]) as unknown as PtyChild
|
|
254
271
|
xterm.onData(resp => child.terminal.write(resp)) // @xterm auto-answers DA1/DSR → back to child
|
|
255
272
|
|
|
256
|
-
/**
|
|
257
|
-
*
|
|
273
|
+
/** Catch up a (re)attaching client to the current screen + history ONCE by REPLAYING the child's recent
|
|
274
|
+
* RAW output (its own bytes from the ring) — so the client renders exactly what a directly-launched
|
|
275
|
+
* claude would, with native scrollback and NO duplication. Backpressure-safe — a ~1 MB replay MUST NOT
|
|
276
|
+
* drop past the socket buffer (port-dep #2). Called EXACTLY ONCE per client (its first FRAME_RESIZE):
|
|
277
|
+
* re-replaying on a later resize would stack a second history copy in a terminal that ignores \x1b[3J
|
|
278
|
+
* (Apple Terminal has no E3) → smeared scroll-up history (Bug B). After the catch-up the supervisor
|
|
279
|
+
* stops intervening: a resize just re-sizes the child (applyGeometry → SIGWINCH), and the child redraws
|
|
280
|
+
* its own viewport while the terminal reflows its own scrollback — exactly like a direct claude launch.
|
|
281
|
+
* NO clears (\x1b[2J/3J): Apple Terminal scrolls 2J into scrollback and ignores 3J — claude's own
|
|
282
|
+
* print+\n stream is the only clean primitive. A leading \x1b[0m resets any inherited SGR before the
|
|
283
|
+
* replay; the child's SIGWINCH redraw (from applyGeometry) then settles the current screen at the
|
|
284
|
+
* client's width. Empty ring (fresh session, no output yet) → nothing to replay; the SIGWINCH redraw
|
|
285
|
+
* paints the first screen. */
|
|
258
286
|
const repaint = (s: { write(b: Buffer): number }): void => {
|
|
259
|
-
|
|
260
|
-
sendClient(s, frame(FRAME_DATA, Buffer.from(
|
|
261
|
-
}
|
|
262
|
-
/** Refresh ONLY the visible viewport (current screen), never the scrollback — for a resize AFTER the
|
|
263
|
-
* first full paint. `\x1b[2J\x1b[H` clears the screen and homes WITHOUT touching scrollback (no
|
|
264
|
-
* \x1b[3J), then we write exactly the viewport rows (no scrollback segments), so nothing scrolls into
|
|
265
|
-
* the client's native scrollback. This is what keeps a mid-session resize from stacking another
|
|
266
|
-
* history copy in a terminal that ignores \x1b[3J (Bug B). */
|
|
267
|
-
const repaintViewport = (s: { write(b: Buffer): number }): void => {
|
|
268
|
-
const lines = ser.serialize().split('\r\n')
|
|
269
|
-
const viewport = lines.slice(Math.max(0, lines.length - xterm.rows)).join('\r\n')
|
|
270
|
-
sendClient(s, frame(FRAME_DATA, Buffer.from('\x1b[2J\x1b[H' + viewport, 'utf8')))
|
|
287
|
+
if (rawRingBytes === 0) return
|
|
288
|
+
sendClient(s, frame(FRAME_DATA, Buffer.concat([Buffer.from('\x1b[0m', 'latin1'), ...rawRing])))
|
|
271
289
|
}
|
|
272
290
|
|
|
273
291
|
// Resize the model + child pty to (c, r). Shared by client FRAME_RESIZE and the last-detach restore.
|
|
@@ -305,10 +323,11 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
305
323
|
applyGeometry(sz.cols, sz.rows)
|
|
306
324
|
if (!painted.has(s as object)) {
|
|
307
325
|
painted.add(s as object)
|
|
308
|
-
repaint(s) // FIRST
|
|
309
|
-
} else {
|
|
310
|
-
repaintViewport(s) // later resize: refresh the screen only — never re-dump scrollback (anti-stack)
|
|
326
|
+
repaint(s) // FIRST resize for this client = the one-time catch-up (full snapshot at its real size)
|
|
311
327
|
}
|
|
328
|
+
// LATER resizes: nothing from the supervisor. applyGeometry already SIGWINCH'd the child → it
|
|
329
|
+
// redraws its own viewport (forwarded live) and the terminal reflows its own scrollback, exactly
|
|
330
|
+
// like a directly-launched claude. Re-dumping here would stack history in a no-\x1b[3J terminal.
|
|
312
331
|
}
|
|
313
332
|
}
|
|
314
333
|
|