@agfpd/iapeer 0.4.26 → 0.4.27
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/launch/adapters/claude.ts +28 -0
- package/src/launch/adapters/codex.ts +22 -3
- package/src/launch/adapters/notifier.ts +4 -0
- package/src/launch/adapters/telegram.ts +4 -0
- package/src/launch/adapters/transcriptTail.ts +64 -0
- package/src/launch/adapters/voicetalk.ts +4 -0
- package/src/launch/types.ts +9 -2
- package/src/lifecycle/index.ts +28 -56
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ import { homedir } from 'os'
|
|
|
24
24
|
import { join } from 'path'
|
|
25
25
|
import { readdirSync, realpathSync, statSync } from 'fs'
|
|
26
26
|
import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from '../types.ts'
|
|
27
|
+
import { lastTimestampedEntryMs } from './transcriptTail.ts'
|
|
27
28
|
|
|
28
29
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
30
|
// Boot dialog + ready markers (lifecycle claudeBootDialog / claudeInputReady)
|
|
@@ -280,6 +281,33 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
280
281
|
return newest > 0 ? newest : null
|
|
281
282
|
},
|
|
282
283
|
|
|
284
|
+
/**
|
|
285
|
+
* IDLE-accounting signal: the content-timestamp of the last meaningful entry in the ACTIVE transcript
|
|
286
|
+
* (newest .jsonl by file mtime), read from its tail. Immune to the file re-save (file-mtime is fresh
|
|
287
|
+
* but the last ENTRY is not) and to the statusline pane-log tick (the transcript is untouched by a
|
|
288
|
+
* status redraw). null when the dir is absent/empty or no timestamped entry sits in the tail.
|
|
289
|
+
*/
|
|
290
|
+
lastTurnMtime(cwd: string): number | null {
|
|
291
|
+
let entries: string[]
|
|
292
|
+
try {
|
|
293
|
+
entries = readdirSync(transcriptDir(cwd))
|
|
294
|
+
} catch {
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
let best = { path: '', mt: 0 }
|
|
298
|
+
for (const name of entries) {
|
|
299
|
+
if (!name.endsWith('.jsonl')) continue
|
|
300
|
+
try {
|
|
301
|
+
const path = join(transcriptDir(cwd), name)
|
|
302
|
+
const mt = statSync(path).mtimeMs
|
|
303
|
+
if (mt > best.mt) best = { path, mt }
|
|
304
|
+
} catch {
|
|
305
|
+
/* race */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return best.path ? lastTimestampedEntryMs(best.path) : null
|
|
309
|
+
},
|
|
310
|
+
|
|
283
311
|
/**
|
|
284
312
|
* Resume preflight (fail-loud — never a silent fresh fallback): resolve the
|
|
285
313
|
* newest transcript uuid for cwd via the claude-slug dir scan (lifecycle.
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { homedir } from 'os'
|
|
39
39
|
import { join } from 'path'
|
|
40
40
|
import { readFileSync, readdirSync, realpathSync, statSync } from 'fs'
|
|
41
|
+
import { lastTimestampedEntryMs } from './transcriptTail.ts'
|
|
41
42
|
import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, RuntimeAdapter } from '../types.ts'
|
|
42
43
|
|
|
43
44
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -114,9 +115,16 @@ function canonicalPath(p: string): string {
|
|
|
114
115
|
* (the model produced its first turn); idle accounting reads the same proxy.
|
|
115
116
|
*/
|
|
116
117
|
function newestCodexSessionMtime(cwd: string): number | null {
|
|
118
|
+
return newestCodexSession(cwd)?.mt ?? null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The newest ~/.codex/sessions/**\/*.jsonl whose recorded session cwd realpath-matches `cwd`, as
|
|
122
|
+
* {path, mt}, or null when none. Shared by newestActivityMtime (file-mtime) and lastTurnMtime (needs
|
|
123
|
+
* the PATH to read the tail). Codex nests its session logs in a date tree, so the scan recurses. */
|
|
124
|
+
function newestCodexSession(cwd: string): { path: string; mt: number } | null {
|
|
117
125
|
const root = join(homedir(), '.codex', 'sessions')
|
|
118
126
|
const target = canonicalPath(cwd)
|
|
119
|
-
let
|
|
127
|
+
let best = { path: '', mt: 0 }
|
|
120
128
|
function visit(dir: string): void {
|
|
121
129
|
let entries
|
|
122
130
|
try {
|
|
@@ -134,14 +142,14 @@ function newestCodexSessionMtime(cwd: string): number | null {
|
|
|
134
142
|
if (canonicalPath(codexSessionCwd(path) ?? '') !== target) continue
|
|
135
143
|
try {
|
|
136
144
|
const mt = statSync(path).mtimeMs
|
|
137
|
-
if (mt >
|
|
145
|
+
if (mt > best.mt) best = { path, mt }
|
|
138
146
|
} catch {
|
|
139
147
|
/* race — entry vanished between readdir and stat */
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
visit(root)
|
|
144
|
-
return
|
|
152
|
+
return best.path ? best : null
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -259,6 +267,17 @@ export const codexAdapter: RuntimeAdapter = {
|
|
|
259
267
|
return newestCodexSessionMtime(cwd)
|
|
260
268
|
},
|
|
261
269
|
|
|
270
|
+
/**
|
|
271
|
+
* IDLE-accounting signal: the content-timestamp of the last meaningful entry in the active codex
|
|
272
|
+
* session jsonl (newest cwd-matching file), read from its tail. codex entries (event_msg /
|
|
273
|
+
* response_item) carry a top-level `timestamp`, so this reflects real turn activity — immune to a
|
|
274
|
+
* file re-save and to a statusline pane-log tick. null when no cwd-matching session / no timestamp.
|
|
275
|
+
*/
|
|
276
|
+
lastTurnMtime(cwd: string): number | null {
|
|
277
|
+
const s = newestCodexSession(cwd)
|
|
278
|
+
return s ? lastTimestampedEntryMs(s.path) : null
|
|
279
|
+
},
|
|
280
|
+
|
|
262
281
|
/**
|
|
263
282
|
* Resume preflight (fail-loud — never a silent fresh fallback). Codex resumes
|
|
264
283
|
* via 'resume --last' with NO ref: it matches the newest session for cwd
|
|
@@ -68,6 +68,10 @@ export const notifierAdapter: RuntimeAdapter = {
|
|
|
68
68
|
return null
|
|
69
69
|
},
|
|
70
70
|
|
|
71
|
+
lastTurnMtime(_cwd: string): number | null {
|
|
72
|
+
return null // router — no transcript
|
|
73
|
+
},
|
|
74
|
+
|
|
71
75
|
/** Nothing to resume: a router does not replay a transcript. */
|
|
72
76
|
resolveResume(_cwd: string): { ok: boolean; ref?: string; reason?: string } {
|
|
73
77
|
return { ok: true }
|
|
@@ -98,6 +98,10 @@ export const telegramAdapter: RuntimeAdapter = {
|
|
|
98
98
|
return null
|
|
99
99
|
},
|
|
100
100
|
|
|
101
|
+
lastTurnMtime(_cwd: string): number | null {
|
|
102
|
+
return null // router — no transcript
|
|
103
|
+
},
|
|
104
|
+
|
|
101
105
|
/**
|
|
102
106
|
* Nothing to resume: a router does not replay a transcript, so resume cannot
|
|
103
107
|
* fail-loud the way claude/codex do — there is no prior session ref to resolve
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Shared transcript-tail reader for the idle-accounting signal.
|
|
2
|
+
//
|
|
3
|
+
// The RELIABLE "last real turn" time is the content-timestamp of the LAST meaningful transcript entry —
|
|
4
|
+
// NOT the file mtime (a live claude session RE-SAVES its .jsonl without a new entry, so file-mtime is
|
|
5
|
+
// falsely fresh at idle) and NOT the pane-log mtime (a statusline / footer re-render writes pty bytes at
|
|
6
|
+
// idle, so the pane-log is falsely fresh too). The transcript ENTRY stream only advances on actual turn
|
|
7
|
+
// activity (user/assistant/tool/turn_duration for claude; event_msg/response_item for codex), so the
|
|
8
|
+
// last entry that carries a `timestamp` is the true idle signal — immune to both noise sources.
|
|
9
|
+
|
|
10
|
+
import { openSync, readSync, closeSync, statSync } from 'fs'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Content-timestamp (epoch ms) of the LAST jsonl entry carrying a `timestamp` field, read from the
|
|
14
|
+
* file's TAIL (cheap — only the last `tailBytes` are read). Metadata records without a `timestamp`
|
|
15
|
+
* (claude `mode` / `ai-title` / `bridge-session`) are skipped. Returns null when the file is
|
|
16
|
+
* unreadable or no timestamped entry sits in the tail (the caller then floors at wokeAt). A partial
|
|
17
|
+
* FIRST line in the tail buffer is harmless: we scan from the END and return on the first complete
|
|
18
|
+
* timestamped entry, so a truncated leading line is only ever reached (and skipped) when nothing
|
|
19
|
+
* matched.
|
|
20
|
+
*/
|
|
21
|
+
export function lastTimestampedEntryMs(path: string, tailBytes = 65536): number | null {
|
|
22
|
+
let fd: number
|
|
23
|
+
try {
|
|
24
|
+
fd = openSync(path, 'r')
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const size = statSync(path).size
|
|
30
|
+
const start = Math.max(0, size - tailBytes)
|
|
31
|
+
const len = size - start
|
|
32
|
+
if (len <= 0) return null
|
|
33
|
+
const buf = Buffer.allocUnsafe(len)
|
|
34
|
+
let read = 0
|
|
35
|
+
while (read < len) {
|
|
36
|
+
const n = readSync(fd, buf, read, len - read, start + read)
|
|
37
|
+
if (n <= 0) break
|
|
38
|
+
read += n
|
|
39
|
+
}
|
|
40
|
+
const lines = buf.subarray(0, read).toString('utf8').split(/\r?\n/)
|
|
41
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
42
|
+
const line = lines[i].trim()
|
|
43
|
+
if (!line) continue
|
|
44
|
+
let obj: unknown
|
|
45
|
+
try {
|
|
46
|
+
obj = JSON.parse(line)
|
|
47
|
+
} catch {
|
|
48
|
+
continue // partial leading line / non-json → skip
|
|
49
|
+
}
|
|
50
|
+
const ts = (obj as { timestamp?: unknown })?.timestamp
|
|
51
|
+
if (typeof ts === 'string') {
|
|
52
|
+
const ms = Date.parse(ts)
|
|
53
|
+
if (!Number.isNaN(ms)) return ms
|
|
54
|
+
} else if (typeof ts === 'number' && Number.isFinite(ts)) {
|
|
55
|
+
return ts
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
} finally {
|
|
62
|
+
closeSync(fd)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -81,6 +81,10 @@ export const voicetalkAdapter: RuntimeAdapter = {
|
|
|
81
81
|
return null
|
|
82
82
|
},
|
|
83
83
|
|
|
84
|
+
lastTurnMtime(_cwd: string): number | null {
|
|
85
|
+
return null // router — no transcript
|
|
86
|
+
},
|
|
87
|
+
|
|
84
88
|
/** Nothing to resume: a router does not replay a transcript, so resume cannot
|
|
85
89
|
* fail-loud the way claude/codex do. Always {ok:true}. */
|
|
86
90
|
resolveResume(_cwd: string): { ok: boolean; ref?: string; reason?: string } {
|
package/src/launch/types.ts
CHANGED
|
@@ -263,10 +263,17 @@ export interface RuntimeAdapter {
|
|
|
263
263
|
* AND startup dialogs gone). Router runtimes return true (no input surface). */
|
|
264
264
|
isInputReady(pane: string): boolean
|
|
265
265
|
|
|
266
|
-
/** Newest activity-proxy mtime
|
|
267
|
-
*
|
|
266
|
+
/** Newest activity-proxy FILE mtime — used by the READY-GATE (it waits for ANY transcript write past
|
|
267
|
+
* baseline = "the model produced its first turn"). NOT for idle accounting: a live session re-saves
|
|
268
|
+
* this file without a new entry, so it is falsely fresh at idle. null for a router (no proxy). */
|
|
268
269
|
newestActivityMtime(cwd: string): number | null
|
|
269
270
|
|
|
271
|
+
/** Content-timestamp (epoch ms) of the LAST meaningful transcript ENTRY — the reliable IDLE-accounting
|
|
272
|
+
* signal. Unlike newestActivityMtime (file mtime, re-touched at idle) and the pane-log mtime (a
|
|
273
|
+
* statusline re-render ticks it at idle), the entry stream only advances on real turn activity.
|
|
274
|
+
* superviseTick uses THIS for idle/quiet age. null for a router (no transcript). */
|
|
275
|
+
lastTurnMtime(cwd: string): number | null
|
|
276
|
+
|
|
270
277
|
/** Resume preflight — validate a resume request fail-loud (never silent fresh).
|
|
271
278
|
* Returns the resolved ref (claude uuid) or ok:false with a reason. */
|
|
272
279
|
resolveResume(cwd: string): { ok: boolean; ref?: string; reason?: string }
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -344,21 +344,6 @@ export function clearEphemeralArmed(cfg: LifecycleConfig, identity: string): voi
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
/** mtime (ms) of a hosted session's pane-log (`<logDir>/<identity>.log`) — the RAW TUI
|
|
348
|
-
* render byte-stream the supervisor appends for the session's whole life. It advances
|
|
349
|
-
* ~1s while the TUI is rendering a turn (the "esc to interrupt" elapsed counter ticks
|
|
350
|
-
* during BOTH model generation AND a tool run) and goes quiet at the prompt. So it is
|
|
351
|
-
* the truest active-turn liveness signal — the SAME mtime telegram-runtime reads for its
|
|
352
|
-
* typing/activity indicator — and unlike the transcript proxy it does NOT go quiet during
|
|
353
|
-
* a long model generation (no JSONL write until the message completes). null if absent. */
|
|
354
|
-
function paneLogMtimeMs(cfg: LifecycleConfig, identity: string): number | null {
|
|
355
|
-
try {
|
|
356
|
-
return statSync(join(cfg.logDir, `${identity}.log`)).mtimeMs
|
|
357
|
-
} catch {
|
|
358
|
-
return null
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
347
|
function deathsPath(cfg: LifecycleConfig, identity: string): string {
|
|
363
348
|
return join(cfg.stateDir, `${identity}.deaths`)
|
|
364
349
|
}
|
|
@@ -1157,15 +1142,14 @@ export interface SuperviseOutcome {
|
|
|
1157
1142
|
export interface SuperviseDeps {
|
|
1158
1143
|
env?: NodeJS.ProcessEnv
|
|
1159
1144
|
nowMs?: number
|
|
1160
|
-
/**
|
|
1161
|
-
*
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1145
|
+
/** IDLE-proxy seam (default: the runtime adapter's `lastTurnMtime` = content-ts of the last meaningful
|
|
1146
|
+
* transcript entry). This is the RELIABLE idle signal — NOT the transcript FILE mtime (re-saved at
|
|
1147
|
+
* idle) and NOT the pane-log mtime (a statusline re-render ticks it at idle); both falsely keep an
|
|
1148
|
+
* idle session young. Tests inject a controlled value to exercise the idle/quiet age hermetically. */
|
|
1149
|
+
lastTurnMtime?: (runtime: string, cwd: string) => number | null
|
|
1150
|
+
/** Liveness seam (default: the real `sessionAlive` = pty-host pid-file check). Tests inject a fixed
|
|
1151
|
+
* verdict so the idle path is exercised hermetically — no live pty session needed. */
|
|
1165
1152
|
sessionAlive?: (sock: string, identity: string) => boolean
|
|
1166
|
-
/** Pane-log mtime seam (default: the real pane-log file mtime). Tests inject a controlled value to
|
|
1167
|
-
* exercise the ephemeral active-turn vs idle distinction without a real pane-log on disk. */
|
|
1168
|
-
paneLogMtime?: (identity: string) => number | null
|
|
1169
1153
|
}
|
|
1170
1154
|
|
|
1171
1155
|
/** Resolved per-tick context threaded into superviseOnePeer (deps already defaulted by superviseTick). */
|
|
@@ -1173,9 +1157,8 @@ interface SuperviseCtx {
|
|
|
1173
1157
|
cfg: LifecycleConfig
|
|
1174
1158
|
env: NodeJS.ProcessEnv
|
|
1175
1159
|
nowMs: number
|
|
1176
|
-
|
|
1160
|
+
lastTurnMt: (runtime: string, cwd: string) => number | null
|
|
1177
1161
|
isAlive: (sock: string, identity: string) => boolean
|
|
1178
|
-
paneLogMt: (identity: string) => number | null
|
|
1179
1162
|
verbose: boolean
|
|
1180
1163
|
trace: (fields: Record<string, string | number | undefined>) => void
|
|
1181
1164
|
}
|
|
@@ -1188,7 +1171,7 @@ interface SuperviseCtx {
|
|
|
1188
1171
|
* returns exactly one outcome; the trace side-effects mirror the prior inline behaviour.
|
|
1189
1172
|
*/
|
|
1190
1173
|
function superviseOnePeer(s: SessionState, ctx: SuperviseCtx): SuperviseOutcome {
|
|
1191
|
-
const { cfg, env, nowMs,
|
|
1174
|
+
const { cfg, env, nowMs, lastTurnMt, isAlive, verbose, trace } = ctx
|
|
1192
1175
|
// H4 — FIRST, before any reap. A launchd-managed peer is read-only.
|
|
1193
1176
|
if (isLaunchdManaged(s.personality, env)) {
|
|
1194
1177
|
if (verbose) trace({ identity: s.identity, action: 'skipped-launchd', outcome: 'read-only-h4' })
|
|
@@ -1240,34 +1223,24 @@ function superviseOnePeer(s: SessionState, ctx: SuperviseCtx): SuperviseOutcome
|
|
|
1240
1223
|
trace({ identity: s.identity, action: 'reaped-gone', death: gone.death, reason: gone.reason, outcome: 'fresh-next-msg' })
|
|
1241
1224
|
return { identity: s.identity, action: 'reaped-gone', reason: gone.reason }
|
|
1242
1225
|
}
|
|
1243
|
-
// Idle accounting via the
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
// transcript
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1226
|
+
// Idle accounting via the LAST MEANINGFUL TRANSCRIPT ENTRY's content-timestamp (adapter.lastTurnMtime)
|
|
1227
|
+
// — the only RELIABLE idle signal. The two raw mtimes both report FALSE freshness for an idle session:
|
|
1228
|
+
// • pane-log mtime — a statusline / footer re-render writes pty bytes at idle (the clock/usage/ctx%
|
|
1229
|
+
// redraw + Claude Code's own update-check/token-counter), so the pane-log ticks at the prompt. The
|
|
1230
|
+
// 0.4.16 invariant "the pane-log goes quiet ONLY at the prompt" was broken by the statusline.
|
|
1231
|
+
// • transcript FILE mtime — a live claude session RE-SAVES its .jsonl without appending a new entry,
|
|
1232
|
+
// so the file is fresh while the last real turn was hours ago (the 0.4.16 re-touch).
|
|
1233
|
+
// Both made real peers live idle 3-5h UNREAPED (incident 23.06: perplex/mrwriter/doc). The transcript
|
|
1234
|
+
// ENTRY stream only advances on actual turn activity, so the last timestamped entry is the true last
|
|
1235
|
+
// turn. The launch ready-gate keeps newestActivityMtime (file-mtime advance = "a turn produced
|
|
1236
|
+
// output"). FLOORED at wokeAt: a freshly-woken session that produced no turn yet (lastTurn from a PRIOR
|
|
1237
|
+
// session is old) must not be reaped out from under an attaching op — max(lastTurn, wokeAt) reaps it
|
|
1238
|
+
// idleSecs after the wake, never before. null lastTurn (unreadable / churned out of the tail) → wokeAt
|
|
1239
|
+
// governs (an old wokeAt = woken-long-ago = idle → reaped; a recent wokeAt = just-woken → protected).
|
|
1255
1240
|
const ephemeral = isEphemeralPeer(s.cwd)
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
mt = Math.max(plMt, s.wokeAt)
|
|
1260
|
-
} else {
|
|
1261
|
-
// Pane-log MISSING (a legacy pre-pane-log supervisor) → fall back to the transcript
|
|
1262
|
-
// proxy, NEVER to wokeAt-as-activity: a wokeAt-only age would read an ACTIVE legacy
|
|
1263
|
-
// session's idle from its hours-old wokeAt and reap it MID-WORK. The transcript is
|
|
1264
|
-
// false-fresh at idle (so an IDLE legacy peer lingers warm until its next respawn
|
|
1265
|
-
// earns a pane-log — bounded, self-heals), but an ACTIVE legacy peer keeps a
|
|
1266
|
-
// genuinely-fresh transcript and is correctly protected. Safe direction preserved.
|
|
1267
|
-
let tx = 0
|
|
1268
|
-
try { tx = activityMtime(s.runtime, s.cwd) ?? 0 } catch { /* no adapter → wokeAt */ }
|
|
1269
|
-
mt = Math.max(tx, s.wokeAt)
|
|
1270
|
-
}
|
|
1241
|
+
let lt = 0
|
|
1242
|
+
try { lt = lastTurnMt(s.runtime, s.cwd) ?? 0 } catch { /* unreadable → wokeAt floor */ }
|
|
1243
|
+
const mt = Math.max(lt, s.wokeAt)
|
|
1271
1244
|
const ageSecs = Math.floor((nowMs - mt) / 1000)
|
|
1272
1245
|
// wake_policy:ephemeral M2 — die-after-reply: an ARMED ephemeral session (the
|
|
1273
1246
|
// daemon routed its outbound reply) is reaped after a QUIET window, checked
|
|
@@ -1337,9 +1310,8 @@ function superviseOnePeer(s: SessionState, ctx: SuperviseCtx): SuperviseOutcome
|
|
|
1337
1310
|
export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): SuperviseOutcome[] {
|
|
1338
1311
|
const env = deps.env ?? process.env
|
|
1339
1312
|
const nowMs = deps.nowMs ?? Date.now()
|
|
1340
|
-
const
|
|
1313
|
+
const lastTurnMt = deps.lastTurnMtime ?? ((rt: string, c: string) => getAdapter(rt as Runtime).lastTurnMtime(c))
|
|
1341
1314
|
const isAlive = deps.sessionAlive ?? sessionAlive
|
|
1342
|
-
const paneLogMt = deps.paneLogMtime ?? ((identity: string) => paneLogMtimeMs(cfg, identity))
|
|
1343
1315
|
const verbose = superviseLogVerbose(env)
|
|
1344
1316
|
// Pane-log volume cap (launch/cmdlog.ts capPaneLogs): the per-identity pane-log
|
|
1345
1317
|
// (<logDir>/<identity>.log) is the RAW TUI byte stream pipe-pane/the supervisor
|
|
@@ -1356,7 +1328,7 @@ export function superviseTick(cfg: LifecycleConfig, deps: SuperviseDeps = {}): S
|
|
|
1356
1328
|
const trace = (fields: Record<string, string | number | undefined>): void =>
|
|
1357
1329
|
appendLifecycleEvent(cfg.eventLogDir, { ev: 'supervise', ...fields }, { env, nowMs })
|
|
1358
1330
|
const out: SuperviseOutcome[] = []
|
|
1359
|
-
const ctx: SuperviseCtx = { cfg, env, nowMs,
|
|
1331
|
+
const ctx: SuperviseCtx = { cfg, env, nowMs, lastTurnMt, isAlive, verbose, trace }
|
|
1360
1332
|
for (const s of readSessionStates(cfg)) {
|
|
1361
1333
|
// Per-peer ISOLATION: a throw while evaluating ONE peer must NOT abort the whole sweep — that is
|
|
1362
1334
|
// the silent fleet-wide reap-outage class (one malformed peer-state and NO peer gets reaped, the
|