@agfpd/iapeer 0.2.71 → 0.2.72
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
package/src/launch/index.ts
CHANGED
|
@@ -256,10 +256,14 @@ export const launch: LaunchFn = async (
|
|
|
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
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
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
259
|
+
// this whole block is skipped and the tmux path is byte-identical (golden). Routers (telegram/
|
|
260
|
+
// notifier) are ALSO hostable flag-on (cutover infra-track): launchd STILL owns their lifecycle via
|
|
261
|
+
// run-infra (H4) — the supervisor only provides the pty + delivery socket + pane-log; the router
|
|
262
|
+
// branch below skips every TUI phase (no boot dialogs — BOOT_ADAPTERS has no router entry so the
|
|
263
|
+
// supervisor's boot-driver is skipped; no transcript ready-gate — newestActivityMtime=null). @xterm
|
|
264
|
+
// stays out of THIS process (ptyHost: @xterm-free start + deliver + sessionAlive; readiness's @xterm
|
|
265
|
+
// import is dynamic and only loads here, flag-on — and the router branch never reaches it).
|
|
266
|
+
if ((adapter.kind === 'tui' || adapter.kind === 'router') && ptyHostEnabled(cfg.logDir, identity)) {
|
|
263
267
|
const invocation = buildLaunchInvocation(spec, adapter, cfg)
|
|
264
268
|
mkdirSync(cfg.logDir, { recursive: true, mode: 0o700 }) // the supervisor writes the pane-log here
|
|
265
269
|
const paneLogPath = `${cfg.logDir}/${identity}.log`
|
|
@@ -272,6 +276,17 @@ export const launch: LaunchFn = async (
|
|
|
272
276
|
invocation,
|
|
273
277
|
})
|
|
274
278
|
if (host.ok) {
|
|
279
|
+
// ROUTER (telegram/notifier): no startup dialogs and no transcript. The session is "up" the
|
|
280
|
+
// instant the supervisor child is serving — symmetric to the tmux router path, where launch
|
|
281
|
+
// returns READY the moment `tmux new-session` succeeds and the always-on caller (runAlwaysOn)
|
|
282
|
+
// does its own BOOT_RECHECK to catch an immediate child-death. Always-on routers launch with an
|
|
283
|
+
// EMPTY firstMessage; deliveries arrive LATER over the socket (deliverHosted → child stdin → the
|
|
284
|
+
// router's <iap>-marker stdin reader). So confirm the hosted session is live and return ready —
|
|
285
|
+
// no waitHostReady (would load @xterm for nothing), no first-message deliver, no ready-gate.
|
|
286
|
+
if (adapter.kind === 'router') {
|
|
287
|
+
if (hostSessionAlive(identity)) return ready(identity)
|
|
288
|
+
return fail(identity, 'hosted router session not alive immediately after bringup')
|
|
289
|
+
}
|
|
275
290
|
// BOOT: the supervisor's boot-driver dismisses startup dialogs internally; wait for the input
|
|
276
291
|
// surface via the pane-log model, then deliver over the socket + ready-gate on a model turn —
|
|
277
292
|
// the SAME shape as the tmux boot/deliver/ready-gate below, with host equivalents.
|
package/src/launch/launchdRun.ts
CHANGED
|
@@ -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 (!
|
|
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 (!
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
+
})
|
package/src/transport/index.ts
CHANGED
|
@@ -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)
|