@agfpd/iapeer 0.2.63 → 0.2.65

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.
@@ -0,0 +1,102 @@
1
+ // Spawn-flip cutover Block 2, Ф0b-2 — host-aware warm delivery (`deliverWarm`).
2
+ //
3
+ // The warm-deliver path becomes host-aware: a supervisor-HOSTED target is delivered over its socket
4
+ // (Ф0a leaf) and CONFIRMED by the SAME final arbiter as the tmux busy path — a transcript-mtime
5
+ // advance, NOT the socket-ack. These suites cover the host branch hermetically (injected seams: a
6
+ // fake host-detect + host-deliver + a transcript-mtime proxy) so no live supervisor daemon is needed.
7
+ // The flag-off (tmux) routing is asserted by the host seam staying UNTOUCHED — the byte-identical
8
+ // guarantee that lets this ship dark.
9
+ import { afterEach, describe, expect, test } from 'bun:test'
10
+ import { deliverWarm, type DeliveryTarget, type WarmDeliverSeam } from './index.ts'
11
+
12
+ const hostedTarget: DeliveryTarget = {
13
+ runtime: 'codex',
14
+ personality: 'ptyx',
15
+ address: 'codex-ptyx',
16
+ socketPath: '/tmp/nonexistent-iap-test.sock',
17
+ }
18
+
19
+ const savedGrace = process.env.IAP_HOST_LIVENESS_GRACE_MS
20
+ afterEach(() => {
21
+ if (savedGrace === undefined) delete process.env.IAP_HOST_LIVENESS_GRACE_MS
22
+ else process.env.IAP_HOST_LIVENESS_GRACE_MS = savedGrace
23
+ })
24
+
25
+ describe('deliverWarm — flag-off (no live supervisor session) keeps the tmux path', () => {
26
+ test('hostAlive=false → the host-deliver seam is NEVER touched (routes to deliverViaTmux)', async () => {
27
+ let hostedCalls = 0
28
+ const seam: WarmDeliverSeam = {
29
+ hostAlive: () => false,
30
+ deliverHosted: async () => {
31
+ hostedCalls++
32
+ return { ok: true }
33
+ },
34
+ }
35
+ // The tmux path will fail against a bogus socket — but that is incidental; the contract under
36
+ // test is that flag-off NEVER reaches the host branch. We assert the host seam stayed cold.
37
+ await deliverWarm(hostedTarget, 'hi', '/tmp', seam)
38
+ expect(hostedCalls).toBe(0)
39
+ })
40
+ })
41
+
42
+ describe('deliverWarm — hosted target delivers over the socket + confirms by transcript advance', () => {
43
+ test('socket deliver ok AND transcript advances → ok', async () => {
44
+ let mtime = 100
45
+ const seam: WarmDeliverSeam = {
46
+ hostAlive: () => true,
47
+ deliverHosted: async () => {
48
+ mtime = 200 // the session took our message and started a model turn → transcript moved
49
+ return { ok: true }
50
+ },
51
+ newestActivityMtime: () => mtime,
52
+ }
53
+ const r = await deliverWarm(hostedTarget, 'task', '/peer/cwd', seam)
54
+ expect(r.ok).toBe(true)
55
+ })
56
+
57
+ test('socket-ack is NOT landed: deliver ok but NO transcript advance → loud fail (treated as dead)', async () => {
58
+ process.env.IAP_HOST_LIVENESS_GRACE_MS = '0' // expire the HOST grace immediately — deterministic fail
59
+ let hostedCalls = 0
60
+ const seam: WarmDeliverSeam = {
61
+ hostAlive: () => true,
62
+ deliverHosted: async () => {
63
+ hostedCalls++
64
+ return { ok: true } // CR flushed to the socket — but the session never advanced
65
+ },
66
+ newestActivityMtime: () => 100, // constant → never advances past baseline
67
+ }
68
+ const r = await deliverWarm(hostedTarget, 'task', '/peer/cwd', seam)
69
+ expect(hostedCalls).toBe(1)
70
+ expect(r.ok).toBe(false)
71
+ if (!r.ok) expect(r.error.message).toContain('no transcript advance')
72
+ })
73
+
74
+ test('socket deliver fails (socket dead/stalled) → loud fail, no false ok', async () => {
75
+ const seam: WarmDeliverSeam = {
76
+ hostAlive: () => true,
77
+ deliverHosted: async () => ({ ok: false, error: 'socket dead during submit' }),
78
+ newestActivityMtime: () => 100,
79
+ }
80
+ const r = await deliverWarm(hostedTarget, 'task', '/peer/cwd', seam)
81
+ expect(r.ok).toBe(false)
82
+ if (!r.ok) {
83
+ expect(r.error.message).toContain('deliver failed')
84
+ expect(r.error.message).toContain('socket dead')
85
+ }
86
+ })
87
+
88
+ test('no cwd (direct caller, no activity proxy) → confirmed-only (socket-ack), no mtime probe', async () => {
89
+ let mtimeProbes = 0
90
+ const seam: WarmDeliverSeam = {
91
+ hostAlive: () => true,
92
+ deliverHosted: async () => ({ ok: true }),
93
+ newestActivityMtime: () => {
94
+ mtimeProbes++
95
+ return 100
96
+ },
97
+ }
98
+ const r = await deliverWarm(hostedTarget, 'task', undefined, seam)
99
+ expect(r.ok).toBe(true)
100
+ expect(mtimeProbes).toBe(0) // no cwd → the transcript proxy is never consulted
101
+ })
102
+ })
@@ -43,6 +43,23 @@ import type { ResolvedCaller } from '../identity/index.ts'
43
43
  // dependency: launch does NOT import transport, so no cycle.
44
44
  import { getAdapter } from '../launch/index.ts'
45
45
  import type { ControlCommand, DeliveryMarkers } from '../launch/types.ts'
46
+ // Spawn-flip Ф0b-2: warm-deliver is HOST-AWARE. hostSessionAlive (runtime-state host detect, an
47
+ // @xterm-free pid-file check) + deliverHosted (the pure-protocol socket leaf, Ф0a) come from
48
+ // launch/ptyHost — already in transport's module graph via getAdapter above and @xterm-free by
49
+ // construction (the daemon @xterm-probe covers it). No new cycle: ptyHost's subtree never imports
50
+ // transport. Flag-off (no live supervisor session) → hostSessionAlive is a cheap miss → tmux path.
51
+ import {
52
+ deliverHosted,
53
+ hasAttachedSupervisorClient,
54
+ hostRunDir,
55
+ hostSessionAlive,
56
+ hostSessionToken,
57
+ HOST_COLS,
58
+ HOST_ROWS,
59
+ } from '../launch/ptyHost.ts'
60
+ import { sendControlToHost, type DeliverResult } from '../supervisor/deliver.ts'
61
+ import { keysToBytes } from '../supervisor/protocol.ts' // pure send-keys-vocab→pty-byte translation (boot-driver slice a), @xterm-free
62
+ import { paneLogComposerOccupied } from '../launch/readyGateModel.ts' // spawn-flip Ф0b-3 slice 3c: hosted busy-composer detector (slice 2). @xterm dynamic-loaded inside it → warm path stays @xterm-free with the flip off
46
63
 
47
64
  export interface OnlinePeer {
48
65
  personality: string
@@ -408,6 +425,17 @@ function livenessGraceMs(): number {
408
425
  return Number.isFinite(n) && n >= 0 ? n : 3000
409
426
  }
410
427
 
428
+ // Host (spawn-flip Ф0b-2) liveness grace — DECOUPLED from the tmux grace by design. The hosted path
429
+ // has no input-clear fast confirm (no capture-pane), so a transcript advance is its SOLE landed-proof
430
+ // and may need its own calibration. `IAP_HOST_LIVENESS_GRACE_MS` tunes ONLY the hosted path; when
431
+ // unset it falls back to the shared `livenessGraceMs()` — so calibrating hosted NEVER loosens the
432
+ // tmux/fleet grace (a global bump there would be a fleet regression).
433
+ function hostLivenessGraceMs(): number {
434
+ const raw = process.env.IAP_HOST_LIVENESS_GRACE_MS
435
+ const n = raw === undefined ? NaN : Number(raw)
436
+ return Number.isFinite(n) && n >= 0 ? n : livenessGraceMs()
437
+ }
438
+
411
439
  /**
412
440
  * Deliver `envelope` into the target's tmux session and CONFIRM it landed in a LIVE
413
441
  * session (Ф-B delivery guarantee, contract Демон §Гарантия доставки). `cwd` (the
@@ -472,6 +500,78 @@ export function deliverViaTmux(target: DeliveryTarget, envelope: string, cwd?: s
472
500
  return ok(undefined)
473
501
  }
474
502
 
503
+ // ─── Host-aware warm delivery (spawn-flip cutover Block 2, Ф0b-2) ────────────────────────────────
504
+
505
+ /** Test seam for `deliverWarm`. Production defaults route on the real supervisor; tests inject a
506
+ * fake host-detect + host-deliver + a temp transcript proxy, so the host branch is covered with no
507
+ * live daemon. Omitting all fields = pure production behaviour. */
508
+ export interface WarmDeliverSeam {
509
+ /** Runtime-state host detector (NOT the `.pty-host` marker). Default: the real `hostSessionAlive`. */
510
+ hostAlive?: (identity: string) => boolean
511
+ /** Deliver to the hosted session over its socket. Default: the real `deliverHosted` (Ф0a leaf). */
512
+ deliverHosted?: (identity: string, envelope: string) => Promise<DeliverResult>
513
+ /** Activity proxy for the landed-confirm. Default: the target runtime adapter's transcript mtime. */
514
+ newestActivityMtime?: (cwd: string) => number | null
515
+ }
516
+
517
+ /**
518
+ * Warm-deliver `envelope` to a LIVE local target, HOST-AWARE. A supervisor-HOSTED target (spawn-flip
519
+ * Ф0b: detected by live runtime state, NOT the `.pty-host` marker) is delivered over its socket; any
520
+ * other target takes the unchanged `deliverViaTmux` path. The host branch CONFIRMS landing by the
521
+ * SAME final arbiter as the tmux busy path — a transcript-mtime advance past the pre-deliver baseline,
522
+ * NOT the socket-ack (a flushed CR proves the bytes left us, NOT that the session accepted them;
523
+ * trusting the ack would re-open the silent-loss class the contract forbids). Flag-off (no live
524
+ * supervisor session) → `hostAlive` is a cheap false → `deliverViaTmux` verbatim (byte-identical).
525
+ */
526
+ export async function deliverWarm(
527
+ target: DeliveryTarget,
528
+ envelope: string,
529
+ cwd?: string,
530
+ seam: WarmDeliverSeam = {},
531
+ ): Promise<Result<void>> {
532
+ const hostAlive = seam.hostAlive ?? hostSessionAlive
533
+ if (!hostAlive(target.address)) return deliverViaTmux(target, envelope, cwd)
534
+ return deliverViaHost(target, envelope, cwd, seam)
535
+ }
536
+
537
+ async function deliverViaHost(
538
+ target: DeliveryTarget,
539
+ envelope: string,
540
+ cwd: string | undefined,
541
+ seam: WarmDeliverSeam,
542
+ ): Promise<Result<void>> {
543
+ const adapter = getAdapter(target.runtime)
544
+ const mtimeOf = seam.newestActivityMtime ?? ((c: string) => adapter.newestActivityMtime(c))
545
+ // Baseline the activity proxy BEFORE delivery (a paste/CR does not move the transcript — only a
546
+ // model turn does), so an advance afterwards proves the session is alive and took our message.
547
+ const baseline = cwd ? mtimeOf(cwd) ?? 0 : 0
548
+ const send = seam.deliverHosted ?? deliverHosted
549
+ const sent = await send(target.address, envelope)
550
+ if (!sent.ok) {
551
+ return err(
552
+ `hosted peer "${target.personality}" (${target.runtime}) deliver failed: ${sent.error ?? 'unknown error'}; ` +
553
+ `message NOT delivered`,
554
+ )
555
+ }
556
+ // socket-ack ≠ landed. With no activity proxy (direct callers/tests without a cwd) we can only
557
+ // trust the flushed submit — confirmed-only, mirroring deliverViaTmux's legacy-caller path.
558
+ if (!cwd) return ok(undefined)
559
+ // Confirm landing by the SAME final arbiter as the tmux busy path: a transcript-mtime advance.
560
+ // Grace-poll the proxy (the model may take a beat to write its turn); a hosted session has no
561
+ // input-clear fast path (no capture-pane), so this is the SOLE confirm. Prefer a false-FAIL
562
+ // (sender retries) over a false-OK (silent loss, forbidden).
563
+ const graceMs = hostLivenessGraceMs()
564
+ const graceDeadline = monotonicMs() + graceMs
565
+ do {
566
+ if ((mtimeOf(cwd) ?? 0) > baseline) return ok(undefined)
567
+ sleepSync(LIVENESS_POLL_MS)
568
+ } while (monotonicMs() < graceDeadline)
569
+ return err(
570
+ `hosted peer "${target.personality}" (${target.runtime}) is listed live but did not accept the message ` +
571
+ `(no transcript advance within ${graceMs}ms) — treated as dead; message NOT delivered`,
572
+ )
573
+ }
574
+
475
575
 
476
576
  // ─── Compact done gate (dialogue control must report FACT, not keystroke delivery) ──
477
577
 
@@ -705,11 +805,36 @@ export function waitForCompactDone(
705
805
  * when normal delivery would not land). An unsupported command (router runtimes,
706
806
  * unknown name) → explicit refusal, not a silent no-op.
707
807
  */
708
- export function executeControlOnTarget(target: DeliveryTarget, command: ControlCommand): Result<void> {
808
+ /** Test seam for the hosted control branch (spawn-flip Ф0b-2 slice 3): inject host-detect + the
809
+ * socket control-send so the host path is covered without a live supervisor. Omitting all = prod. */
810
+ export interface ControlHostSeam {
811
+ hostAlive?: (identity: string) => boolean
812
+ sendControl?: (runDir: string, session: string, chunks: Buffer[], opts: { stepDelayMs?: number }) => Promise<DeliverResult>
813
+ }
814
+
815
+ export async function executeControlOnTarget(
816
+ target: DeliveryTarget,
817
+ command: ControlCommand,
818
+ seam: ControlHostSeam = {},
819
+ ): Promise<Result<void>> {
709
820
  const plan = getAdapter(target.runtime).executeControl(command)
710
821
  if (!plan) {
711
822
  return err(`runtime "${target.runtime}" does not support control command "${command.name}"`)
712
823
  }
824
+ // SPAWN-FLIP Ф0b-2 slice 3: a supervisor-HOSTED target has no tmux send-keys — translate each plan
825
+ // step to pty bytes with keysToBytes (REUSED from the boot-driver, NOT reimplemented) and send them
826
+ // over the supervisor socket. Same UNCONDITIONAL semantics (no ready-gate, no submit-confirm) — the
827
+ // point of interrupt is to break a stuck turn. Flag-off (no live host) → the unchanged tmux path.
828
+ const hostAlive = seam.hostAlive ?? hostSessionAlive
829
+ if (hostAlive(target.address)) {
830
+ const chunks = plan.sequence.map(keys => keysToBytes(keys))
831
+ const send = seam.sendControl ?? sendControlToHost
832
+ const sent = await send(hostRunDir(), target.address, chunks, { stepDelayMs: plan.stepDelayMs })
833
+ if (!sent.ok) {
834
+ return err(`hosted control "${command.name}" failed for "${target.personality}" (${target.runtime}): ${sent.error ?? 'unknown error'}`)
835
+ }
836
+ return ok(undefined)
837
+ }
713
838
  for (const keys of plan.sequence) {
714
839
  const r = tmux(target.socketPath, 'send-keys', '-t', target.address, ...keys)
715
840
  if (!r.ok) return err(`tmux send-keys failed for control "${command.name}": ${r.err.trim() || 'unknown error'}`)
@@ -732,14 +857,14 @@ export interface ControlResult {
732
857
  * "specify runtime"). The clean-slash control namespace (interrupt/compact/…) is the
733
858
  * caller's; /alias-* expansions are message, not control (docs/Control §namespace).
734
859
  */
735
- export function routeControl(personality: string, runtime: string | undefined, command: ControlCommand): Result<ControlResult> {
860
+ export async function routeControl(personality: string, runtime: string | undefined, command: ControlCommand): Promise<Result<ControlResult>> {
736
861
  if (!isValidName(personality)) {
737
862
  return err(`invalid personality "${personality}" — must match /^[a-z][a-z0-9-]{0,31}$/`)
738
863
  }
739
864
  if (runtime && !isRuntime(runtime)) return err(`invalid runtime "${runtime}"`)
740
865
  const target = resolveDeliveryTarget({ personality, runtime })
741
866
  if (!target.ok) return err(`cannot control "${personality}": ${target.error.message}`)
742
- const done = executeControlOnTarget(target.value, command)
867
+ const done = await executeControlOnTarget(target.value, command)
743
868
  if (!done.ok) return done
744
869
  return ok({
745
870
  ok: true,
@@ -893,19 +1018,25 @@ export interface ComposerDeliveryQueueOptions {
893
1018
  env?: NodeJS.ProcessEnv
894
1019
  pollMs?: number
895
1020
  forceTimeoutMs?: number
896
- /** Test seam / alternate detector. Production default is the full gate:
897
- * local TUI runtime + attached tmux client + non-dim composer text. */
898
- shouldQueue?: (args: ComposerQueueTryEnqueueArgs) => boolean
899
- /** Test seam for the drain phase. Production default re-reads the pane color
900
- * state (WITHOUT the attached-client prerequisite): once queued, abandoned
901
- * human text waits until cleared or until the 120s force ceiling. */
902
- hasHumanInput?: (target: DeliveryTarget) => boolean
903
- /** Test seam; production default compares tmux session/pane identity captured
904
- * at enqueue time, so `/new`/death/restart fails the old queued envelope rather
905
- * than delivering it into a fresh session with the same name. */
1021
+ /** Daemon pane-log dir (spawn-flip Ф0b-3 slice 3c) — needed by the HOSTED occupancy default to read
1022
+ * `<logDir>/<identity>.log`. Absent → hosted targets never hold (safe: deliver rather than stall). */
1023
+ logDir?: string
1024
+ /** Test seam / alternate detector. Production default is the full gate — tmux: local TUI + attached
1025
+ * tmux client + non-dim composer text; HOSTED (spawn-flip): local TUI + attached SUPERVISOR client +
1026
+ * model-occupied composer. May be async (the hosted occupancy reads an @xterm model). */
1027
+ shouldQueue?: (args: ComposerQueueTryEnqueueArgs) => boolean | Promise<boolean>
1028
+ /** Test seam for the drain phase. Production default re-reads the composer occupancy (WITHOUT the
1029
+ * attached-client prerequisite): once queued, abandoned human text waits until cleared or the 120s
1030
+ * ceiling. tmux → capture-pane colour; HOSTED → pane-log model. May be async. */
1031
+ hasHumanInput?: (target: DeliveryTarget) => boolean | Promise<boolean>
1032
+ /** Test seam; production default compares the session-replacement token captured at enqueue time, so
1033
+ * `/new`/death/restart fails the old queued envelope rather than delivering it into a fresh same-named
1034
+ * session — tmux: session_id:pane_id; HOSTED: the supervisor daemon pid. */
906
1035
  sessionToken?: (target: DeliveryTarget) => string | null
907
1036
  sessionAlive?: (job: ComposerQueuedDelivery) => boolean
908
- deliver?: (target: DeliveryTarget, envelope: string, cwd?: string) => Result<void>
1037
+ /** Delivery sink. Production default is the HOST-AWARE deliverWarm (tmux target → deliverViaTmux
1038
+ * verbatim, hosted target → the supervisor socket). May be async. */
1039
+ deliver?: (target: DeliveryTarget, envelope: string, cwd?: string) => Result<void> | Promise<Result<void>>
909
1040
  notifyFailed: (job: ComposerQueuedDelivery, reason: string) => void | Promise<void>
910
1041
  noteLiveTopic?: (identity: string, topic: string) => void
911
1042
  onDelivered?: (caller: ResolvedCaller) => void
@@ -933,8 +1064,36 @@ function targetSessionToken(target: DeliveryTarget): string | null {
933
1064
  return r.out.trim() || null
934
1065
  }
935
1066
 
1067
+ // ── Host-aware composer-queue predicates (spawn-flip Ф0b-3 slice 3c) ─────────────────────────────────
1068
+ // Each dispatches on hostSessionAlive(address): a HOSTED target uses the supervisor signals, any other
1069
+ // target falls through to the UNCHANGED tmux predicate (so the tmux fleet's observable queue behavior
1070
+ // is byte-identical — only the call becomes awaitable).
1071
+
1072
+ /** Enqueue gate. HOSTED: hold ONLY when a HUMAN is attached (supervisor client) AND the composer model
1073
+ * is occupied — the slice-2 anti-bug: with no operator attached the composer text is the AI's own
1074
+ * output, never held. tmux: the unchanged shouldQueueForHumanComposer. */
1075
+ async function shouldQueueForComposer(target: DeliveryTarget, logDir: string | undefined): Promise<boolean> {
1076
+ if (!hostSessionAlive(target.address)) return shouldQueueForHumanComposer(target)
1077
+ if (!isSupportedLocalRuntime(target.runtime) || !hasAttachedSupervisorClient(target.address)) return false
1078
+ if (!logDir) return false // no pane-log dir → cannot read occupancy → don't hold (deliver, never stall)
1079
+ return paneLogComposerOccupied(`${logDir}/${target.address}.log`, HOST_COLS, HOST_ROWS, target.runtime)
1080
+ }
1081
+
1082
+ /** Drain re-check (no attached-client prereq — once queued, abandoned input waits until cleared/ceiling).
1083
+ * HOSTED: pane-log model occupancy. tmux: the unchanged targetComposerBlocksDelivery. */
1084
+ async function composerStillBusy(target: DeliveryTarget, logDir: string | undefined): Promise<boolean> {
1085
+ if (!hostSessionAlive(target.address)) return targetComposerBlocksDelivery(target)
1086
+ if (!logDir) return false
1087
+ return paneLogComposerOccupied(`${logDir}/${target.address}.log`, HOST_COLS, HOST_ROWS, target.runtime)
1088
+ }
1089
+
1090
+ /** Session-replacement token. HOSTED: supervisor daemon pid; tmux: session_id:pane_id. */
1091
+ function composerSessionToken(target: DeliveryTarget): string | null {
1092
+ return hostSessionAlive(target.address) ? hostSessionToken(target.address) : targetSessionToken(target)
1093
+ }
1094
+
936
1095
  function queuedTargetStillSameSession(job: ComposerQueuedDelivery): boolean {
937
- return targetSessionToken(job.target) === job.sessionToken
1096
+ return composerSessionToken(job.target) === job.sessionToken
938
1097
  }
939
1098
 
940
1099
  let nextComposerQueueId = 1
@@ -957,11 +1116,12 @@ export function createComposerDeliveryQueue(opts: ComposerDeliveryQueueOptions):
957
1116
  const env = opts.env ?? process.env
958
1117
  const pollMs = opts.pollMs ?? composerQueuePollMs(env)
959
1118
  const forceTimeoutMs = opts.forceTimeoutMs ?? composerQueueForceTimeoutMs(env)
960
- const shouldQueue = opts.shouldQueue ?? ((args: ComposerQueueTryEnqueueArgs) => shouldQueueForHumanComposer(args.target))
961
- const hasHumanInput = opts.hasHumanInput ?? targetComposerBlocksDelivery
962
- const sessionToken = opts.sessionToken ?? targetSessionToken
1119
+ const logDir = opts.logDir
1120
+ const shouldQueue = opts.shouldQueue ?? ((args: ComposerQueueTryEnqueueArgs) => shouldQueueForComposer(args.target, logDir))
1121
+ const hasHumanInput = opts.hasHumanInput ?? ((target: DeliveryTarget) => composerStillBusy(target, logDir))
1122
+ const sessionToken = opts.sessionToken ?? composerSessionToken
963
1123
  const sessionAliveFn = opts.sessionAlive ?? queuedTargetStillSameSession
964
- const deliver = opts.deliver ?? deliverViaTmux
1124
+ const deliver = opts.deliver ?? deliverWarm
965
1125
  const queues = new Map<string, ComposerQueuedDelivery[]>()
966
1126
  const draining = new Set<string>()
967
1127
  let closed = false
@@ -997,12 +1157,12 @@ export function createComposerDeliveryQueue(opts: ComposerDeliveryQueueOptions):
997
1157
 
998
1158
  const ageMs = monotonicMs() - job.enqueuedAtMs
999
1159
  const force = ageMs >= forceTimeoutMs
1000
- if (!force && hasHumanInput(job.target)) {
1160
+ if (!force && (await hasHumanInput(job.target))) {
1001
1161
  await delay(pollMs)
1002
1162
  continue
1003
1163
  }
1004
1164
 
1005
- const delivered = deliver(job.target, job.envelope, job.peer.cwd)
1165
+ const delivered = await deliver(job.target, job.envelope, job.peer.cwd)
1006
1166
  q.shift()
1007
1167
  if (!delivered.ok) {
1008
1168
  await notifyFailed(job, `queued delivery to ${job.target.personality} (${job.target.runtime}) failed: ${delivered.error.message}`)
@@ -1020,7 +1180,7 @@ export function createComposerDeliveryQueue(opts: ComposerDeliveryQueueOptions):
1020
1180
  return {
1021
1181
  tryEnqueue: async args => {
1022
1182
  if (closed) return err('composer delivery queue is closing; message NOT queued')
1023
- if (!shouldQueue(args)) return null
1183
+ if (!(await shouldQueue(args))) return null
1024
1184
  const token = sessionToken(args.target)
1025
1185
  if (!token) {
1026
1186
  return err(
@@ -1202,7 +1362,9 @@ export async function routeSend(
1202
1362
  const queued = await deps.composerQueue?.tryEnqueue({ caller, peer, target: target.value, envelope, topic })
1203
1363
  if (queued) return queued
1204
1364
  // peer.cwd enables the Ф-B transcript-mtime liveness probe (busy-session case).
1205
- const delivered = deliverViaTmux(target.value, envelope, peer.cwd)
1365
+ // deliverWarm is HOST-AWARE (spawn-flip Ф0b-2): a supervisor-hosted target delivers over its
1366
+ // socket, any other target keeps the tmux path. Flag-off → byte-identical to deliverViaTmux.
1367
+ const delivered = await deliverWarm(target.value, envelope, peer.cwd)
1206
1368
  if (!delivered.ok) return delivered
1207
1369
  noteTopic(deps, target.value.address, topic)
1208
1370
  return ok({
@@ -1243,7 +1405,7 @@ export async function routeSend(
1243
1405
  if (woke.taskDelivered === false) {
1244
1406
  const queued = await deps.composerQueue?.tryEnqueue({ caller, peer, target: live.value, envelope, topic })
1245
1407
  if (queued) return queued
1246
- const delivered = deliverViaTmux(live.value, envelope, peer.cwd)
1408
+ const delivered = await deliverWarm(live.value, envelope, peer.cwd)
1247
1409
  if (!delivered.ok) return delivered
1248
1410
  noteTopic(deps, live.value.address, topic)
1249
1411
  return ok({