@agfpd/iapeer 0.2.83 → 0.2.85

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.83",
3
+ "version": "0.2.85",
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",
@@ -163,6 +163,13 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
163
163
  const clients = new Set<unknown>()
164
164
  const writers = new WeakMap<object, BackpressureWriter>()
165
165
  const framers = new WeakMap<object, (chunk: Uint8Array) => void>()
166
+ // FIRST-PAINT TRACKING (Bug B fix): a client gets the full scrollback catch-up EXACTLY ONCE — on its
167
+ // first FRAME_RESIZE (which runClient always sends on connect, at the real terminal size). Re-dumping
168
+ // on a later resize STACKS duplicate histories in a terminal that ignores \x1b[3J (Apple Terminal has
169
+ // NO E3 capability) → smeared/overlapping scroll-up history. After the catch-up the supervisor does
170
+ // NOTHING on resize — the child redraws its viewport (SIGWINCH) and the terminal reflows its own
171
+ // scrollback, like a directly-launched claude.
172
+ const painted = new WeakSet<object>()
166
173
  // ATTACHED-CLIENT MARKER (spawn-flip Ф0b-3 slice 3c): keep `<runDir>/<session>.attached` in sync with
167
174
  // clients.size — present iff ≥1 operator is attached. The warm-deliver path (a DIFFERENT process)
168
175
  // reads it to gate the hosted busy-composer hold: hold ONLY when a human is attached (their unfinished
@@ -247,8 +254,14 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
247
254
  } as unknown as Parameters<typeof Bun.spawn>[1]) as unknown as PtyChild
248
255
  xterm.onData(resp => child.terminal.write(resp)) // @xterm auto-answers DA1/DSR → back to child
249
256
 
250
- /** Repaint a (re)attaching client from the headless snapshot: clear+home, then the full grid.
251
- * Backpressure-safe — a ~90 KB heavy snapshot MUST NOT drop past the socket buffer (port-dep #2). */
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. */
252
265
  const repaint = (s: { write(b: Buffer): number }): void => {
253
266
  const snap = '\x1b[2J\x1b[3J\x1b[H' + stripSpinnerScrollback(ser.serialize(), xterm.rows)
254
267
  sendClient(s, frame(FRAME_DATA, Buffer.from(snap, 'utf8')))
@@ -287,7 +300,13 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
287
300
  } else if (type === FRAME_RESIZE) {
288
301
  const sz = parseSize(payload)
289
302
  applyGeometry(sz.cols, sz.rows)
290
- repaint(s) // instant paint from the snapshot (delivered COMPLETE via backpressure)
303
+ if (!painted.has(s as object)) {
304
+ painted.add(s as object)
305
+ repaint(s) // FIRST resize for this client = the one-time catch-up (full snapshot at its real size)
306
+ }
307
+ // LATER resizes: nothing from the supervisor. applyGeometry already SIGWINCH'd the child → it
308
+ // redraws its own viewport (forwarded live) and the terminal reflows its own scrollback, exactly
309
+ // like a directly-launched claude. Re-dumping here would stack history in a no-\x1b[3J terminal.
291
310
  }
292
311
  }
293
312
 
@@ -303,7 +322,11 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
303
322
  framers.set(s as object, makeFramer((t, p) => onClientFrame(s, t, p)))
304
323
  clients.add(s)
305
324
  syncAttached() // ≥1 client now attached → marker present
306
- repaint(s) // UNCONDITIONAL repaint on attach (tmux-like)
325
+ // NO repaint here. A viewer's runClient sends its real terminal size as the FIRST FRAME_RESIZE on
326
+ // connect (client.ts open → "repaint at our size"); we paint THEN, ONCE, at the correct geometry.
327
+ // Painting here at the serve geometry (e.g. 220×50) would send a second, wrong-width dump that
328
+ // STACKS in a terminal ignoring \x1b[3J (Apple Terminal) → smeared scroll-up history (Bug B). A
329
+ // deliver client (deliverToHost) never resizes and ignores output → it gets no wasted paint.
307
330
  },
308
331
  data(s: object, d: Uint8Array) {
309
332
  framers.get(s)?.(d)