@agfpd/iapeer 0.2.64 → 0.2.66
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/cli/cli.test.ts +2 -2
- package/src/cli/index.ts +16 -3
- package/src/cli/listTui.ts +7 -0
- package/src/daemon/main.ts +4 -0
- package/src/launch/hostComposerOccupied.test.ts +63 -0
- package/src/launch/index.ts +57 -24
- package/src/launch/invocation.test.ts +134 -0
- package/src/launch/invocation.ts +56 -0
- package/src/launch/ptyHost.test.ts +33 -0
- package/src/launch/ptyHost.ts +148 -0
- package/src/launch/readyGateModel.ts +47 -6
- package/src/lifecycle/index.ts +11 -1
- package/src/shadow/enumerate-skip.test.ts +65 -0
- package/src/shadow/index.ts +13 -37
- package/src/shadow/render.ts +60 -0
- package/src/supervisor/attached-marker.test.ts +84 -0
- package/src/supervisor/boot.test.ts +66 -0
- package/src/supervisor/boot.ts +34 -0
- package/src/supervisor/daemon.test.ts +66 -1
- package/src/supervisor/daemon.ts +146 -7
- package/src/supervisor/deliver.test.ts +58 -0
- package/src/supervisor/deliver.ts +120 -0
- package/src/supervisor/index.ts +93 -7
- package/src/supervisor/panelog.test.ts +95 -0
- package/src/supervisor/paths.ts +9 -0
- package/src/supervisor/protocol.test.ts +91 -0
- package/src/supervisor/protocol.ts +49 -0
- package/src/supervisor/selfargv.test.ts +21 -0
- package/src/transport/composerQueue.test.ts +30 -0
- package/src/transport/hostControl.test.ts +87 -0
- package/src/transport/hostDeliver.test.ts +102 -0
- package/src/transport/index.ts +186 -24
package/src/transport/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
897
|
-
*
|
|
898
|
-
|
|
899
|
-
/** Test seam
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
|
|
903
|
-
/** Test seam
|
|
904
|
-
*
|
|
905
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
961
|
-
const
|
|
962
|
-
const
|
|
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 ??
|
|
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
|
-
|
|
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 =
|
|
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({
|