@agfpd/iapeer 0.2.88 → 0.2.90
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/launch/ptyHost.ts +10 -1
- package/src/shadow/render.ts +5 -58
- package/src/supervisor/daemon.ts +45 -35
- package/src/supervisor/paths.ts +45 -0
- package/src/transport/index.ts +3 -4
package/package.json
CHANGED
package/src/launch/ptyHost.ts
CHANGED
|
@@ -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
|
package/src/shadow/render.ts
CHANGED
|
@@ -29,64 +29,11 @@ export function modelToPlainText(term: Terminal, cols: number, rows: number): st
|
|
|
29
29
|
return lines.join('\n')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* rows as `line\r\n` into a fresh terminal reproduces native scrollback with NO duplication. Replaying
|
|
38
|
-
* the child's RAW frame bytes can't do this (relative cursor-up can't reach scrolled-off lines on a
|
|
39
|
-
* linear dump → overlap), and SerializeAddon re-emits escapes that the no-scroll-region redraws turn
|
|
40
|
-
* into duplicate scrollback. Trailing blank rows are dropped so a fresh/short session emits no wall of
|
|
41
|
-
* blanks. SGR colour is a deliberate follow-up — plain text first kills the mush without colour as a
|
|
42
|
-
* second source of truth.
|
|
43
|
-
*/
|
|
44
|
-
export function modelAttachSnapshotRows(term: Terminal, cols: number, rows: number): string[] {
|
|
45
|
-
const b = term.buffer.active
|
|
46
|
-
const out: string[] = []
|
|
47
|
-
const end = b.baseY + rows
|
|
48
|
-
for (let y = 0; y < end; y++) {
|
|
49
|
-
const l = b.getLine(y)
|
|
50
|
-
out.push(l ? l.translateToString(true, 0, cols) : '')
|
|
51
|
-
}
|
|
52
|
-
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop()
|
|
53
|
-
return out
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface XtermModeData {
|
|
57
|
-
applicationCursorKeysMode?: boolean
|
|
58
|
-
applicationKeypadMode?: boolean
|
|
59
|
-
bracketedPasteMode?: boolean
|
|
60
|
-
sendFocusMode?: boolean
|
|
61
|
-
mouseTrackingMode?: string
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Terminal MODES restore prefix for the attach snapshot. A plain-text snapshot loses the private
|
|
66
|
-
* modes the child set (bracketed paste `?2004h`, application cursor keys `?1h`, focus `?1004h`,
|
|
67
|
-
* keypad, mouse) — the RAW-byte replay restored them only INCIDENTALLY because those set-sequences
|
|
68
|
-
* sat inline in the bytes. Without them, after attach paste / arrow keys can behave unlike a native
|
|
69
|
-
* run. We re-assert them explicitly from the model's tracked mode state (proposed API; guarded).
|
|
70
|
-
* Ends by showing the cursor.
|
|
71
|
-
*/
|
|
72
|
-
export function terminalModesPrefix(term: Terminal): string {
|
|
73
|
-
let out = ''
|
|
74
|
-
try {
|
|
75
|
-
const m = (term as unknown as { modes?: XtermModeData }).modes
|
|
76
|
-
if (m) {
|
|
77
|
-
if (m.applicationCursorKeysMode) out += '\x1b[?1h'
|
|
78
|
-
if (m.applicationKeypadMode) out += '\x1b='
|
|
79
|
-
if (m.bracketedPasteMode) out += '\x1b[?2004h'
|
|
80
|
-
if (m.sendFocusMode) out += '\x1b[?1004h'
|
|
81
|
-
const mouse: Record<string, string> = { x10: '?9h', vt200: '?1000h', drag: '?1002h', any: '?1003h' }
|
|
82
|
-
const seq = m.mouseTrackingMode ? mouse[m.mouseTrackingMode] : undefined
|
|
83
|
-
if (seq) out += '\x1b[' + seq
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
/* modes is proposed API — absent → no restore, snapshot still renders */
|
|
87
|
-
}
|
|
88
|
-
return out + '\x1b[?25h'
|
|
89
|
-
}
|
|
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.
|
|
90
37
|
|
|
91
38
|
const promptGlyph = (rt: string): string => (rt === 'claude' ? '❯' : '›')
|
|
92
39
|
|
package/src/supervisor/daemon.ts
CHANGED
|
@@ -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 {
|
|
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,21 +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
|
|
307
|
-
//
|
|
308
|
-
// resuming live-forward (single-threaded) → no gap,
|
|
309
|
-
// owns the
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
sendClient(s, frame(FRAME_DATA, Buffer.from(body, 'utf8'))) // UTF-8: rows carry real glyphs (❯, box chars)
|
|
319
|
-
}
|
|
320
|
-
initializing.delete(s as object) // snapshot sent → resume live forwarding for this client
|
|
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
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
// Resize the model + child pty to (c, r). Shared by LATER client resizes and the last-detach restore.
|
|
@@ -335,44 +341,47 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
335
341
|
} catch {
|
|
336
342
|
/* transient */
|
|
337
343
|
}
|
|
344
|
+
writeGeometry(runDir, session, c, r) // record current child geometry → sidecar for warm-deliver / ready-gate readers
|
|
338
345
|
}
|
|
339
|
-
// FIRST attach for a client. Order
|
|
340
|
-
// 1. DRAIN to stability — pending child bytes were generated under the OLD
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
// 3. SNAPSHOT —
|
|
345
|
-
// 4. CHILD resize
|
|
346
|
-
//
|
|
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.
|
|
347
355
|
const firstAttach = async (s: { write(b: Buffer): number }, c: number, r: number): Promise<void> => {
|
|
348
356
|
await awaitModelParsedStable()
|
|
349
357
|
const changed = c !== cols || r !== rows
|
|
350
358
|
if (changed) {
|
|
351
359
|
try {
|
|
352
|
-
xterm.resize(c, r) // MODEL only —
|
|
360
|
+
xterm.resize(c, r) // MODEL only (for the serialize round-trip) — child resized below, after the snapshot
|
|
353
361
|
} catch {
|
|
354
362
|
/* transient */
|
|
355
363
|
}
|
|
356
364
|
cols = c
|
|
357
365
|
rows = r
|
|
358
366
|
}
|
|
359
|
-
sendSnapshot(s) //
|
|
360
|
-
// CHILD resize only when the size actually CHANGED. A reattach at the SAME geometry must NOT SIGWINCH the
|
|
361
|
-
// child — a gratuitous SIGWINCH makes claude re-render on-screen content, which (if taller than the
|
|
362
|
-
// viewport) scrolls a DUPLICATE copy into the model. Same-size reattach = pure snapshot, zero pollution.
|
|
367
|
+
sendSnapshot(s) // serialize the model → send → lift gate (synchronous, no interleave)
|
|
363
368
|
if (changed) {
|
|
364
369
|
try {
|
|
365
|
-
child.terminal.resize(c, r) // SIGWINCH
|
|
370
|
+
child.terminal.resize(c, r) // honest one-SIGWINCH resize (post-snapshot, forwarded live), like native
|
|
366
371
|
} catch {
|
|
367
372
|
/* transient */
|
|
368
373
|
}
|
|
374
|
+
writeGeometry(runDir, session, c, r) // record the new current child geometry for the warm-deliver readers
|
|
369
375
|
}
|
|
370
376
|
}
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
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).
|
|
374
383
|
const restoreServeGeometryIfIdle = (): void => {
|
|
375
|
-
|
|
384
|
+
/* intentional no-op: detach must NOT resize the child — see comment above */
|
|
376
385
|
}
|
|
377
386
|
|
|
378
387
|
const onClientFrame = (s: { write(b: Buffer): number }, type: number, payload: Buffer): void => {
|
|
@@ -429,6 +438,7 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
429
438
|
},
|
|
430
439
|
})
|
|
431
440
|
writeFileSync(pid, String(process.pid))
|
|
441
|
+
writeGeometry(runDir, session, serveCols, serveRows) // initial geometry sidecar (= serve); updated on every real child resize
|
|
432
442
|
|
|
433
443
|
let down = false
|
|
434
444
|
function shutdown(): void {
|
|
@@ -450,7 +460,7 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
|
|
|
450
460
|
} catch {
|
|
451
461
|
/* */
|
|
452
462
|
}
|
|
453
|
-
for (const f of [sock, pid, servePath(runDir, session), attachedMarker]) {
|
|
463
|
+
for (const f of [sock, pid, servePath(runDir, session), attachedMarker, geometryPath(runDir, session)]) {
|
|
454
464
|
try {
|
|
455
465
|
unlinkSync(f)
|
|
456
466
|
} catch {
|
package/src/supervisor/paths.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/transport/index.ts
CHANGED
|
@@ -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`,
|
|
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`,
|
|
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. */
|