@agfpd/iapeer 0.4.107 → 0.4.109

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.4.107",
3
+ "version": "0.4.109",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
package/src/cli/index.ts CHANGED
@@ -1102,8 +1102,19 @@ async function connectChannelVerb(
1102
1102
  ): Promise<number> {
1103
1103
  // A channel whose runtime package was never installed would provision a plist whose
1104
1104
  // launcher does not resolve — provisionPeer refuses, but late and obscurely. Say it here.
1105
- const { isRuntimeInstalled } = await import('../init/index.ts')
1106
- if (!isRuntimeInstalled(runtime as Runtime, env)) {
1105
+ //
1106
+ // NB: `isRuntimeInstalled` is the AGENTIC probe and is WRONG for this question — it ends
1107
+ // at `Bun.which(runtime)`, while an infra launcher is named `<runtime>-runtime`
1108
+ // (INFRA_RUNTIME_DEFAULT_BIN). It reported a fully installed voicetalk as missing and
1109
+ // blocked the very path this verb exists for (caught live on the first real run).
1110
+ // The installed-ness of a runtime PACKAGE is its self-deployed manifest — the contract's
1111
+ // own install stamp ("on self-install the package writes ~/.iapeer/runtimes/<rt>/runtime.json;
1112
+ // the core reads it", docs/13). Deliberately NOT a PATH probe for the launcher: that would
1113
+ // read host state a temp-rooted run cannot control, and whether the launcher RESOLVES is
1114
+ // already checked downstream by provisionPeer, which refuses with its own clear error
1115
+ // rather than writing a plist that crash-loops.
1116
+ const { runtimeManifestPath } = await import('../runtime/index.ts')
1117
+ if (!existsSync(runtimeManifestPath(runtime as Runtime, { env }))) {
1107
1118
  errOut(`connect ${runtime} ${peer}: runtime "${runtime}" is not installed on this host — \`iapeer install-runtime ${runtime}\` first\n`)
1108
1119
  return 1
1109
1120
  }
@@ -3,7 +3,7 @@
3
3
  // port-deps live here + in protocol.ts: #1 TERM_RESET on EVERY exit path (incl. SIGHUP window-close,
4
4
  // via writeSync which works inside a signal handler), and #2 backpressure is the daemon's side.
5
5
  import { writeSync } from 'node:fs'
6
- import { FRAME_DATA, FRAME_RESIZE, TERM_RESET, frame, makeFramer, sizePayload } from './protocol.ts'
6
+ import { FRAME_DATA, FRAME_RESIZE, TERM_ATTACH_RESET, TERM_RESET, frame, makeFramer, sizePayload } from './protocol.ts'
7
7
  import { ownershipVerdict, pidStartToken, readPidFile, sockPath } from './paths.ts'
8
8
 
9
9
  const DETACH = 0x1d // Ctrl-]
@@ -81,6 +81,17 @@ export async function runSupervisorClient(runDir: string, session: string): Prom
81
81
  // Ready the terminal BEFORE connecting, so the repaint snapshot renders the instant it arrives.
82
82
  if (process.stdin.isTTY) process.stdin.setRawMode(true)
83
83
  process.stdin.resume()
84
+ // The daemon owns the emulated session/model; THIS client owns the physical operator viewport.
85
+ // Normalize that viewport before connecting so its one-time snapshot cannot be appended below a
86
+ // shell prompt or a prior attach's differently-wrapped frame. HOME+ED0 is intentional: unlike
87
+ // ED2/ED3 it does not trigger Apple Terminal's clear-to-scrollback trap (protocol.ts).
88
+ if (process.stdout.isTTY) {
89
+ try {
90
+ writeSync(1, TERM_ATTACH_RESET)
91
+ } catch {
92
+ /* a failed cosmetic reset must not block attach */
93
+ }
94
+ }
84
95
 
85
96
  const curSize = (): Buffer => sizePayload(process.stdout.columns || 80, process.stdout.rows || 24)
86
97
  const sock = await Bun.connect({
@@ -142,9 +153,9 @@ export async function runSupervisorClient(runDir: string, session: string): Prom
142
153
  })
143
154
  }
144
155
  process.on('exit', () => restore()) // last-resort sync net
145
- // NO pre-repaint attach banner: any stdout (even stderr — same tty device) printed before the daemon's
146
- // snapshot lands pollutes the fresh terminal's scrollback, defeating native-history reconstruction. The
147
- // detach key (Ctrl-]) is documented in CLI help / the attach verb, not emitted as a scrollback line.
156
+ // NO pre-repaint attach banner: after TERM_ATTACH_RESET, the daemon snapshot must be the first CONTENT
157
+ // painted into the normalized viewport. The detach key (Ctrl-]) is documented in CLI help / the attach
158
+ // verb, not emitted as a scrollback line.
148
159
  // STAY ATTACHED. The client lives on its handles (raw stdin + the socket); EVERY terminal path
149
160
  // (detach Ctrl-], session-end close(), SIGHUP/SIGTERM, socket error) calls process.exit ITSELF.
150
161
  // This promise INTENTIONALLY never resolves: returning here would resolve runSupervisorClient the
@@ -399,8 +399,9 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
399
399
  * Scrollback rows are spinner-stripped; the viewport stays verbatim. The child's SIGWINCH redraw (from
400
400
  * applyGeometry) then settles the live viewport at the client's width. Backpressure-safe
401
401
  * (BackpressureWriter) — a large snapshot MUST NOT drop past the socket buffer (port-dep #2). Called
402
- * EXACTLY ONCE per client (its first FRAME_RESIZE). NO clears (\x1b[2J/3J): a fresh window needs none,
403
- * and Apple Terminal scrolls 2J into scrollback + ignores 3J. */
402
+ * EXACTLY ONCE per client (its first FRAME_RESIZE). The daemon/model sends NO clears
403
+ * (\x1b[2J/3J): Apple Terminal scrolls 2J into scrollback + ignores 3J. The attach client owns its
404
+ * physical viewport and normalizes it once with HOME+ED0 before connecting. */
404
405
  // Await until the model has parsed ALL writes AND none arrived during the wait (stable). Awaiting a single
405
406
  // write promise is not enough: while the client is gated (initializing, live-forward skipped) new child
406
407
  // bytes still land in the model and bump writeSeq — loop until the sequence holds steady so the snapshot
@@ -164,6 +164,18 @@ export function keysToBytes(keys: string[], enc: KeyEncoding = {}): Buffer {
164
164
  return Buffer.from(out, 'utf8')
165
165
  }
166
166
 
167
+ // ── client terminal normalization ────────────────────────────────────────────────
168
+ // A (re)attach can start in an already-used Terminal.app normal buffer: the shell prompt and a prior
169
+ // attach's differently-wrapped frame still occupy the physical viewport. The daemon's serialized model
170
+ // deliberately carries NO clear (it is shared protocol data, while the stale viewport belongs to each
171
+ // client), so the client must home + erase the CURRENT display before the one-time snapshot arrives.
172
+ //
173
+ // Use ED0 from HOME, NOT ED2/ED3. Apple Terminal implements neither of the latter as the primitive we
174
+ // need: ED2 pushes the old display into native scrollback and ED3 is ignored, so repeated attaches stack
175
+ // broken-width dumps in history. HOME+ED0 clears the viewport in place; the daemon still sends exactly
176
+ // one model snapshot and later SIGWINCH resizes remain raw/live (no repeated dump).
177
+ export const TERM_ATTACH_RESET = '\x1b[H\x1b[J'
178
+
167
179
  // ── port-dep #1: terminal reset on every client exit path ───────────────────────
168
180
  // The child sets a STACK of app-modes on the live terminal (alt-screen, bracketed-paste,
169
181
  // focus-events, theme-update, ALL mouse tracking, kitty-keyboard, scroll-region, hidden cursor).