@agfpd/iapeer 0.2.89 → 0.2.91

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.89",
3
+ "version": "0.2.91",
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",
@@ -12,7 +12,7 @@
12
12
  // - readiness reuses paneLogViewport, whose @xterm import is dynamic and only loads flag-on.
13
13
  import { existsSync, readFileSync } from 'node:fs'
14
14
  import { deliverToHost, type DeliverResult } from '../supervisor/deliver.ts'
15
- import { attachedPath, defaultRunDir, pidPath } from '../supervisor/paths.ts'
15
+ import { attachedPath, defaultRunDir, pidPath, readGeometry } from '../supervisor/paths.ts'
16
16
  import { sessionAlive as hostDaemonAlive } from '../supervisor/client.ts'
17
17
  import { killSession as killSupervisorSession, listSessions as listSupervisorSessions } from '../supervisor/index.ts'
18
18
  import { parseSessionName } from '../core/socket.ts'
@@ -72,6 +72,15 @@ export function hasAttachedSupervisorClient(identity: string): boolean {
72
72
  return existsSync(attachedPath(hostRunDir(), identity))
73
73
  }
74
74
 
75
+ /** Current child pty geometry of a hosted session, from the supervisor's `<session>.geometry.json`
76
+ * sidecar, or HOST defaults when absent/unreadable. Warm-deliver occupancy / ready-gate readers use THIS
77
+ * rather than assuming the fixed HOST geometry, because a DETACHED session keeps its last client geometry
78
+ * (the child is no longer reverted to serve on detach — that revert caused the reattach-duplicate bug).
79
+ * Reading the actual geometry keeps the pane-log composer-occupancy model aligned with the real pty size. */
80
+ export function hostGeometry(identity: string): { cols: number; rows: number } {
81
+ return readGeometry(hostRunDir(), identity) ?? { cols: HOST_COLS, rows: HOST_ROWS }
82
+ }
83
+
75
84
  /**
76
85
  * A session-REPLACEMENT token for a hosted session (spawn-flip Ф0b-3 slice 3c): the supervisor
77
86
  * daemon's pid. A `/new` or restart spawns a NEW daemon (new pid), so a message queued against the old
@@ -29,61 +29,11 @@ export function modelToPlainText(term: Terminal, cols: number, rows: number): st
29
29
  return lines.join('\n')
30
30
  }
31
31
 
32
- /**
33
- * ATTACH HISTORY rows — the model's SCROLLBACK ONLY (rows `0..baseY`, the committed history ABOVE the
34
- * live viewport), as physical rows of plain text. The LIVE viewport (`baseY..`) is DELIBERATELY EXCLUDED:
35
- * claude paints the current screen itself (its live render, forwarded right after attach), so including
36
- * the viewport here would DOUBLE-PAINT it — the snapshot's copy collides with claude's live repaint →
37
- * smeared overlap (the recurring bug). So on attach we ONLY prepend the history; claude shows the current
38
- * screen. The @xterm model has already flattened claude's relative-cursor redraws into committed lines, so
39
- * these rows render as native scrollback with no duplication. Trailing blank rows are dropped. SGR colour
40
- * is a deliberate follow-up — plain text first.
41
- */
42
- export function modelAttachSnapshotRows(term: Terminal, cols: number): string[] {
43
- const b = term.buffer.active
44
- const out: string[] = []
45
- for (let y = 0; y < b.baseY; y++) {
46
- const l = b.getLine(y)
47
- out.push(l ? l.translateToString(true, 0, cols) : '')
48
- }
49
- while (out.length > 0 && out[out.length - 1].trim() === '') out.pop()
50
- return out
51
- }
52
-
53
- interface XtermModeData {
54
- applicationCursorKeysMode?: boolean
55
- applicationKeypadMode?: boolean
56
- bracketedPasteMode?: boolean
57
- sendFocusMode?: boolean
58
- mouseTrackingMode?: string
59
- }
60
-
61
- /**
62
- * Terminal MODES restore prefix for the attach snapshot. A plain-text snapshot loses the private
63
- * modes the child set (bracketed paste `?2004h`, application cursor keys `?1h`, focus `?1004h`,
64
- * keypad, mouse) — the RAW-byte replay restored them only INCIDENTALLY because those set-sequences
65
- * sat inline in the bytes. Without them, after attach paste / arrow keys can behave unlike a native
66
- * run. We re-assert them explicitly from the model's tracked mode state (proposed API; guarded).
67
- * Ends by showing the cursor.
68
- */
69
- export function terminalModesPrefix(term: Terminal): string {
70
- let out = ''
71
- try {
72
- const m = (term as unknown as { modes?: XtermModeData }).modes
73
- if (m) {
74
- if (m.applicationCursorKeysMode) out += '\x1b[?1h'
75
- if (m.applicationKeypadMode) out += '\x1b='
76
- if (m.bracketedPasteMode) out += '\x1b[?2004h'
77
- if (m.sendFocusMode) out += '\x1b[?1004h'
78
- const mouse: Record<string, string> = { x10: '?9h', vt200: '?1000h', drag: '?1002h', any: '?1003h' }
79
- const seq = m.mouseTrackingMode ? mouse[m.mouseTrackingMode] : undefined
80
- if (seq) out += '\x1b[' + seq
81
- }
82
- } catch {
83
- /* modes is proposed API — absent → no restore, snapshot still renders */
84
- }
85
- return out + '\x1b[?25h'
86
- }
32
+ // NOTE: the attach catch-up SNAPSHOT is built in the supervisor daemon via SerializeAddon.serialize()
33
+ // (scrollback + screen + cursor + SGR colour + private modes, all at once), NOT from this leaf. Earlier
34
+ // plain-text attempts here (modelAttachSnapshotRows / terminalModesPrefix) were removed: translateToString
35
+ // is text-only (→ monochrome) and a hand-rolled rows dump can't reproduce the grid+cursor the way the
36
+ // emulate-then-rerender multiplexers (tmux/zellij) do. See the daemon's sendSnapshot.
87
37
 
88
38
  const promptGlyph = (rt: string): string => (rt === 'claude' ? '❯' : '›')
89
39
 
@@ -110,7 +60,14 @@ export function composerOccupancyFromModel(term: Terminal, cols: number, rows: n
110
60
  const l = b.getLine(py)!
111
61
  for (let x = px + 1; x < cols; x++) {
112
62
  const c = l.getCell(x); if ((c?.getChars() || ' ').trim() === '') continue
113
- if (!(c!.isDim() || (c!.isFgPalette() && c!.getFgColor() === 246))) return true
63
+ // OCCUPIED iff a real human glyph after the prompt: DEFAULT foreground AND non-dim. Verified on BOTH
64
+ // runtimes — typed input renders at the terminal default fg (isFgDefault, fg=-1) and non-dim. Everything
65
+ // else after the prompt is runtime chrome / ghost and must be ignored: claude's dim/palette-246
66
+ // placeholder; codex's DIM default-fg suggestion ("Find and fix a bug in @filename", varies) AND its
67
+ // TRUECOLOR status hint ("<model> <effort> · <cwd>"). The old check (`!dim && !palette-246`) ghosted the
68
+ // dim suggestion but MISSED codex's truecolor status (non-dim, non-palette) → false OCCUPIED → spurious
69
+ // delivery queue ("queuedBy: composer") whenever a human was attached to a codex session.
70
+ if (!c!.isDim() && c!.isFgDefault()) return true
114
71
  }
115
72
  return false
116
73
  }
@@ -11,6 +11,7 @@
11
11
  // dump, both of which corrupt scrollback for a relative-cursor, no-scroll-region TUI like claude.
12
12
  // @xterm/headless bundles under `bun build --compile` (pure JS; the Bun-native pty has no native addon).
13
13
  import { Terminal } from '@xterm/headless'
14
+ import { SerializeAddon } from '@xterm/addon-serialize'
14
15
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, unlinkSync, writeFileSync, writeSync } from 'node:fs'
15
16
  import { dirname } from 'node:path'
16
17
  import {
@@ -23,9 +24,9 @@ import {
23
24
  parseSize,
24
25
  stripQueries,
25
26
  } from './protocol.ts'
26
- import { attachedPath, pidPath, servePath, sockPath } from './paths.ts'
27
+ import { attachedPath, geometryPath, pidPath, servePath, sockPath, writeGeometry } from './paths.ts'
27
28
  import { nextBootAction, type BootPredicates } from './boot.ts'
28
- import { modelAttachSnapshotRows, modelToPlainText, terminalModesPrefix } from '../shadow/render.ts'
29
+ import { modelToPlainText } from '../shadow/render.ts'
29
30
  import { codexAdapter } from '../launch/adapters/codex.ts'
30
31
  import { claudeAdapter } from '../launch/adapters/claude.ts'
31
32
 
@@ -132,6 +133,12 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
132
133
  const pid = pidPath(runDir, session)
133
134
 
134
135
  const xterm = new Terminal({ cols, rows, allowProposedApi: true, scrollback: 5000 })
136
+ // SerializeAddon turns the resolved model (scrollback + screen + cursor + SGR colour + private modes)
137
+ // into a byte string that recreates it in a fresh terminal — the attach catch-up. This is how tmux /
138
+ // screen / zellij restore state on attach (emulate the VT, re-render on attach). Color comes free here
139
+ // (translateToString was text-only → the monochrome bug); serialize() carries SGR (xterm PR #2369).
140
+ const serializeAddon = new SerializeAddon()
141
+ xterm.loadAddon(serializeAddon)
135
142
 
136
143
  const bin = opts.bin ?? knownLauncher(runtime) ?? runtime
137
144
  // SERVING SEAM (slice b): opts.argv (launch-composed, carries the system-prompt swap + PEER_START_ARGS)
@@ -303,22 +310,20 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
303
310
  if (seq === writeSeq) return
304
311
  }
305
312
  }
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
+ // Build + send the one-time attach SNAPSHOT (full resolved model: scrollback + screen + cursor + SGR colour
314
+ // + private modes) via SerializeAddon, and LIFT the initializing gate — SYNCHRONOUS (no await), so the live
315
+ // data() handler cannot interleave between serializing and resuming live-forward (single-threaded) → no gap,
316
+ // no double-forward. Called only from firstAttach, which owns the order (drain → model-resize-to-client →
317
+ // THIS → child-resize-if-changed). serialize() recreates the EXACT grid incl. cursor, so claude's live bytes
318
+ // continue seamlessly from the matching state — no separate viewport repaint, no double-paint overlap. The
319
+ // model is clean because the child is NEVER shrunk on detach (the resize-churn that polluted scrollback is
320
+ // gone), so serialize faithfully reports clean coloured history. Trailing \x1b[m guards the wrap bg-bleed
321
+ // artifact (xterm.js #3102). This is the tmux/zellij "re-render the emulated model on attach" architecture.
313
322
  const sendSnapshot = (s: { write(b: Buffer): number }): void => {
314
- const historyRows = modelAttachSnapshotRows(xterm, cols) // SCROLLBACK only — claude paints the viewport live
315
- const cleaned = stripSpinnerScrollback(historyRows.join('\r\n'), 0) // strip spinner remnants from ALL of history
316
- // Prepend the committed HISTORY above the screen claude is about to repaint, after a modes-restore prefix.
317
- // NO viewport rows — that is claude's job (forwarded live), which avoids the snapshot↔live double-paint
318
- // overlap. Always send at least the modes prefix (restores bracketed-paste / app-cursor for a fresh session).
319
- const body = '\x1b[0m' + terminalModesPrefix(xterm) + (cleaned ? cleaned + '\r\n' : '')
320
- sendClient(s, frame(FRAME_DATA, Buffer.from(body, 'utf8'))) // UTF-8: rows carry real glyphs (❯, box chars)
321
- initializing.delete(s as object) // history sent → resume live forwarding (claude's viewport repaint)
323
+ const snap = serializeAddon.serialize({ scrollback: xterm.buffer.active.baseY }) // all scrollback + screen, with colour
324
+ const body = snap.length > 0 ? snap + '\x1b[m' : ''
325
+ if (body) sendClient(s, frame(FRAME_DATA, Buffer.from(body, 'utf8')))
326
+ initializing.delete(s as object) // snapshot sent → resume live forwarding
322
327
  }
323
328
 
324
329
  // Resize the model + child pty to (c, r). Shared by LATER client resizes and the last-detach restore.
@@ -336,44 +341,47 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
336
341
  } catch {
337
342
  /* transient */
338
343
  }
344
+ writeGeometry(runDir, session, c, r) // record current child geometry → sidecar for warm-deliver / ready-gate readers
339
345
  }
340
- // FIRST attach for a client. Order matters for a clean frame0 (codex review):
341
- // 1. DRAIN to stability — pending child bytes were generated under the OLD pty geometry; parsing them
342
- // AFTER a model resize would rewrap them wrong, and the drain also captures bytes that land while the
343
- // client is gated, so they make the snapshot rather than being lost.
344
- // 2. MODEL resize to the client geometry (so the snapshot wraps at the client's width).
345
- // 3. SNAPSHOT — build + send + lift gate, SYNCHRONOUS (no await), so no live byte interleaves.
346
- // 4. CHILD resize → SIGWINCH → claude's live reflow, forwarded AFTER the snapshot (post-gate).
347
- // Resizing the child FIRST would SIGWINCH claude into a reflow whose bytes pollute frame0.
346
+ // FIRST attach for a client. Order (codex review) for a clean, coloured, correctly-sized snapshot:
347
+ // 1. DRAIN to stability — pending child bytes were generated under the OLD geometry; parse them BEFORE a
348
+ // model resize (else they rewrap wrong), and capture bytes that land while the client is gated.
349
+ // 2. MODEL resize to the client geometry IF it differs — serialize() round-trips only at matching dims
350
+ // (xterm.js #3093), so the model must be the client's size before we serialize.
351
+ // 3. SNAPSHOT — serialize + send + lift gate, SYNCHRONOUS (no await), so no live byte interleaves.
352
+ // 4. CHILD resize IF it differs — an honest one-SIGWINCH window-resize (like native). NOT a nudge, NOT a
353
+ // forced repaint: serialize already recreated screen+cursor, so claude's live bytes continue from the
354
+ // matching grid. A same-size reattach touches the child ZERO times → pure snapshot, no re-render.
348
355
  const firstAttach = async (s: { write(b: Buffer): number }, c: number, r: number): Promise<void> => {
349
356
  await awaitModelParsedStable()
350
357
  const changed = c !== cols || r !== rows
351
358
  if (changed) {
352
359
  try {
353
- xterm.resize(c, r) // MODEL only — not the child yet
360
+ xterm.resize(c, r) // MODEL only (for the serialize round-trip) — child resized below, after the snapshot
354
361
  } catch {
355
362
  /* transient */
356
363
  }
357
364
  cols = c
358
365
  rows = r
359
366
  }
360
- sendSnapshot(s) // prepend the scrollback HISTORY (no viewport) + lift gate — synchronous, no interleave
361
- // Force claude to repaint its CURRENT viewport BELOW the prepended history. A SIGWINCH triggers a full
362
- // redraw; the (r-1)→r nudge guarantees it fires even on a SAME-size reattach (else an idle claude would
363
- // leave the viewport blank until its next frame). Safe now: the snapshot carries NO viewport, so claude's
364
- // repaint has nothing to collide with — the smeared-overlap class is gone at its source.
365
- try {
366
- child.terminal.resize(c, Math.max(1, r - 1))
367
- child.terminal.resize(c, r)
368
- } catch {
369
- /* transient */
367
+ sendSnapshot(s) // serialize the model → send → lift gate (synchronous, no interleave)
368
+ if (changed) {
369
+ try {
370
+ child.terminal.resize(c, r) // honest one-SIGWINCH resize (post-snapshot, forwarded live), like native
371
+ } catch {
372
+ /* transient */
373
+ }
374
+ writeGeometry(runDir, session, c, r) // record the new current child geometry for the warm-deliver readers
370
375
  }
371
376
  }
372
- // Restore the serve geometry when the last client detaches, so the unattached session never sticks at
373
- // a departed client's size (FINDING-2 — would mismatch the fixed-geometry warm-deliver/ready-gate
374
- // viewport model). No-op when already at serve geometry or a client is still attached.
377
+ // DETACH = NO RESIZE (emulator invariant). A detached hosted TUI keeps its LAST client geometry — it is
378
+ // NEVER shrunk back to serve. The old FINDING-2 revert shrank the child into a tiny viewport, making claude
379
+ // reflow its live region and scroll DUPLICATE copies into scrollback — the root of the reattach-dup bug.
380
+ // tmux/screen likewise never collapse a detached session's size. Warm-deliver / ready-gate readers follow
381
+ // the actual geometry via the `<session>.geometry.json` sidecar instead of assuming serve. Kept as a no-op
382
+ // (callers in the close/drain/error handlers stay unchanged).
375
383
  const restoreServeGeometryIfIdle = (): void => {
376
- if (clients.size === 0 && (cols !== serveCols || rows !== serveRows)) applyGeometry(serveCols, serveRows)
384
+ /* intentional no-op: detach must NOT resize the child — see comment above */
377
385
  }
378
386
 
379
387
  const onClientFrame = (s: { write(b: Buffer): number }, type: number, payload: Buffer): void => {
@@ -430,6 +438,7 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
430
438
  },
431
439
  })
432
440
  writeFileSync(pid, String(process.pid))
441
+ writeGeometry(runDir, session, serveCols, serveRows) // initial geometry sidecar (= serve); updated on every real child resize
433
442
 
434
443
  let down = false
435
444
  function shutdown(): void {
@@ -451,7 +460,7 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
451
460
  } catch {
452
461
  /* */
453
462
  }
454
- for (const f of [sock, pid, servePath(runDir, session), attachedMarker]) {
463
+ for (const f of [sock, pid, servePath(runDir, session), attachedMarker, geometryPath(runDir, session)]) {
455
464
  try {
456
465
  unlinkSync(f)
457
466
  } catch {
@@ -1,6 +1,8 @@
1
1
  // Supervisor session paths — under a configurable run dir (NOT hardcoded). Prod resolves it to
2
2
  // ~/.iapeer/state/supervisor via the storage layer; tests pass a temp dir, so the daemon's
3
3
  // socket/pid files never touch a real location.
4
+ import { renameSync, unlinkSync, writeFileSync } from 'node:fs'
5
+ import { readFileSync } from 'node:fs'
4
6
  import { join } from 'node:path'
5
7
  import { pluginStateDir } from '../storage/index.ts'
6
8
 
@@ -21,3 +23,46 @@ export const servePath = (runDir: string, session: string): string => join(runDi
21
23
  * reads it to gate the hosted busy-composer hold — never paste over a HUMAN's unfinished input, but
22
24
  * never hold for the AI's own composer output when no operator is attached. */
23
25
  export const attachedPath = (runDir: string, session: string): string => join(runDir, `${session}.attached`)
26
+
27
+ /** Geometry sidecar (emulator-model fix): the CURRENT child pty geometry of a hosted session. The
28
+ * supervisor (the only process that resizes the child) writes it after every successful resize; the
29
+ * warm-deliver / ready-gate readers in OTHER processes read it to build their pane-log viewport model at
30
+ * the session's actual size instead of assuming the fixed HOST geometry. Critical because a detached
31
+ * session is NO LONGER reverted to serve geometry (that revert caused claude to reflow into a tiny
32
+ * viewport → duplicate scrollback) — so the child can sit at the last client's size, and readers must
33
+ * follow it. Absent / unreadable → readers fall back to HOST geometry. */
34
+ export const geometryPath = (runDir: string, session: string): string => join(runDir, `${session}.geometry.json`)
35
+
36
+ /** Atomically (temp + rename) record the current child geometry — best-effort, never throws (a hint file). */
37
+ export function writeGeometry(runDir: string, session: string, cols: number, rows: number): void {
38
+ const p = geometryPath(runDir, session)
39
+ try {
40
+ const tmp = `${p}.${process.pid}.tmp`
41
+ writeFileSync(tmp, JSON.stringify({ cols, rows }), { mode: 0o600 })
42
+ renameSync(tmp, p)
43
+ } catch {
44
+ /* best-effort: a geometry hint never blocks serving */
45
+ }
46
+ }
47
+
48
+ /** Read the current child geometry of a hosted session; null when absent / unreadable / invalid. */
49
+ export function readGeometry(runDir: string, session: string): { cols: number; rows: number } | null {
50
+ try {
51
+ const o = JSON.parse(readFileSync(geometryPath(runDir, session), 'utf8')) as { cols?: unknown; rows?: unknown }
52
+ if (typeof o.cols === 'number' && typeof o.rows === 'number' && o.cols > 0 && o.rows > 0) {
53
+ return { cols: o.cols, rows: o.rows }
54
+ }
55
+ } catch {
56
+ /* absent / malformed → caller falls back to HOST */
57
+ }
58
+ return null
59
+ }
60
+
61
+ /** Remove the geometry sidecar (shutdown cleanup); best-effort. */
62
+ export function clearGeometry(runDir: string, session: string): void {
63
+ try {
64
+ unlinkSync(geometryPath(runDir, session))
65
+ } catch {
66
+ /* */
67
+ }
68
+ }
@@ -51,12 +51,11 @@ import type { ControlCommand, DeliveryMarkers } from '../launch/types.ts'
51
51
  import {
52
52
  deliverHosted,
53
53
  hasAttachedSupervisorClient,
54
+ hostGeometry,
54
55
  hostRunDir,
55
56
  hostSessionAlive,
56
57
  hostSessionToken,
57
58
  listHostedPeers,
58
- HOST_COLS,
59
- HOST_ROWS,
60
59
  } from '../launch/ptyHost.ts'
61
60
  import { sendControlToHost, type DeliverResult } from '../supervisor/deliver.ts'
62
61
  import { keysToBytes } from '../supervisor/protocol.ts' // pure send-keys-vocab→pty-byte translation (boot-driver slice a), @xterm-free
@@ -1158,7 +1157,7 @@ async function shouldQueueForComposer(target: DeliveryTarget, logDir: string | u
1158
1157
  if (!hostSessionAlive(target.address)) return shouldQueueForHumanComposer(target)
1159
1158
  if (!isSupportedLocalRuntime(target.runtime) || !hasAttachedSupervisorClient(target.address)) return false
1160
1159
  if (!logDir) return false // no pane-log dir → cannot read occupancy → don't hold (deliver, never stall)
1161
- return paneLogComposerOccupied(`${logDir}/${target.address}.log`, HOST_COLS, HOST_ROWS, target.runtime)
1160
+ return paneLogComposerOccupied(`${logDir}/${target.address}.log`, hostGeometry(target.address).cols, hostGeometry(target.address).rows, target.runtime)
1162
1161
  }
1163
1162
 
1164
1163
  /** Drain re-check (no attached-client prereq — once queued, abandoned input waits until cleared/ceiling).
@@ -1166,7 +1165,7 @@ async function shouldQueueForComposer(target: DeliveryTarget, logDir: string | u
1166
1165
  async function composerStillBusy(target: DeliveryTarget, logDir: string | undefined): Promise<boolean> {
1167
1166
  if (!hostSessionAlive(target.address)) return targetComposerBlocksDelivery(target)
1168
1167
  if (!logDir) return false
1169
- return paneLogComposerOccupied(`${logDir}/${target.address}.log`, HOST_COLS, HOST_ROWS, target.runtime)
1168
+ return paneLogComposerOccupied(`${logDir}/${target.address}.log`, hostGeometry(target.address).cols, hostGeometry(target.address).rows, target.runtime)
1170
1169
  }
1171
1170
 
1172
1171
  /** Session-replacement token. HOSTED: supervisor daemon pid; tmux: session_id:pane_id. */