@volter/supercode-terminal 0.2.30

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.
@@ -0,0 +1,529 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { runAsync } from "./internal/exec.js";
5
+ import { openLocalTerminal, terminalShellCommand } from "./local-terminal.js";
6
+ import { createSessionRollTracker } from "./session-roll.js";
7
+ import { createLifecycleStore, gatherLifecycleObservation, observeLifecycle, } from "./session-lifecycle.js";
8
+ const CODEX_ROLLOUT_ID = /\/rollout-[^/]*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/iu;
9
+ const CODEX_SESSION_ID = /\bSession:\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/giu;
10
+ const APPLE_FIELD_SEPARATOR = String.fromCharCode(31);
11
+ /**
12
+ * Host-only local terminal capability for embedders.
13
+ *
14
+ * The public surface contains durable agent session ids and semantic state only.
15
+ * Native window ids, tty paths, process ids, AppleScript references, and tmux
16
+ * handles remain private to this process.
17
+ */
18
+ export class NativeTerminalSessionHost {
19
+ lifecycleStore = createLifecycleStore();
20
+ rollTracker;
21
+ observationPending = Promise.resolve();
22
+ claudeConfigDir;
23
+ resolvedSessionIds = new Map();
24
+ sessions = new Map();
25
+ constructor(options = {}) {
26
+ this.claudeConfigDir = options.claudeConfigDir
27
+ ?? (options.home === undefined ? process.env.CLAUDE_CONFIG_DIR : undefined)
28
+ ?? join(options.home ?? homedir(), ".claude");
29
+ this.rollTracker = createSessionRollTracker({ claudeConfigDir: this.claudeConfigDir });
30
+ }
31
+ async listSessions() {
32
+ const result = this.observationPending.then(() => this.observeSessions());
33
+ this.observationPending = result.catch(() => undefined);
34
+ return result;
35
+ }
36
+ async observeSessions() {
37
+ assertSupportedPlatform();
38
+ const [itermRunning, terminalRunning] = await Promise.all([
39
+ applicationRunning("iTerm2"),
40
+ applicationRunning("Terminal"),
41
+ ]);
42
+ const surfaces = (await Promise.all([
43
+ ...(itermRunning ? [listItermSurfaces()] : []),
44
+ ...(terminalRunning ? [listTerminalSurfaces()] : []),
45
+ ])).flat();
46
+ // Both applications share one ownership/evidence batch. A successor live
47
+ // in the other application is not available for adoption.
48
+ const observations = [await this.observeSurfaces(surfaces)];
49
+ const next = new Map();
50
+ const activeIdentityKeys = new Set();
51
+ for (const entries of observations) {
52
+ for (const entry of entries) {
53
+ activeIdentityKeys.add(entry.identityKey);
54
+ next.set(entry.session.agentSessionId, entry);
55
+ }
56
+ }
57
+ for (const key of this.resolvedSessionIds.keys()) {
58
+ if (!activeIdentityKeys.has(key))
59
+ this.resolvedSessionIds.delete(key);
60
+ }
61
+ this.sessions = next;
62
+ return [...next.values()].flatMap((entry) => /^(?:claude|codex):\d+$/u.test(entry.session.agentSessionId) ? [] : [entry.session]);
63
+ }
64
+ async captureSession(agentSessionId, options = {}) {
65
+ const entry = await this.requireSession(agentSessionId);
66
+ const content = await captureSurface(entry.address);
67
+ return options.lines === undefined ? content : content.split("\n").slice(-options.lines).join("\n");
68
+ }
69
+ async openSession(command, options) {
70
+ assertSupportedPlatform();
71
+ if (options.application === "terminal") {
72
+ await openLocalTerminal(command);
73
+ return;
74
+ }
75
+ await runAsync("osascript", ["-e", `
76
+ tell application "iTerm"
77
+ activate
78
+ create window with default profile
79
+ tell current session of current window
80
+ write text ${appleString(terminalShellCommand(command))}
81
+ end tell
82
+ end tell
83
+ `], { timeoutMs: 5_000 });
84
+ }
85
+ hasSessionCandidate(agentSessionId, options) {
86
+ if (this.sessions.has(agentSessionId))
87
+ return true;
88
+ if (!/^codex:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(agentSessionId)) {
89
+ return false;
90
+ }
91
+ return [...this.sessions.values()].some((entry) => /^codex:\d+$/u.test(entry.observedSessionId) && entry.cwd === options.cwd);
92
+ }
93
+ async resolveSession(agentSessionId, options) {
94
+ if (!/^codex:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(agentSessionId)) {
95
+ return null;
96
+ }
97
+ await this.listSessions();
98
+ const candidates = [...this.sessions.values()].filter((entry) => {
99
+ if (entry.session.agentSessionId === agentSessionId)
100
+ return true;
101
+ return /^codex:\d+$/u.test(entry.session.agentSessionId);
102
+ });
103
+ for (const entry of candidates) {
104
+ const pid = pidKeyedAgentPid(entry.observedSessionId, "codex");
105
+ if (pid !== undefined && entry.cwd !== options.cwd)
106
+ continue;
107
+ const resolved = await probeCodexSessionId(entry.address);
108
+ if (`codex:${resolved}` !== agentSessionId)
109
+ continue;
110
+ this.resolvedSessionIds.set(entry.identityKey, agentSessionId);
111
+ const session = Object.freeze({ ...entry.session, agentSessionId });
112
+ this.sessions.delete(entry.session.agentSessionId);
113
+ this.sessions.set(agentSessionId, { ...entry, session });
114
+ return {
115
+ proof: { observedAtMs: Date.now(), source: "native_terminal_status", turn: "idle" },
116
+ session,
117
+ };
118
+ }
119
+ return null;
120
+ }
121
+ async relinquishSession(agentSessionId, proof) {
122
+ if (proof.turn !== "idle" ||
123
+ !proof.source ||
124
+ !Number.isFinite(proof.observedAtMs) ||
125
+ Date.now() - proof.observedAtMs > 3_000) {
126
+ throw new Error("Native session handoff requires a fresh external idle proof.");
127
+ }
128
+ const entry = await this.requireSession(agentSessionId);
129
+ if (entry.session.state === "session_running" || entry.session.state === "session_unrecognized") {
130
+ throw new Error(`Native terminal state for ${agentSessionId} is not quiescent; its writer was left untouched.`);
131
+ }
132
+ if (agentSessionId.startsWith("codex:")) {
133
+ const before = await captureSurface(entry.address);
134
+ if (/(?:^|\n)› \/exit[ \t]*(?:\n|$)/u.test(before)) {
135
+ await submitSurfaceLine(entry.address, "");
136
+ }
137
+ else {
138
+ await submitSurfaceLine(entry.address, "/exit");
139
+ await new Promise((resolve) => setTimeout(resolve, 100));
140
+ const content = await captureSurface(entry.address);
141
+ if (/(?:^|\n)› \/exit[ \t]*(?:\n|$)/u.test(content))
142
+ await submitSurfaceLine(entry.address, "");
143
+ }
144
+ }
145
+ else {
146
+ await submitSurfaceLine(entry.address, "/exit");
147
+ }
148
+ const deadline = Date.now() + 5_000;
149
+ while (Date.now() < deadline) {
150
+ if (!(await this.listSessions()).some((session) => session.agentSessionId === agentSessionId)) {
151
+ return { agentSessionId, released: true };
152
+ }
153
+ await new Promise((resolve) => setTimeout(resolve, 100));
154
+ }
155
+ throw new Error(`Native writer ${agentSessionId} did not exit; do not resume it elsewhere yet.`);
156
+ }
157
+ async requireSession(agentSessionId) {
158
+ await this.listSessions();
159
+ const entry = this.sessions.get(agentSessionId);
160
+ if (!entry)
161
+ throw new Error(`No user-owned native terminal currently owns ${agentSessionId}.`);
162
+ return entry;
163
+ }
164
+ async observeSurfaces(surfaces) {
165
+ const rootPidByTty = new Map();
166
+ for (const entry of await resolveTtyRootPids(surfaces.map((surface) => surface.tty))) {
167
+ rootPidByTty.set(entry.tty, entry.rootPid);
168
+ }
169
+ const panes = await Promise.all(surfaces.flatMap((surface) => {
170
+ const tty = normalizeTty(surface.tty);
171
+ const rootPid = rootPidByTty.get(tty);
172
+ if (!tty || rootPid === undefined)
173
+ return [];
174
+ return [(async () => ({
175
+ contents: await captureSurface(surface),
176
+ paneId: tty,
177
+ rootPid,
178
+ target: tty,
179
+ }))()];
180
+ }));
181
+ const store = this.lifecycleStore;
182
+ const claudeByPid = new Map();
183
+ const claudeCwdByPid = new Map();
184
+ const sessionRollClaims = new Map();
185
+ const { leasesByDir, processRows, startTimes, sessionRolls } = await gatherLifecycleObservation(panes, store, {
186
+ gather: async (ids, detectedIdsByPane) => {
187
+ // Reserve the existing bare-Claude heuristic's candidates before any
188
+ // rollover adoption, including on the first observation. Resolve each
189
+ // PID once and reuse the result below; no second cwd/project scan.
190
+ for (const id of ids) {
191
+ const pid = pidKeyedAgentPid(id, "claude");
192
+ if (pid === undefined || claudeByPid.has(pid))
193
+ continue;
194
+ const cwd = await resolveProcessCwd(pid);
195
+ claudeCwdByPid.set(pid, cwd);
196
+ claudeByPid.set(pid, cwd ? latestClaudeSessionIdForCwd(cwd, this.claudeConfigDir) : undefined);
197
+ }
198
+ for (const [paneId, id] of detectedIdsByPane ?? []) {
199
+ const pid = pidKeyedAgentPid(id, "claude");
200
+ const alias = pid === undefined ? undefined : claudeByPid.get(pid);
201
+ if (alias)
202
+ sessionRollClaims.set(paneId, `claude:${alias}`);
203
+ }
204
+ return this.rollTracker.gather([
205
+ ...ids,
206
+ ...[...claudeByPid.values()].flatMap((id) => id ? [`claude:${id}`] : []),
207
+ ]);
208
+ },
209
+ });
210
+ const lifecycle = observeLifecycle({ io: { leasesByDir, startTimes, sessionRolls, sessionRollClaims }, panes, processRows, store });
211
+ const overrideByTty = await resolvePidKeyedSessionIds(lifecycle.panes, surfaces, claudeByPid);
212
+ const cwdByObservedSession = new Map(await Promise.all(lifecycle.panes.flatMap((pane) => {
213
+ const pid = pidKeyedAgentPid(pane.currentSessionId, "codex")
214
+ ?? pidKeyedAgentPid(pane.currentSessionId, "claude");
215
+ return pane.currentSessionId && pid !== undefined
216
+ ? [(claudeCwdByPid.has(pid) ? Promise.resolve(claudeCwdByPid.get(pid)) : resolveProcessCwd(pid))
217
+ .then((cwd) => [pane.currentSessionId, cwd])]
218
+ : [];
219
+ })));
220
+ const stateById = new Map(lifecycle.sessions.map((session) => [session.sessionId, session.state]));
221
+ const surfaceByTty = new Map(surfaces.map((surface) => [normalizeTty(surface.tty), surface]));
222
+ return lifecycle.panes.flatMap((pane) => {
223
+ if (!pane.currentSessionId)
224
+ return [];
225
+ const address = surfaceByTty.get(normalizeTty(pane.paneId));
226
+ if (!address)
227
+ return [];
228
+ const backend = address.backend;
229
+ const identityKey = `${backend}\0${normalizeTty(pane.paneId)}\0${pane.currentSessionId}`;
230
+ const agentSessionId = this.resolvedSessionIds.get(identityKey)
231
+ ?? overrideByTty.get(normalizeTty(pane.paneId))
232
+ ?? pane.currentSessionId;
233
+ const state = stateById.get(pane.currentSessionId);
234
+ if (!state)
235
+ return [];
236
+ return [{
237
+ address,
238
+ cwd: cwdByObservedSession.get(pane.currentSessionId),
239
+ identityKey,
240
+ observedSessionId: pane.currentSessionId,
241
+ session: Object.freeze({ agentSessionId, backend, state }),
242
+ }];
243
+ });
244
+ }
245
+ }
246
+ async function probeCodexSessionId(surface) {
247
+ const before = await captureSurface(surface);
248
+ const visibleSessionId = [...before.matchAll(CODEX_SESSION_ID)].at(-1)?.[1];
249
+ if (visibleSessionId)
250
+ return visibleSessionId;
251
+ if (!before.includes("› Ask Codex to do anything"))
252
+ return null;
253
+ await submitSurfaceLine(surface, "/status");
254
+ const deadline = Date.now() + 3_000;
255
+ let submittedAutocomplete = false;
256
+ while (Date.now() < deadline) {
257
+ const content = await captureSurface(surface);
258
+ const matches = [...content.matchAll(CODEX_SESSION_ID)];
259
+ const sessionId = matches.at(-1)?.[1];
260
+ if (sessionId)
261
+ return sessionId;
262
+ if (!submittedAutocomplete && (content.includes("/status show current session configuration") ||
263
+ /(?:^|\n)› \/status[ \t]*(?:\n|$)/u.test(content))) {
264
+ submittedAutocomplete = true;
265
+ await submitSurfaceLine(surface, "");
266
+ }
267
+ await new Promise((resolve) => setTimeout(resolve, 100));
268
+ }
269
+ return null;
270
+ }
271
+ async function resolvePidKeyedSessionIds(panes, surfaces, claudeByPid) {
272
+ const ttys = new Set(surfaces.map((surface) => normalizeTty(surface.tty)));
273
+ const overrides = new Map();
274
+ const unresolvedCodex = panes.flatMap((pane) => {
275
+ const pid = pidKeyedAgentPid(pane.currentSessionId, "codex");
276
+ return pid === undefined ? [] : [pid];
277
+ });
278
+ const codexByPid = await resolveCodexSessionIdsByRootPid(unresolvedCodex);
279
+ for (const pane of panes) {
280
+ const tty = normalizeTty(pane.paneId);
281
+ if (!ttys.has(tty))
282
+ continue;
283
+ const codexPid = pidKeyedAgentPid(pane.currentSessionId, "codex");
284
+ const codexId = codexPid === undefined ? undefined : codexByPid.get(codexPid);
285
+ if (codexId) {
286
+ overrides.set(tty, `codex:${codexId}`);
287
+ continue;
288
+ }
289
+ const claudePid = pidKeyedAgentPid(pane.currentSessionId, "claude");
290
+ if (claudePid === undefined)
291
+ continue;
292
+ const claudeId = claudeByPid.get(claudePid);
293
+ if (claudeId)
294
+ overrides.set(tty, `claude:${claudeId}`);
295
+ }
296
+ return overrides;
297
+ }
298
+ async function listTerminalSurfaces() {
299
+ const output = (await runAsync("osascript", ["-e", `
300
+ set sep to ASCII character 31
301
+ tell application "Terminal"
302
+ set output to ""
303
+ repeat with w in windows
304
+ try
305
+ set wname to name of w
306
+ repeat with t in tabs of w
307
+ try
308
+ set output to output & (tty of t) & sep & wname & (ASCII character 10)
309
+ end try
310
+ end repeat
311
+ end try
312
+ end repeat
313
+ return output
314
+ end tell
315
+ `], { timeoutMs: 5_000 })).trim();
316
+ return parseSurfaceLines(output, "native-terminal-app");
317
+ }
318
+ async function listItermSurfaces() {
319
+ const output = (await runAsync("osascript", ["-e", `
320
+ set sep to ASCII character 31
321
+ tell application "iTerm"
322
+ set output to ""
323
+ repeat with w in windows
324
+ try
325
+ set s to current session of current tab of w
326
+ set output to output & (tty of s as text) & sep & (name of w as text) & (ASCII character 10)
327
+ end try
328
+ end repeat
329
+ return output
330
+ end tell
331
+ `], { timeoutMs: 5_000 })).trim();
332
+ return parseSurfaceLines(output, "iterm");
333
+ }
334
+ function parseSurfaceLines(output, backend) {
335
+ if (!output)
336
+ return [];
337
+ return output.split(/\n/u).flatMap((line) => {
338
+ const [tty, name] = line.split(APPLE_FIELD_SEPARATOR);
339
+ return tty ? [{ backend, name: name ?? "", tty: tty.trim() }] : [];
340
+ });
341
+ }
342
+ async function captureSurface(surface) {
343
+ const application = surface.backend === "iterm" ? "iTerm" : "Terminal";
344
+ const contentExpression = surface.backend === "iterm" ? "contents of s" : "contents of tab ti of w";
345
+ return (await runAsync("osascript", ["-e", `
346
+ tell application "${application}"
347
+ ${surface.backend === "iterm" ? `repeat with w in windows
348
+ try
349
+ set s to current session of current tab of w
350
+ if (tty of s as text) is ${appleString(surface.tty)} then return ${contentExpression}
351
+ end try
352
+ end repeat` : `repeat with wi from 1 to count windows
353
+ set w to window wi
354
+ repeat with ti from 1 to count tabs of w
355
+ if (tty of tab ti of w) is ${appleString(surface.tty)} then return ${contentExpression}
356
+ end repeat
357
+ end repeat`}
358
+ return ""
359
+ end tell
360
+ `], { timeoutMs: 5_000 })).replace(/\n+$/u, "");
361
+ }
362
+ async function submitSurfaceLine(surface, text) {
363
+ if (surface.backend === "iterm") {
364
+ await runAsync("osascript", ["-e", `
365
+ tell application "iTerm"
366
+ repeat with w in windows
367
+ try
368
+ set s to current session of current tab of w
369
+ if (tty of s as text) is ${appleString(surface.tty)} then
370
+ tell s to write text ${appleString(text)}
371
+ return
372
+ end if
373
+ end try
374
+ end repeat
375
+ end tell
376
+ `], { timeoutMs: 5_000 });
377
+ return;
378
+ }
379
+ await runAsync("osascript", ["-e", `
380
+ tell application "Terminal"
381
+ repeat with wi from 1 to count windows
382
+ set w to window wi
383
+ repeat with ti from 1 to count tabs of w
384
+ if (tty of tab ti of w) is ${appleString(surface.tty)} then
385
+ do script ${appleString(`${text}\n`)} in tab ti of w
386
+ return
387
+ end if
388
+ end repeat
389
+ end repeat
390
+ end tell
391
+ `], { timeoutMs: 5_000 });
392
+ }
393
+ async function resolveTtyRootPids(ttys) {
394
+ const bare = [...new Set(ttys.map(normalizeTty).filter(Boolean))];
395
+ if (bare.length === 0)
396
+ return [];
397
+ const output = await runAsync("ps", ["-t", bare.join(","), "-o", "pid=,ppid=,tty="], { timeoutMs: 5_000 }).catch(() => "");
398
+ const rowsByTty = new Map();
399
+ for (const line of output.split(/\n/u)) {
400
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)$/u);
401
+ if (!match?.[1] || !match[2] || !match[3])
402
+ continue;
403
+ const rowTty = normalizeTty(match[3]);
404
+ const key = bare.find((candidate) => candidate === rowTty || candidate.endsWith(rowTty) || rowTty.endsWith(candidate));
405
+ if (!key)
406
+ continue;
407
+ const rows = rowsByTty.get(key) ?? [];
408
+ rows.push({ pid: Number(match[1]), ppid: Number(match[2]), tty: key });
409
+ rowsByTty.set(key, rows);
410
+ }
411
+ return [...rowsByTty].flatMap(([tty, rows]) => {
412
+ const pids = new Set(rows.map((row) => row.pid));
413
+ const root = rows.filter((row) => !pids.has(row.ppid)).sort((left, right) => left.pid - right.pid)[0]
414
+ ?? [...rows].sort((left, right) => left.pid - right.pid)[0];
415
+ return root ? [{ rootPid: root.pid, tty }] : [];
416
+ });
417
+ }
418
+ async function resolveProcessCwd(pid) {
419
+ const output = await runAsync("lsof", ["-a", "-d", "cwd", "-p", String(pid), "-Fn"], { timeoutMs: 4_000 }).catch(() => "");
420
+ return output.split(/\n/u).find((line) => line.startsWith("n"))?.slice(1);
421
+ }
422
+ async function resolveCodexSessionIdsByRootPid(rootPids) {
423
+ const roots = [...new Set(rootPids.filter((pid) => Number.isInteger(pid) && pid > 0))];
424
+ if (roots.length === 0)
425
+ return new Map();
426
+ const rows = parseProcessRows(await runAsync("ps", ["-axo", "pid=,ppid=,comm="], { timeoutMs: 5_000 }).catch(() => ""));
427
+ const descendants = new Map(roots.map((root) => [root, collectDescendants(root, rows)]));
428
+ const candidates = rows.filter((row) => [...descendants.values()].some((pids) => pids.has(row.pid)) && /(?:^|\/)codex$/u.test(row.command));
429
+ if (candidates.length === 0)
430
+ return new Map();
431
+ const output = await runAsync("lsof", ["-Fn", "-a", "-p", candidates.map((row) => row.pid).join(",")], { timeoutMs: 5_000 }).catch(() => "");
432
+ const rolloutByPid = parseOpenCodexRollouts(output);
433
+ const resolved = new Map();
434
+ for (const [root, pids] of descendants) {
435
+ for (const pid of pids) {
436
+ const id = rolloutByPid.get(pid);
437
+ if (id) {
438
+ resolved.set(root, id);
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ return resolved;
444
+ }
445
+ function parseOpenCodexRollouts(output) {
446
+ const result = new Map();
447
+ let pid;
448
+ for (const line of output.split("\n")) {
449
+ if (line.startsWith("p")) {
450
+ const value = Number(line.slice(1));
451
+ pid = Number.isInteger(value) && value > 0 ? value : undefined;
452
+ }
453
+ else if (pid !== undefined && line.startsWith("n")) {
454
+ const match = line.slice(1).match(CODEX_ROLLOUT_ID);
455
+ if (match?.[1])
456
+ result.set(pid, match[1]);
457
+ }
458
+ }
459
+ return result;
460
+ }
461
+ function parseProcessRows(output) {
462
+ return output.split("\n").flatMap((line) => {
463
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/u);
464
+ return match?.[1] && match[2] && match[3]
465
+ ? [{ command: match[3], pid: Number(match[1]), ppid: Number(match[2]) }]
466
+ : [];
467
+ });
468
+ }
469
+ function collectDescendants(root, rows) {
470
+ const children = new Map();
471
+ for (const row of rows)
472
+ children.set(row.ppid, [...(children.get(row.ppid) ?? []), row.pid]);
473
+ const result = new Set([root]);
474
+ const queue = [root];
475
+ while (queue.length > 0) {
476
+ const parent = queue.shift();
477
+ if (parent === undefined)
478
+ break;
479
+ for (const child of children.get(parent) ?? []) {
480
+ if (result.has(child))
481
+ continue;
482
+ result.add(child);
483
+ queue.push(child);
484
+ }
485
+ }
486
+ return result;
487
+ }
488
+ function latestClaudeSessionIdForCwd(cwd, claudeConfigDir) {
489
+ const directory = join(claudeConfigDir, "projects", cwd.replace(/[/. ]/gu, "-"));
490
+ let entries;
491
+ try {
492
+ entries = readdirSync(directory);
493
+ }
494
+ catch {
495
+ return undefined;
496
+ }
497
+ let newest;
498
+ for (const entry of entries) {
499
+ if (!entry.endsWith(".jsonl"))
500
+ continue;
501
+ try {
502
+ const mtimeMs = statSync(join(directory, entry)).mtimeMs;
503
+ if (!newest || mtimeMs > newest.mtimeMs)
504
+ newest = { id: entry.slice(0, -6), mtimeMs };
505
+ }
506
+ catch {
507
+ // The transcript may disappear between listing and stat.
508
+ }
509
+ }
510
+ return newest?.id;
511
+ }
512
+ function pidKeyedAgentPid(currentSessionId, agent) {
513
+ const match = currentSessionId?.match(new RegExp(`^${agent}:(\\d+)$`, "u"));
514
+ return match?.[1] ? Number(match[1]) : undefined;
515
+ }
516
+ function normalizeTty(tty) {
517
+ return tty?.trim().replace(/^\/dev\//u, "") ?? "";
518
+ }
519
+ async function applicationRunning(name) {
520
+ const output = await runAsync("pgrep", ["-a", "-x", name], { timeoutMs: 1_000 }).catch(() => "");
521
+ return output.trim().length > 0;
522
+ }
523
+ function appleString(value) {
524
+ return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
525
+ }
526
+ function assertSupportedPlatform() {
527
+ if (process.platform !== "darwin")
528
+ throw new Error(`Native macOS terminals are not supported on ${process.platform}.`);
529
+ }
@@ -0,0 +1,162 @@
1
+ export { clearRollHeadVerdict, createSessionRollTracker, scanTranscriptHead } from "./session-roll.js";
2
+ export type { HeadScan, SessionRollTracker } from "./session-roll.js";
3
+ export type SessionLifecycleState = "session_running" | "session_stopped_background_running" | "session_unrecognized" | "session_waiting";
4
+ export type PaneLifecycle = {
5
+ currentSessionId: string | null;
6
+ paneId: string;
7
+ processes?: PaneProcessLifecycle[];
8
+ rootPid: number;
9
+ sessionIds: string[];
10
+ target: string;
11
+ };
12
+ /** Exact live process inventory rooted at one host-owned pane. This is
13
+ * machine evidence, not an agent-session inference, so non-agent services can
14
+ * retain the same PID/start-time/listener authority as agent descendants. */
15
+ export type PaneProcessLifecycle = {
16
+ args?: string;
17
+ command?: string;
18
+ listeners?: ProcessListener[];
19
+ pgid?: number;
20
+ pid: number;
21
+ ppid?: number;
22
+ processStartTime?: string;
23
+ };
24
+ export type SessionAttentionSignal = "asking" | "errored";
25
+ export type DerivationSource = "agent-declared" | "cooperative" | "launch-context" | "process-tree" | "screen-heuristic" | "transcript";
26
+ export type DerivationConfidence = "high" | "low";
27
+ export type SessionDerivation<TValue = SessionLifecycleState> = {
28
+ confidence: DerivationConfidence;
29
+ disagrees?: DerivationSource[];
30
+ source: DerivationSource;
31
+ value: TValue;
32
+ };
33
+ export type CooperativeSignalEvent = "turn-end" | "turn-start";
34
+ export type CooperativeSignal = {
35
+ atMs: number;
36
+ event: CooperativeSignalEvent;
37
+ };
38
+ export type CooperativeSignalStore = Map<string, CooperativeSignal>;
39
+ export declare const COOPERATIVE_SIGNAL_TTL_MS = 120000;
40
+ export declare function createCooperativeSignalStore(): CooperativeSignalStore;
41
+ export declare function recordCooperativeSignal(store: CooperativeSignalStore, sessionId: string, event: string, nowMs: number): void;
42
+ export type AttentionUrgency = "now" | "soon" | "whenever";
43
+ export type AttentionRequest = {
44
+ detail?: string;
45
+ raisedAt: string;
46
+ reason: string;
47
+ urgency: AttentionUrgency;
48
+ };
49
+ export type AttentionRequestStore = Map<string, AttentionRequest>;
50
+ export declare function createAttentionRequestStore(): AttentionRequestStore;
51
+ export declare function isAttentionUrgency(value: unknown): value is AttentionUrgency;
52
+ export declare function recordAttentionRequest(store: AttentionRequestStore, sessionId: string, request: {
53
+ detail?: string;
54
+ reason: string;
55
+ urgency: AttentionUrgency;
56
+ }, nowMs: number): AttentionRequest;
57
+ export declare function clearAttentionRequest(store: AttentionRequestStore, sessionId: string): void;
58
+ export type SessionLifecycle = {
59
+ sessionId: string;
60
+ agentSessionId?: string;
61
+ harness: string;
62
+ cwd?: string;
63
+ isControl?: boolean;
64
+ attachedPaneId: string | null;
65
+ lastPaneId: string | null;
66
+ firstSeenAt: string;
67
+ lastSeenAt: string;
68
+ state: SessionLifecycleState;
69
+ derivation: SessionDerivation;
70
+ signal?: SessionAttentionSignal;
71
+ mainPid?: number;
72
+ mainProcessStartTime?: string;
73
+ backgroundLeaseDir?: string;
74
+ background: BackgroundWorkItem[];
75
+ attentionRequest?: AttentionRequest;
76
+ };
77
+ export type LifecycleSnapshot = {
78
+ observedAt: string;
79
+ panes: PaneLifecycle[];
80
+ sessions: SessionLifecycle[];
81
+ };
82
+ export type LifecycleStore = {
83
+ derivationUnknownCount: number;
84
+ observeDurationMs: number;
85
+ panes: Map<string, PaneLifecycle>;
86
+ sessions: Map<string, SessionLifecycle>;
87
+ };
88
+ export type PaneProcessObservation = {
89
+ backgroundLeaseDir?: string;
90
+ contents?: string;
91
+ controlCwd?: string;
92
+ isControl?: boolean;
93
+ paneId: string;
94
+ target: string;
95
+ rootPid: number;
96
+ knownAgentSessionId?: string;
97
+ };
98
+ export type ProcessTreeRow = {
99
+ pid: number;
100
+ ppid: number;
101
+ pgid?: number;
102
+ stat?: string;
103
+ command: string;
104
+ args: string;
105
+ listeners?: ProcessListener[];
106
+ processStartTime?: string;
107
+ };
108
+ export type ProcessListener = {
109
+ address: string;
110
+ port: number;
111
+ };
112
+ export type BackgroundWorkItem = {
113
+ args?: string;
114
+ command?: string;
115
+ description?: string;
116
+ listeners?: ProcessListener[];
117
+ pid: number;
118
+ ppid?: number;
119
+ pgid?: number;
120
+ processStartTime?: string;
121
+ source: "descendant" | "explicit-lease" | "leased-descendant";
122
+ };
123
+ type BackgroundLeaseFile = {
124
+ args?: unknown;
125
+ command?: unknown;
126
+ description?: unknown;
127
+ pid?: unknown;
128
+ processStartTime?: unknown;
129
+ };
130
+ export declare function snapshotProcessTreeAsync(rootPids?: number[], options?: {
131
+ additionalPids?: number[];
132
+ includeListeners?: boolean;
133
+ }): Promise<Map<number, ProcessTreeRow>>;
134
+ export declare function snapshotProcessTree(rootPids?: number[]): Map<number, ProcessTreeRow>;
135
+ export declare function snapshotDescendantPids(rootPids: number[]): number[];
136
+ export declare function createLifecycleStore(): LifecycleStore;
137
+ export type ObserveIo = {
138
+ cooperativeSignals?: CooperativeSignalStore;
139
+ leasesByDir: Map<string, BackgroundLeaseFile[]>;
140
+ sessionRolls?: Map<string, string>;
141
+ sessionRollClaims?: ReadonlyMap<string, string>;
142
+ startTimes: Map<number, string>;
143
+ };
144
+ export declare function gatherLifecycleObservation(panes: PaneProcessObservation[], store: LifecycleStore, rollTracker?: {
145
+ gather(sessionIds: string[], detectedIdsByPane?: ReadonlyMap<string, string>): Promise<Map<string, string>>;
146
+ }): Promise<{
147
+ leasesByDir: ObserveIo["leasesByDir"];
148
+ processRows: Map<number, ProcessTreeRow>;
149
+ sessionRolls?: Map<string, string>;
150
+ startTimes: Map<number, string>;
151
+ }>;
152
+ export declare function observeLifecycle(options: {
153
+ io?: ObserveIo;
154
+ now?: Date;
155
+ panes: PaneProcessObservation[];
156
+ processRows: Map<number, ProcessTreeRow>;
157
+ store?: LifecycleStore;
158
+ }): LifecycleSnapshot;
159
+ export declare function classifyAttentionSignal(contents?: string): SessionAttentionSignal | undefined;
160
+ export declare const DERIVATION_DEGRADED_LOG_TAG = "[substrate:lifecycle.derivation-degraded]";
161
+ export declare function gatherProcessStartTimes(): Promise<Map<number, string>>;
162
+ export declare function gatherBackgroundLeases(dirs: Array<string | undefined>): Promise<Map<string, BackgroundLeaseFile[]>>;