@agfpd/iapeer 0.4.8 → 0.4.9
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/index.ts +13 -80
- package/src/cli/listTui.ts +4 -14
- package/src/launch/cmdlog.ts +7 -110
- package/src/launch/index.ts +14 -314
- package/src/launch/launchdRun.ts +9 -52
- package/src/launch/ptyHost.ts +0 -12
- package/src/launch/readyGateModel.ts +9 -80
- package/src/lifecycle/index.ts +25 -101
- package/src/supervisor/daemon.ts +1 -1
- package/src/{shadow → supervisor}/render.ts +10 -10
- package/src/transport/index.ts +41 -400
- package/src/update/index.ts +0 -9
- package/src/launch/canary.ts +0 -239
- package/src/shadow/gate.ts +0 -396
- package/src/shadow/index.ts +0 -174
- package/src/shadow/install.ts +0 -256
package/src/lifecycle/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from '../core/constants.ts'
|
|
27
27
|
import { buildProcessAddress, buildSocketPath, parseSessionName } from '../core/socket.ts'
|
|
28
28
|
import { err, ok, type Result } from '../core/errors.ts'
|
|
29
|
-
import {
|
|
29
|
+
import { capPaneLogs } from '../launch/cmdlog.ts'
|
|
30
30
|
import { resolveGlobalRoot } from '../storage/index.ts'
|
|
31
31
|
import { readPeerProfile, resolveIdentity } from '../identity/index.ts'
|
|
32
32
|
import {
|
|
@@ -47,8 +47,7 @@ import {
|
|
|
47
47
|
type LaunchSpec,
|
|
48
48
|
} from '../launch/index.ts'
|
|
49
49
|
import { composeSystemPrompt, gatherPromptInput } from '../launch/composeSystemPrompt.ts'
|
|
50
|
-
import {
|
|
51
|
-
import { hostSessionAlive, killPtyHost, ptyHostEnabled } from '../launch/ptyHost.ts'
|
|
50
|
+
import { hostSessionAlive, killPtyHost } from '../launch/ptyHost.ts'
|
|
52
51
|
import { appendLifecycleEvent, superviseLogVerbose } from './eventlog.ts'
|
|
53
52
|
|
|
54
53
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -494,18 +493,6 @@ export function purgeIdentityState(cfg: LifecycleConfig, identity: string): stri
|
|
|
494
493
|
/* best-effort — a locked/vanished entry must not fail the remove */
|
|
495
494
|
}
|
|
496
495
|
}
|
|
497
|
-
// tmux command-log dir (0.2.40 observability) — identity-keyed like the state
|
|
498
|
-
// markers above, so it must die with the registry record too (the leak class
|
|
499
|
-
// noted at the 0.2.40 acceptance: a removed peer left its cmdlog dir forever).
|
|
500
|
-
try {
|
|
501
|
-
const cmdDir = cmdLogDirFor(cfg.eventLogDir, identity)
|
|
502
|
-
if (existsSync(cmdDir)) {
|
|
503
|
-
rmSync(cmdDir, { recursive: true, force: true })
|
|
504
|
-
removed.push(`tmux-cmdlog/${identity}`)
|
|
505
|
-
}
|
|
506
|
-
} catch {
|
|
507
|
-
/* best-effort */
|
|
508
|
-
}
|
|
509
496
|
return removed
|
|
510
497
|
}
|
|
511
498
|
|
|
@@ -724,52 +711,23 @@ export async function withWakeLock<T>(
|
|
|
724
711
|
}
|
|
725
712
|
|
|
726
713
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
727
|
-
//
|
|
714
|
+
// liveness
|
|
728
715
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
729
716
|
|
|
730
|
-
function
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
* SIGKILL-class and exits.log stayed empty, because the pane-died hook needs a
|
|
745
|
-
* living tmux event loop). Two distinguishable classes:
|
|
746
|
-
* - `session-gone` — the server on the socket still ANSWERS but the session is not
|
|
747
|
-
* there: a pane died inside a living server → the pane-died hook had its chance,
|
|
748
|
-
* exits.log should carry the cause.
|
|
749
|
-
* - `server-dead` — the server itself is gone: the socket file is missing, or it
|
|
750
|
-
* exists but nothing serves it (stale socket — the SIGKILL/OOM class). pane-died
|
|
751
|
-
* could never fire; the server-death canary (launch/canary.ts) is the recorder
|
|
752
|
-
* for this class (`ev=server-exit` + forensics), this line is the backstop.
|
|
753
|
-
* `hosted=true` (pty-supervisor peer) reframes ONLY the absent-socket reason: a hosted peer
|
|
754
|
-
* has NO tmux socket, so that branch would otherwise misattribute its death to a tmux server
|
|
755
|
-
* that never existed. Its real cause lives in the supervisor exits.log. A socket that EXISTS
|
|
756
|
-
* is a genuine tmux server, so `hosted` never overrides the session-gone / stale-socket arm. */
|
|
757
|
-
export function classifyGoneSession(sock: string, hosted = false): { death: 'server-dead' | 'session-gone'; reason: string } {
|
|
758
|
-
if (!existsSync(sock)) {
|
|
759
|
-
// No socket file. For a pty-supervisor-hosted peer this is EXPECTED — it never had a tmux
|
|
760
|
-
// socket; its death is the supervisor session ending (cause in the supervisor exits.log
|
|
761
|
-
// death-EVENT), NOT a tmux server vanishing. For a tmux peer it IS a dead tmux server.
|
|
762
|
-
// The check is gated on the absent socket on purpose: a socket that EXISTS (below) means a
|
|
763
|
-
// real tmux server, so `hosted` (pty-enabled-by-default) must not override genuine tmux.
|
|
764
|
-
return hosted
|
|
765
|
-
? { death: 'server-dead', reason: 'pty host session ended (supervisor) — cause in exits.log (death-EVENT)' }
|
|
766
|
-
: { death: 'server-dead', reason: 'tmux server gone (socket file missing)' }
|
|
767
|
-
}
|
|
768
|
-
// Ask the SERVER, not the session: any server-level command answering (exit 0)
|
|
769
|
-
// proves the server is alive and merely lost this session.
|
|
770
|
-
return tmux(sock, 'list-sessions').ok
|
|
771
|
-
? { death: 'session-gone', reason: 'session gone, tmux server alive (exit cause should be in exits.log)' }
|
|
772
|
-
: { death: 'server-dead', reason: 'tmux server dead — stale socket (SIGKILL class — OOM only if a matching JetsamEvent report exists; check exits.log ev=server-exit canary record)' }
|
|
717
|
+
function sessionAlive(_sock: string, identity: string): boolean {
|
|
718
|
+
// pty-only: a peer is alive iff its supervisor session is live (a cheap pid-file check). Routing
|
|
719
|
+
// liveness on the live session (not the `.pty-host` spawn-intent marker) keeps a marker rollback safe
|
|
720
|
+
// on a still-running session. (`_sock` retained for call-site compatibility; no tmux sockets exist.)
|
|
721
|
+
return hostSessionAlive(identity)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Death-class tag for a gone session (pty-only). A gone peer is a supervisor session that ended; its
|
|
725
|
+
* real cause lives in the supervisor exits.log (death-EVENT). There is no tmux server to distinguish
|
|
726
|
+
* `session-gone` (pane died, server alive) from `server-dead` (server gone). This tag is
|
|
727
|
+
* observability-only (lifecycle.log trace) and drives no decision. The `death`/`hosted` shape is kept
|
|
728
|
+
* for the trace schema + call-site compatibility. */
|
|
729
|
+
export function classifyGoneSession(_sock: string, _hosted = false): { death: 'server-dead' | 'session-gone'; reason: string } {
|
|
730
|
+
return { death: 'server-dead', reason: 'pty host session ended (supervisor) — cause in exits.log (death-EVENT)' }
|
|
773
731
|
}
|
|
774
732
|
|
|
775
733
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1149,26 +1107,11 @@ export async function wakeOrSpawn(args: WakeArgs, deps: WakeDeps = {}): Promise<
|
|
|
1149
1107
|
// Reap — kill a session (used by idle-reap / supervise; H4-guarded by callers)
|
|
1150
1108
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1151
1109
|
|
|
1152
|
-
export function killSession(
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
|
|
1157
|
-
tmux(sock, 'kill-session', '-t', identity)
|
|
1158
|
-
const sessions = tmux(sock, 'list-sessions', '-F', '#{session_name}').out
|
|
1159
|
-
if (!sessions.trim()) {
|
|
1160
|
-
// Deliberate server teardown → silence the canary EXPLICITLY first: signal
|
|
1161
|
-
// the channel (client exits 0) AND dismiss the sh recorder (canary v2 no
|
|
1162
|
-
// longer reads a signaled client alone as deliberate — see canary.ts).
|
|
1163
|
-
signalCanaryClean(sock, identity)
|
|
1164
|
-
dismissCanary(identity)
|
|
1165
|
-
tmux(sock, 'kill-server')
|
|
1166
|
-
try {
|
|
1167
|
-
rmSync(sock, { force: true })
|
|
1168
|
-
} catch {
|
|
1169
|
-
/* best-effort */
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1110
|
+
export function killSession(_sock: string, identity: string): void {
|
|
1111
|
+
// pty-only: SIGTERM the supervisor daemon — its shutdown SIGKILLs the child and removes the
|
|
1112
|
+
// sock/pid/serve-spec/markers. Idempotent if the session is already gone. (`_sock` retained for
|
|
1113
|
+
// call-site compatibility; no tmux sockets exist.)
|
|
1114
|
+
killPtyHost(identity)
|
|
1172
1115
|
}
|
|
1173
1116
|
|
|
1174
1117
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1212,11 +1155,6 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1212
1155
|
const activityMtime = deps.newestActivityMtime ?? ((rt: string, c: string) => getAdapter(rt as Runtime).newestActivityMtime(c))
|
|
1213
1156
|
const isAlive = deps.sessionAlive ?? sessionAlive
|
|
1214
1157
|
const verbose = superviseLogVerbose(env)
|
|
1215
|
-
// tmux command-log volume cap (killer-hunt socket-command layer, launch/cmdlog.ts):
|
|
1216
|
-
// a -v server log under a busy TUI grows ~7 KB/s, so each tick tail-keeps any log
|
|
1217
|
-
// over the cap. Best-effort by construction; the killer's command is the LAST
|
|
1218
|
-
// line of a dying log, so capping a LIVE peer's log never costs the death tail.
|
|
1219
|
-
capCmdLogs(cfg.eventLogDir)
|
|
1220
1158
|
// Pane-log volume cap (launch/cmdlog.ts capPaneLogs): the per-identity pane-log
|
|
1221
1159
|
// (<logDir>/<identity>.log) is the RAW TUI byte stream pipe-pane/the supervisor
|
|
1222
1160
|
// append for a session's whole life — it had NO bound and grew to hundreds of MB
|
|
@@ -1283,7 +1221,7 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1283
1221
|
// distinguishable in lifecycle.log: `session-gone` (pane died, server alive →
|
|
1284
1222
|
// exits.log should have the cause) vs `server-dead` (whole tmux server died →
|
|
1285
1223
|
// exits.log structurally empty; this line is the only durable trace).
|
|
1286
|
-
const gone = classifyGoneSession(sock
|
|
1224
|
+
const gone = classifyGoneSession(sock)
|
|
1287
1225
|
out.push({ identity: s.identity, action: 'reaped-gone', reason: gone.reason })
|
|
1288
1226
|
trace({ identity: s.identity, action: 'reaped-gone', death: gone.death, reason: gone.reason, outcome: 'fresh-next-msg' })
|
|
1289
1227
|
continue
|
|
@@ -1366,22 +1304,8 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1366
1304
|
// every ALIVE daemon-owned server — covers a fleet launched by older code
|
|
1367
1305
|
// within one tick of a deploy, no session restarts. Idempotent (pgrep on
|
|
1368
1306
|
// the per-identity wait-for channel), best-effort, pure observability.
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
// alive server means the previous canary VANISHED mid-watch (the death-#4
|
|
1372
|
-
// blind spot was exactly this churn being invisible); 'failed' means the
|
|
1373
|
-
// server is currently UNWATCHED. At most one line per loss, not per tick.
|
|
1374
|
-
// SPAWN-FLIP Ф0b-3: the tmux server-death canary is meaningless for a supervisor-HOSTED session
|
|
1375
|
-
// (no tmux server — the retrofit arms a watcher that immediately fires a spurious
|
|
1376
|
-
// `server-vanished`). A hosted death is recorded by the supervisor's exits.log (host=supervisor,
|
|
1377
|
-
// resilience-b), so skip the canary retrofit for hosted peers (mirror of the observer .pty-host
|
|
1378
|
-
// skip — tmux-assuming infra must not run against a hosted session).
|
|
1379
|
-
if (!hostSessionAlive(s.identity)) {
|
|
1380
|
-
const canaryState = ensureServerCanary({ identity: s.identity, sock, exitLogDir: cfg.eventLogDir, env })
|
|
1381
|
-
if (canaryState !== 'already') {
|
|
1382
|
-
trace({ identity: s.identity, action: 'canary', state: canaryState, origin: 'retrofit' })
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1307
|
+
// pty-only: no tmux server-death canary — a hosted death is recorded by the supervisor's
|
|
1308
|
+
// exits.log (host=supervisor, resilience-b), classified by classifyGoneSession on the next sweep.
|
|
1385
1309
|
out.push({ identity: s.identity, action: 'alive' })
|
|
1386
1310
|
if (verbose) trace({ identity: s.identity, action: 'alive', age: `${ageSecs}s` })
|
|
1387
1311
|
}
|
package/src/supervisor/daemon.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from './protocol.ts'
|
|
27
27
|
import { attachedPath, geometryPath, pidPath, servePath, sockPath, writeGeometry } from './paths.ts'
|
|
28
28
|
import { nextBootAction, type BootPredicates } from './boot.ts'
|
|
29
|
-
import { modelToPlainText } from '
|
|
29
|
+
import { modelToPlainText } from './render.ts'
|
|
30
30
|
import { codexAdapter } from '../launch/adapters/codex.ts'
|
|
31
31
|
import { claudeAdapter } from '../launch/adapters/claude.ts'
|
|
32
32
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Leaf render helper: @xterm model → plain text
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Leaf render helper: @xterm model → plain text + composer-occupancy verdict. A shared LIVE leaf used
|
|
2
|
+
// by the Ф1 ready-gate flip (launch/readyGateModel) and the supervisor pane model. Type-only @xterm
|
|
3
|
+
// import → no runtime dependency beyond the Terminal instance the caller already holds; hermetically
|
|
4
|
+
// testable. (Formerly src/shadow/render.ts — kept after the tmux→pty cutover retired the shadow
|
|
5
|
+
// burn-in observer; only the observer modules were removed, this leaf survives.)
|
|
5
6
|
import type { Terminal } from '@xterm/headless'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -10,7 +11,7 @@ import type { Terminal } from '@xterm/headless'
|
|
|
10
11
|
* whitespace per line is trimmed (capture-pane does the same), cells coalesced left-to-right.
|
|
11
12
|
*
|
|
12
13
|
* 0-divergence-validated vs capture-pane on READY screens by the ready-gate burn-in; the supervisor
|
|
13
|
-
* boot-driver additionally
|
|
14
|
+
* boot-driver additionally validated it on BOOT-DIALOG screens (a different screen state the
|
|
14
15
|
* ready-gate observation never saw).
|
|
15
16
|
*/
|
|
16
17
|
export function modelToPlainText(term: Terminal, cols: number, rows: number): string {
|
|
@@ -40,11 +41,10 @@ const promptGlyph = (rt: string): string => (rt === 'claude' ? '❯' : '›')
|
|
|
40
41
|
/**
|
|
41
42
|
* PTY composer-occupancy verdict: getCell on the model's viewport composer row (the row whose first
|
|
42
43
|
* visible cell is the prompt glyph); human input = a non-ghost (non-dim, non-grey246) visible cell
|
|
43
|
-
* after it. Mirrors
|
|
44
|
-
* in this leaf (type-only @xterm) so the
|
|
45
|
-
* pulling
|
|
46
|
-
*
|
|
47
|
-
* shadow/index.ts so the observer's existing importers keep resolving.
|
|
44
|
+
* after it. Mirrors the prior tmux capture-pane occupancy check (validated 0-divergence vs it by the
|
|
45
|
+
* burn-in). Lives in this leaf (type-only @xterm) so the warm-deliver hosted occupancy guard can reuse
|
|
46
|
+
* it WITHOUT pulling an @xterm-loading graph into the warm-deliver path; the Terminal instance is built
|
|
47
|
+
* by the caller (paneLogComposerOccupied dynamic-imports @xterm).
|
|
48
48
|
*/
|
|
49
49
|
export function composerOccupancyFromModel(term: Terminal, cols: number, rows: number, runtime: string): boolean {
|
|
50
50
|
const b = term.buffer.active, top = b.baseY, bot = b.baseY + rows, g = promptGlyph(runtime)
|