@agfpd/iapeer 0.2.71 → 0.2.73

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.71",
3
+ "version": "0.2.73",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  // key-by-key retype), not the tty discipline.
13
13
 
14
14
  import { afterEach, describe, expect, test } from 'bun:test'
15
- import { mkdtempSync, readFileSync, rmSync, statSync } from 'fs'
15
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'
16
16
  import { tmpdir } from 'os'
17
17
  import { join } from 'path'
18
18
  import { spawnSync } from 'child_process'
@@ -89,6 +89,11 @@ describe('boot first-message delivery (load-buffer + bracketed paste)', () => {
89
89
  readyGateSecs: 8,
90
90
  logDir: join(root, 'logs'),
91
91
  }
92
+ // pty-supervisor hosting is the DEFAULT now; this fixture exercises the legacy TMUX boot/delivery
93
+ // path (load-buffer + bracketed paste) explicitly, so opt this peer OUT of pty with a .no-pty-host
94
+ // marker (otherwise launch hosts it under the supervisor and the tmux-path assertions never apply).
95
+ mkdirSync(cfg.logDir, { recursive: true })
96
+ writeFileSync(join(cfg.logDir, `${spec.identity}.no-pty-host`), '')
92
97
  try {
93
98
  const r = await launch(spec, catAdapter(recv), firstMessage, cfg)
94
99
  expect(r.status).toBe('READY')
@@ -249,17 +249,22 @@ export const launch: LaunchFn = async (
249
249
  // at a not-yet-created dir. Symmetric to the cfg.logDir mkdir before pipe-pane.
250
250
  mkdirSync(dirname(sock), { recursive: true })
251
251
 
252
- // ── SPAWN-FLIP (cutover Block 2, Ф0a) — per-peer `.pty-host` flag, DARK by default ────────────────
253
- // A flag-on tui peer is HOSTED by the supervisor (it owns the pty + boot-driver + pane-log) instead
252
+ // ── PTY-SUPERVISOR HOSTING — the DEFAULT (opt-out per-peer via `.no-pty-host`) ─────────────────────
253
+ // By default a tui peer is HOSTED by the supervisor (it owns the pty + boot-driver + pane-log) instead
254
254
  // of `tmux new-session`. The supervisor serves the SAME launch-composed argv+env (buildLaunchInvocation,
255
255
  // the single source the tmux path also uses), so the hosted session is parity-identical. Readiness is
256
256
  // read off the pane-log model (the supervisor's boot-driver answers startup dialogs internally);
257
257
  // delivery goes over the supervisor socket. UNCONDITIONAL SPAWN-FALLBACK: any host-bringup failure
258
- // falls through to the tmux path below — a flipped peer is NEVER worse than tmux ON SPAWN. Flag-off →
259
- // this whole block is skipped and the tmux path is byte-identical (golden). Routers are never hosted
260
- // (infra, launchd-owned). @xterm stays out of THIS process (ptyHost: @xterm-free start + deliver +
261
- // sessionAlive; readiness's @xterm import is dynamic and only loads here, flag-on).
262
- if (adapter.kind === 'tui' && ptyHostEnabled(cfg.logDir, identity)) {
258
+ // falls through to the tmux path below — a hosted peer is NEVER worse than tmux ON SPAWN. Opt-OUT
259
+ // (`.no-pty-host` marker) → this whole block is skipped and the tmux path is byte-identical (golden);
260
+ // it is the dev/debug escape valid only while tmux still exists. Routers (telegram/notifier) are ALSO
261
+ // hosted by default (cutover infra-track): launchd STILL owns their lifecycle via run-infra (H4) — the
262
+ // supervisor only provides the pty + delivery socket + pane-log; the router branch below skips every
263
+ // TUI phase (no boot dialogs — BOOT_ADAPTERS has no router entry so the supervisor's boot-driver is
264
+ // skipped; no transcript ready-gate — newestActivityMtime=null). @xterm stays out of THIS process
265
+ // (ptyHost: @xterm-free start + deliver + sessionAlive; readiness's @xterm import is dynamic and only
266
+ // loads here, default-on — and the router branch never reaches it).
267
+ if ((adapter.kind === 'tui' || adapter.kind === 'router') && ptyHostEnabled(cfg.logDir, identity)) {
263
268
  const invocation = buildLaunchInvocation(spec, adapter, cfg)
264
269
  mkdirSync(cfg.logDir, { recursive: true, mode: 0o700 }) // the supervisor writes the pane-log here
265
270
  const paneLogPath = `${cfg.logDir}/${identity}.log`
@@ -272,6 +277,17 @@ export const launch: LaunchFn = async (
272
277
  invocation,
273
278
  })
274
279
  if (host.ok) {
280
+ // ROUTER (telegram/notifier): no startup dialogs and no transcript. The session is "up" the
281
+ // instant the supervisor child is serving — symmetric to the tmux router path, where launch
282
+ // returns READY the moment `tmux new-session` succeeds and the always-on caller (runAlwaysOn)
283
+ // does its own BOOT_RECHECK to catch an immediate child-death. Always-on routers launch with an
284
+ // EMPTY firstMessage; deliveries arrive LATER over the socket (deliverHosted → child stdin → the
285
+ // router's <iap>-marker stdin reader). So confirm the hosted session is live and return ready —
286
+ // no waitHostReady (would load @xterm for nothing), no first-message deliver, no ready-gate.
287
+ if (adapter.kind === 'router') {
288
+ if (hostSessionAlive(identity)) return ready(identity)
289
+ return fail(identity, 'hosted router session not alive immediately after bringup')
290
+ }
275
291
  // BOOT: the supervisor's boot-driver dismisses startup dialogs internally; wait for the input
276
292
  // surface via the pane-log model, then deliver over the socket + ready-gate on a model turn —
277
293
  // the SAME shape as the tmux boot/deliver/ready-gate below, with host equivalents.
@@ -23,6 +23,7 @@ import { buildProcessAddress, buildSocketPath } from '../core/socket.ts'
23
23
  import { peerLogsDir, pluginLogsDir } from '../storage/index.ts'
24
24
  import { readPeerProfile } from '../identity/index.ts'
25
25
  import { getAdapter, launch } from './index.ts'
26
+ import { hostSessionAlive, killPtyHost } from './ptyHost.ts'
26
27
  import { dismissCanary, signalCanaryClean } from './canary.ts'
27
28
  import type { LaunchConfig, LaunchSpec } from './types.ts'
28
29
 
@@ -115,6 +116,14 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
115
116
  const sock = buildSocketPath(runtime, personality, sockDir)
116
117
  const adapter = getAdapter(runtime)
117
118
 
119
+ // Host-aware liveness (cutover infra-track): a flag-on router is supervisor-HOSTED (no tmux session);
120
+ // a flag-off one is tmux. Probe BOTH backings by RUNTIME STATE (not the `.pty-host` marker), so this
121
+ // is robust to a spawn-FALLBACK — if startPtyHost failed and launch fell back to tmux, the marker is
122
+ // present yet the live session is tmux. The router is "up" iff EITHER backing is alive; on child
123
+ // death BOTH go false → block-watch exits → run-infra exits → launchd KeepAlive respawns (the SOLE
124
+ // respawn owner; the supervisor never self-respawns — H4 held).
125
+ const isAlive = (): boolean => hostSessionAlive(identity) || sessionAlive(sock, identity)
126
+
118
127
  const cfg: LaunchConfig = {
119
128
  claudeBin: env.CLAUDE_BIN ?? 'claude',
120
129
  codexBin: env.CODEX_BIN ?? 'codex',
@@ -143,7 +152,7 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
143
152
  const spec = buildAlwaysOnSpec(personality, runtime, cwd, sockDir)
144
153
 
145
154
  // Idempotent: bring up only when not already live.
146
- if (!sessionAlive(sock, identity)) {
155
+ if (!isAlive()) {
147
156
  const result = await launch(spec, adapter, '', cfg)
148
157
  if (result.status !== 'READY') {
149
158
  process.stderr.write(`launchdRun: launch FAILED for ${identity}: ${result.reason ?? 'unknown'}\n`)
@@ -155,7 +164,7 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
155
164
  // (a diagnostic, throttled by the plist) instead of a clean exit that reads as a
156
165
  // healthy run in the launchd logs.
157
166
  await sleep(BOOT_RECHECK_MS)
158
- if (!sessionAlive(sock, identity)) {
167
+ if (!isAlive()) {
159
168
  process.stderr.write(
160
169
  `launchdRun: ${identity} session died immediately after launch — check ${cfg.notifierBin ?? `${runtime}-runtime`}\n`,
161
170
  )
@@ -175,7 +184,7 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
175
184
  }
176
185
  process.on('SIGTERM', onSignal)
177
186
  process.on('SIGINT', onSignal)
178
- while (!stop && sessionAlive(sock, identity)) {
187
+ while (!stop && isAlive()) {
179
188
  await new Promise<void>(resolve => {
180
189
  const timer = setTimeout(resolve, WATCH_INTERVAL_MS)
181
190
  interrupt = () => {
@@ -190,7 +199,14 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
190
199
  // holding stale in-memory state (см. teardownAlwaysOnSession). A natural session
191
200
  // death (stop=false) skips this: there is nothing to tear down, exit 0 →
192
201
  // KeepAlive respawns a fresh bring-up.
193
- if (stop) teardownAlwaysOnSession(sock, identity)
202
+ // Signal-initiated stop (bootout / shutdown / kickstart -k) tears the session down WITH this watcher.
203
+ // A hosted router → kill the supervisor daemon (killPtyHost: SIGTERM → it SIGKILLs the child + cleans
204
+ // up); a tmux router → the tmux teardown. Pick by the live backing. A NATURAL death (stop=false) skips
205
+ // this: nothing to tear down, exit 0 → KeepAlive respawns a fresh bring-up.
206
+ if (stop) {
207
+ if (hostSessionAlive(identity)) killPtyHost(identity)
208
+ else teardownAlwaysOnSession(sock, identity)
209
+ }
194
210
  return 0
195
211
  }
196
212
 
@@ -5,27 +5,32 @@ import { join } from 'node:path'
5
5
  import { ptyHostEnabled } from './ptyHost.ts'
6
6
  import { readyGatePtyFlipEnabled } from './readyGateModel.ts'
7
7
 
8
- describe('ptyHostEnabled — the spawn-flip per-peer flag', () => {
9
- test('off by default; on when the .pty-host marker exists', () => {
8
+ describe('ptyHostEnabled — pty-hosting is the DEFAULT, .no-pty-host opts out', () => {
9
+ test('ON by default (no marker needed); OFF when the .no-pty-host opt-out marker exists; per-peer', () => {
10
10
  const dir = mkdtempSync(join(tmpdir(), 'ptyhost-flag-'))
11
11
  try {
12
+ expect(ptyHostEnabled(dir, 'codex-x')).toBe(true) // DEFAULT: hosted out of the box, no marker
13
+ writeFileSync(join(dir, 'codex-x.no-pty-host'), '') // explicit opt-OUT → tmux
12
14
  expect(ptyHostEnabled(dir, 'codex-x')).toBe(false)
13
- writeFileSync(join(dir, 'codex-x.pty-host'), '')
14
- expect(ptyHostEnabled(dir, 'codex-x')).toBe(true)
15
- expect(ptyHostEnabled(dir, 'codex-y')).toBe(false) // per-peer, not global
15
+ expect(ptyHostEnabled(dir, 'codex-y')).toBe(true) // opt-out is per-peer, not global — y stays default-on
16
16
  } finally {
17
17
  rmSync(dir, { recursive: true, force: true })
18
18
  }
19
19
  })
20
20
 
21
- test('DISTINCT from the ready-gate flip marker — a readygate-flipped peer is NOT pty-hosted', () => {
21
+ test('legacy .pty-host opt-in marker is REDUNDANT (ignored); .pty-readygate is orthogonal; only .no-pty-host flips to tmux', () => {
22
22
  const dir = mkdtempSync(join(tmpdir(), 'ptyhost-distinct-'))
23
23
  try {
24
- writeFileSync(join(dir, 'codex-perplex.pty-readygate'), '') // ready-gate flipped, still tmux-hosted
25
- expect(readyGatePtyFlipEnabled(dir, 'codex-perplex')).toBe(true)
26
- expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(false) // the .pty-host marker is separate
24
+ // legacy opt-in marker no longer decides anything — pty is the default with or without it
27
25
  writeFileSync(join(dir, 'codex-perplex.pty-host'), '')
28
- expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(true) // now supervisor-hosted too
26
+ expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(true)
27
+ // the ready-gate viewport-source marker is a SEPARATE concern — it does not decide pty-hosting
28
+ writeFileSync(join(dir, 'codex-perplex.pty-readygate'), '')
29
+ expect(readyGatePtyFlipEnabled(dir, 'codex-perplex')).toBe(true)
30
+ expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(true) // still hosted (default)
31
+ // only the explicit opt-out marker flips it back to tmux
32
+ writeFileSync(join(dir, 'codex-perplex.no-pty-host'), '')
33
+ expect(ptyHostEnabled(dir, 'codex-perplex')).toBe(false)
29
34
  } finally {
30
35
  rmSync(dir, { recursive: true, force: true })
31
36
  }
@@ -26,12 +26,17 @@ import type { RuntimeAdapter } from './types.ts'
26
26
  export const HOST_COLS = 220
27
27
  export const HOST_ROWS = 50
28
28
 
29
- /** Per-peer spawn-flip flag: marker `<logDir>/<identity>.pty-host`. Present → launch hosts the session
30
- * under the supervisor instead of `tmux new-session`. DISTINCT from `.pty-readygate` (the ready-gate
31
- * viewport-source flip): a readygate-flipped peer is still tmux-HOSTED and stays under observer
32
- * cross-check; only a `.pty-host` peer is supervisor-hosted (cross-check retires for it). */
29
+ /** Per-peer pty-hosting decision. pty-supervisor hosting is the DEFAULT — a fresh peer is hosted under
30
+ * the supervisor (not `tmux new-session`) WITHOUT any marker (Артур: «pty из коробки»). A peer opts OUT
31
+ * to the legacy tmux path with a `<logDir>/<identity>.no-pty-host` marker (the dev/debug escape, valid
32
+ * only while tmux still exists). Read in TWO places, both of which want "is this peer pty-hosted": the
33
+ * spawn-gate (launch — host vs tmux) and the shadow observer (skip pty-hosted peers, they have no tmux
34
+ * to compare; an opt-OUT peer is tmux → compared). Legacy `.pty-host` opt-in markers are now REDUNDANT
35
+ * (pty is the default) and harmless — ignored here, so the 29 migrated peers keep hosting unchanged.
36
+ * The golden spawn-fallback (startPtyHost fail → tmux, in launch) is SEPARATE and unaffected: a
37
+ * pty-default peer whose host bring-up fails still falls through to tmux — never worse on spawn. */
33
38
  export function ptyHostEnabled(logDir: string, identity: string): boolean {
34
- return existsSync(`${logDir}/${identity}.pty-host`)
39
+ return !existsSync(`${logDir}/${identity}.no-pty-host`)
35
40
  }
36
41
 
37
42
  /** The supervisor's run dir (session sockets/pids/serve-specs). Env-isolatable via IAPEER_ROOT. */
@@ -4,7 +4,7 @@
4
4
  // not-yet-created dir (prod sock=/tmp always exists; this bit a sandbox pilot).
5
5
 
6
6
  import { afterEach, describe, expect, test } from 'bun:test'
7
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
7
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
8
8
  import { tmpdir } from 'os'
9
9
  import { dirname, join } from 'path'
10
10
  import { spawnSync } from 'child_process'
@@ -51,6 +51,11 @@ describe('launch creates a missing socket dir', () => {
51
51
  alwaysOn: true,
52
52
  }
53
53
 
54
+ // pty-supervisor hosting is the DEFAULT; this regression tests the legacy TMUX new-session sock-dir
55
+ // mkdir, so opt this peer OUT of pty with a .no-pty-host marker (else it is pty-hosted and the tmux
56
+ // mkdir never runs).
57
+ mkdirSync(cfg.logDir, { recursive: true })
58
+ writeFileSync(join(cfg.logDir, `${spec.identity}.no-pty-host`), '')
54
59
  try {
55
60
  // The regression assertion: before the fix the dir was NOT created and
56
61
  // `tmux new-session -S <sock>` failed silently; now launch mkdir's it. (We assert
@@ -1,10 +1,11 @@
1
1
  // Spawn-flip cutover Block 2, Ф0b-3 slice 3b — the fidelity observer SKIPS supervisor-hosted peers.
2
2
  //
3
- // The observer compares the pty-model vs LIVE TMUX. A `.pty-host`-designated peer has no tmux to
4
- // compare against, and during a flip a lingering tmux socket for it would surface FALSE divergences.
5
- // This proves the explicit skip fires IN enumerate even in that race (a live tmux session + a pane-log
6
- // present): the marked peer is NOT enrolled, while an unmarked sibling IS. Real-tmux, guarded (a clean
7
- // CI runner has no tmux; the release machine has it — same pattern as routesend-wake).
3
+ // The observer compares the pty-model vs LIVE TMUX. pty-hosting is now the DEFAULT, so a normal
4
+ // (unmarked) peer has no tmux to compare against — and during a flip a lingering tmux socket for it
5
+ // would surface FALSE divergences. This proves the skip fires IN enumerate even in that race (a live
6
+ // tmux session + a pane-log present): the default peer is NOT enrolled, while a `.no-pty-host` opt-out
7
+ // (tmux) sibling IS. Real-tmux, guarded (a clean CI runner has no tmux; the release machine has it —
8
+ // same pattern as routesend-wake).
8
9
  import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
9
10
  import { spawnSync } from 'node:child_process'
10
11
  import { closeSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
@@ -17,8 +18,8 @@ const tmuxAvailable = spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status ===
17
18
 
18
19
  let sockDir: string
19
20
  let logDir: string
20
- const KEEP = 'keepobs' // unmarked → observed
21
- const SKIP = 'skipobs' // .pty-host marker → skipped
21
+ const KEEP = 'keepobs' // .no-pty-host opt-out (tmux) → observed
22
+ const SKIP = 'skipobs' // default (no marker) → pty-hosted → skipped
22
23
  const addrOf = (p: string) => `codex-${p}`
23
24
  const sockOf = (p: string) => buildSocketPath('codex', p, sockDir)
24
25
 
@@ -43,9 +44,10 @@ beforeAll(() => {
43
44
  startTmux(p)
44
45
  writeFileSync(join(logDir, `${addrOf(p)}.log`), '')
45
46
  }
46
- // SKIP additionally carries the `.pty-host` designation — even WITH a live tmux session, the observer
47
- // must not enroll it (the flip-transition race the skip guards against).
48
- writeFileSync(join(logDir, `${addrOf(SKIP)}.pty-host`), '')
47
+ // KEEP carries the `.no-pty-host` opt-OUT → it is on tmux, so the observer MUST compare it. SKIP is a
48
+ // DEFAULT (unmarked) peer → pty-hosted → skipped even WITH a lingering live tmux session (the
49
+ // flip-transition race the skip guards against).
50
+ writeFileSync(join(logDir, `${addrOf(KEEP)}.no-pty-host`), '')
49
51
  })
50
52
 
51
53
  afterAll(() => {
@@ -54,12 +56,12 @@ afterAll(() => {
54
56
  rmSync(logDir, { recursive: true, force: true })
55
57
  })
56
58
 
57
- describe.if(tmuxAvailable)('observer enumerate skips supervisor-hosted (.pty-host) peers', () => {
58
- test('a .pty-host peer is NOT enrolled even with a live tmux session + pane-log; an unmarked one IS', () => {
59
+ describe.if(tmuxAvailable)('observer enumerate skips pty-default peers, compares .no-pty-host (tmux) peers', () => {
60
+ test('a default (unmarked, pty-hosted) peer is NOT enrolled despite a live tmux session; a .no-pty-host opt-out one IS', () => {
59
61
  const models = new Map<string, any>()
60
62
  enumerate(models, { sockDir, logDir, eventLogDir: logDir })
61
- expect(models.has(addrOf(KEEP))).toBe(true) // unmarked → observed (live tmux + log present)
62
- expect(models.has(addrOf(SKIP))).toBe(false) // .pty-host designation → skipped despite live tmux
63
+ expect(models.has(addrOf(KEEP))).toBe(true) // .no-pty-host opt-out (tmux) → observed (live tmux + log)
64
+ expect(models.has(addrOf(SKIP))).toBe(false) // default pty-hosted → skipped despite lingering live tmux
63
65
  for (const o of models.values()) { try { if (o.fd != null) closeSync(o.fd) } catch { /* */ } }
64
66
  })
65
67
  })
@@ -61,9 +61,11 @@ export function enumerate(models: Map<string, Obs>, opts: ShadowOptions, tracker
61
61
  for (const p of peers) {
62
62
  const addr = buildProcessAddress(p.runtime, p.personality)
63
63
  // SPAWN-FLIP Ф0b-3: NEVER measure a supervisor-HOSTED peer. The observer's premise is pty-model vs
64
- // LIVE TMUX; a `.pty-host`-designated peer has no tmux to compare against, and during a flip a
65
- // lingering tmux socket for it would surface false divergences. Skip by the migration DESIGNATION
66
- // (marker), BEFORE `live` — a peer flipped while enrolled then falls out of `live` and is reaped.
64
+ // LIVE TMUX, so it only compares peers that are actually on tmux. pty-hosting is now the DEFAULT, so
65
+ // ptyHostEnabled is true for a normal peer (hosted → skip) and false only for an explicit `.no-pty-host`
66
+ // opt-OUT peer (tmux → compared). Skip BEFORE `live` — a peer that flips falls out of `live` and is
67
+ // reaped. (Post-migration the whole fleet is pty-default, so this skips everything and the observer is
68
+ // effectively retired until/unless a peer opts back to tmux.)
67
69
  if (ptyHostEnabled(opts.logDir, addr)) continue
68
70
  live.add(addr)
69
71
  if (models.has(addr)) continue
@@ -100,3 +100,38 @@ describe('deliverWarm — hosted target delivers over the socket + confirms by t
100
100
  expect(mtimeProbes).toBe(0) // no cwd → the transcript proxy is never consulted
101
101
  })
102
102
  })
103
+
104
+ // Cutover infra-track — a hosted ROUTER (telegram/notifier) has NO transcript proxy
105
+ // (adapter.newestActivityMtime=null), so the mtime-advance confirm a TUI uses can never be satisfied
106
+ // and would false-FAIL every router delivery. The host path confirms a router by the socket-ack —
107
+ // PARITY with deliverViaTmux's router C-j path (delivery-level confirm; router liveness is structural
108
+ // via launchd, not a model turn). These cases pin that behavior.
109
+ const routerTarget: DeliveryTarget = {
110
+ runtime: 'notifier', // notifierAdapter.kind === 'router'
111
+ personality: 'timer',
112
+ address: 'notifier-timer',
113
+ socketPath: '/tmp/nonexistent-iap-test.sock',
114
+ }
115
+
116
+ describe('deliverWarm — hosted ROUTER confirms by socket-ack (no transcript proxy)', () => {
117
+ test('router deliver ok → ok EVEN THOUGH mtime never advances (the exact scenario a TUI fails on)', async () => {
118
+ process.env.IAP_HOST_LIVENESS_GRACE_MS = '0' // a mtime-gated path would fail at once here
119
+ const seam: WarmDeliverSeam = {
120
+ hostAlive: () => true,
121
+ deliverHosted: async () => ({ ok: true }),
122
+ newestActivityMtime: () => 100, // constant — a router writes no transcript; a TUI here → loud fail
123
+ }
124
+ const r = await deliverWarm(routerTarget, '<iap>x</iap>', '/peer/cwd', seam)
125
+ expect(r.ok).toBe(true) // socket-ack IS the confirm for a router; the non-advancing transcript is irrelevant
126
+ })
127
+
128
+ test('router socket deliver fails (dead/stalled) → loud fail, no false ok', async () => {
129
+ const seam: WarmDeliverSeam = {
130
+ hostAlive: () => true,
131
+ deliverHosted: async () => ({ ok: false, error: 'socket dead during paste' }),
132
+ }
133
+ const r = await deliverWarm(routerTarget, '<iap>x</iap>', '/peer/cwd', seam)
134
+ expect(r.ok).toBe(false)
135
+ if (!r.ok) expect(r.error.message).toContain('deliver failed')
136
+ })
137
+ })
@@ -564,6 +564,13 @@ async function deliverViaHost(
564
564
  `message NOT delivered`,
565
565
  )
566
566
  }
567
+ // Router (telegram/notifier): no transcript proxy (adapter.newestActivityMtime=null) → the
568
+ // mtime-advance confirm below can NEVER be satisfied and would false-FAIL every router delivery.
569
+ // Confirm by the socket-ack just obtained — PARITY with deliverViaTmux's router path (C-j success =
570
+ // delivery-level confirm; a router's liveness is structural via launchd KeepAlive, not a model
571
+ // turn). See deliverViaTmux router branch (paste-buffer -r + C-j, no transcript confirm) + its
572
+ // header note "router … liveness is structural".
573
+ if (adapter.kind === 'router') return ok(undefined)
567
574
  // socket-ack ≠ landed. With no activity proxy (direct callers/tests without a cwd) we can only
568
575
  // trust the flushed submit — confirmed-only, mirroring deliverViaTmux's legacy-caller path.
569
576
  if (!cwd) return ok(undefined)