@agfpd/iapeer 0.2.85 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.85",
3
+ "version": "0.2.86",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -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 + @xterm/addon-serialize (the latter is the repaint source: a reattaching
9
- // client gets the full current screen even if the child is silent). Both bundle under
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)
@@ -170,6 +168,21 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
170
168
  // NOTHING on resize — the child redraws its viewport (SIGWINCH) and the terminal reflows its own
171
169
  // scrollback, like a directly-launched claude.
172
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
+ }
173
186
  // ATTACHED-CLIENT MARKER (spawn-flip Ф0b-3 slice 3c): keep `<runDir>/<session>.attached` in sync with
174
187
  // clients.size — present iff ≥1 operator is attached. The warm-deliver path (a DIFFERENT process)
175
188
  // reads it to gate the hosted busy-composer hold: hold ONLY when a human is attached (their unfinished
@@ -226,7 +239,10 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
226
239
  xterm.write(buf) // authoritative model
227
240
  for (const r of capabilityResponses(s)) child.terminal.write(r) // answer capability queries
228
241
  const fwd = Buffer.from(stripQueries(s), 'latin1') // forward to clients sans queries (daemon is authoritative)
229
- if (fwd.length) for (const c of [...clients]) sendClient(c as { write(b: Buffer): number }, frame(FRAME_DATA, fwd))
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
+ }
230
246
  },
231
247
  exit(_t?: unknown, exitCode?: number | null, signalCode?: number | null) {
232
248
  // resilience-b exit-cause (spawn-flip Ф0b): record WHY the hosted child died into exits.log,
@@ -254,17 +270,22 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
254
270
  } as unknown as Parameters<typeof Bun.spawn>[1]) as unknown as PtyChild
255
271
  xterm.onData(resp => child.terminal.write(resp)) // @xterm auto-answers DA1/DSR → back to child
256
272
 
257
- /** Catch up a (re)attaching client to the current screen + history ONCE: clear+home, then the full
258
- * grid from the headless snapshot. Backpressure-safe — a ~90 KB heavy snapshot MUST NOT drop past the
259
- * socket buffer (port-dep #2). Called EXACTLY ONCE per client (its first FRAME_RESIZE). It is NEVER
260
- * re-issued on a later resize: re-dumping stacks a second history copy in a terminal that ignores
261
- * \x1b[3J (Apple Terminal has no E3) → smeared scroll-up history (Bug B). After the first catch-up the
262
- * supervisor stops intervening: a resize just re-sizes the child (applyGeometry → SIGWINCH), and the
263
- * child redraws its own viewport while the terminal reflows its own scrollback — exactly like a
264
- * directly-launched claude. */
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. */
265
286
  const repaint = (s: { write(b: Buffer): number }): void => {
266
- const snap = '\x1b[2J\x1b[3J\x1b[H' + stripSpinnerScrollback(ser.serialize(), xterm.rows)
267
- sendClient(s, frame(FRAME_DATA, Buffer.from(snap, 'utf8')))
287
+ if (rawRingBytes === 0) return
288
+ sendClient(s, frame(FRAME_DATA, Buffer.concat([Buffer.from('\x1b[0m', 'latin1'), ...rawRing])))
268
289
  }
269
290
 
270
291
  // Resize the model + child pty to (c, r). Shared by client FRAME_RESIZE and the last-detach restore.