@integrity-labs/agt-cli 0.28.207 → 0.28.209

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.
@@ -1,1634 +0,0 @@
1
- import {
2
- claudeModelAlias,
3
- formatMissingVar,
4
- isClaudeFastMode,
5
- probeMcpEnvSubstitution
6
- } from "./chunk-ELBYWACS.js";
7
- import {
8
- reapOrphanChannelMcps
9
- } from "./chunk-XWVM4KPK.js";
10
-
11
- // src/lib/persistent-session.ts
12
- import { spawn, execSync, execFileSync as execFileSync3 } from "child_process";
13
- import { join as join2, dirname } from "path";
14
- import { homedir as homedir2, platform, userInfo } from "os";
15
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3, appendFileSync, mkdirSync as mkdirSync2, chmodSync, copyFileSync, rmSync } from "fs";
16
-
17
- // src/lib/mcp-sanitize.ts
18
- import { readFileSync, writeFileSync } from "fs";
19
- function sanitizeMcpJson(mcpConfigPath, apiHost) {
20
- try {
21
- const mcpRaw = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
22
- const servers = mcpRaw.mcpServers;
23
- if (!servers) return false;
24
- let changed = false;
25
- for (const [key, val] of Object.entries(servers)) {
26
- if (typeof val?.url !== "string") continue;
27
- if (val.url.startsWith("/")) {
28
- if (apiHost) {
29
- val.url = `${apiHost}${val.url}`;
30
- changed = true;
31
- } else {
32
- delete servers[key];
33
- changed = true;
34
- continue;
35
- }
36
- }
37
- const headers = val.headers;
38
- if (headers && typeof headers === "object" && Object.keys(headers).length > 0) {
39
- if (typeof val.type !== "string") {
40
- val.type = "http";
41
- changed = true;
42
- }
43
- continue;
44
- }
45
- const url = val.url;
46
- delete val.url;
47
- delete val.type;
48
- val.command = "npx";
49
- val.args = ["-y", "mcp-remote", url, "--allow-http"];
50
- changed = true;
51
- }
52
- if (changed) writeFileSync(mcpConfigPath, JSON.stringify(mcpRaw, null, 2));
53
- return changed;
54
- } catch {
55
- return false;
56
- }
57
- }
58
-
59
- // src/lib/claude-tools.ts
60
- var BASE_TOOLS = ["Bash", "Read", "Write", "Edit", "Grep", "Glob", "Agent", "Skill", "ToolSearch"];
61
- function buildAllowedTools(mcpServerNames) {
62
- const mcpPatterns = mcpServerNames.map((name) => `mcp__${name.replace(/-/g, "_")}__*`);
63
- return [...mcpPatterns, ...BASE_TOOLS].join(",");
64
- }
65
-
66
- // src/lib/persistent-session.ts
67
- import { randomUUID as randomUUID2 } from "crypto";
68
-
69
- // src/lib/daily-session.ts
70
- import { randomUUID } from "crypto";
71
- import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, renameSync, statSync, writeFileSync as writeFileSync2 } from "fs";
72
- import { homedir } from "os";
73
- import { join } from "path";
74
- var HISTORY_DAYS = 7;
75
- function profileDir(codeName) {
76
- return join(homedir(), ".augmented", codeName);
77
- }
78
- function dailySessionPath(codeName) {
79
- return join(profileDir(codeName), "daily-session.json");
80
- }
81
- function todayLocalIso(now = /* @__PURE__ */ new Date(), timezone) {
82
- if (timezone) {
83
- try {
84
- const fmt = new Intl.DateTimeFormat("en-CA", {
85
- timeZone: timezone,
86
- year: "numeric",
87
- month: "2-digit",
88
- day: "2-digit"
89
- });
90
- return fmt.format(now);
91
- } catch {
92
- }
93
- }
94
- const y = now.getFullYear();
95
- const m = String(now.getMonth() + 1).padStart(2, "0");
96
- const d = String(now.getDate()).padStart(2, "0");
97
- return `${y}-${m}-${d}`;
98
- }
99
- function readFile(codeName) {
100
- const path = dailySessionPath(codeName);
101
- if (!existsSync(path)) return { current: null, history: [] };
102
- try {
103
- const raw = readFileSync2(path, "utf-8");
104
- const parsed = JSON.parse(raw);
105
- return {
106
- current: parsed.current ?? null,
107
- history: Array.isArray(parsed.history) ? parsed.history : []
108
- };
109
- } catch {
110
- return { current: null, history: [] };
111
- }
112
- }
113
- function writeFile(codeName, data) {
114
- const dir = profileDir(codeName);
115
- mkdirSync(dir, { recursive: true });
116
- const finalPath = dailySessionPath(codeName);
117
- const tmpPath = `${finalPath}.${process.pid}.${randomUUID()}.tmp`;
118
- writeFileSync2(tmpPath, JSON.stringify(data, null, 2), "utf-8");
119
- renameSync(tmpPath, finalPath);
120
- }
121
- function trimHistory(history, now, timezone) {
122
- const cutoff = new Date(now);
123
- cutoff.setDate(cutoff.getDate() - HISTORY_DAYS);
124
- const cutoffIso = todayLocalIso(cutoff, timezone);
125
- return history.filter((h) => h.date >= cutoffIso).slice(0, HISTORY_DAYS);
126
- }
127
- function getOrCreateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone) {
128
- const today = todayLocalIso(now, timezone);
129
- const file = readFile(codeName);
130
- if (file.current && file.current.date === today) {
131
- return { sessionId: file.current.sessionId, isNew: false };
132
- }
133
- const next = {
134
- date: today,
135
- sessionId: randomUUID(),
136
- startedAt: now.toISOString()
137
- };
138
- const history = trimHistory(
139
- [...file.current ? [file.current] : [], ...file.history],
140
- now,
141
- timezone
142
- );
143
- writeFile(codeName, { current: next, history });
144
- return { sessionId: next.sessionId, isNew: true };
145
- }
146
- function markDailySessionSpawn(codeName, sessionId, now = /* @__PURE__ */ new Date(), timezone) {
147
- const today = todayLocalIso(now, timezone);
148
- const file = readFile(codeName);
149
- if (file.current && file.current.date === today && file.current.sessionId === sessionId) {
150
- return;
151
- }
152
- const next = {
153
- date: today,
154
- sessionId,
155
- startedAt: now.toISOString()
156
- };
157
- const history = trimHistory(
158
- [...file.current ? [file.current] : [], ...file.history],
159
- now,
160
- timezone
161
- );
162
- writeFile(codeName, { current: next, history });
163
- }
164
- function rotateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone) {
165
- const today = todayLocalIso(now, timezone);
166
- const file = readFile(codeName);
167
- const next = {
168
- date: today,
169
- sessionId: randomUUID(),
170
- startedAt: now.toISOString()
171
- };
172
- const history = trimHistory(
173
- [...file.current ? [file.current] : [], ...file.history],
174
- now,
175
- timezone
176
- );
177
- writeFile(codeName, { current: next, history });
178
- return next.sessionId;
179
- }
180
- function encodeProjectPath(projectDir) {
181
- return "-" + projectDir.replace(/^\//, "").replace(/[/.]/g, "-");
182
- }
183
- function sessionFileExists(projectDir, sessionId) {
184
- const path = join(
185
- homedir(),
186
- ".claude",
187
- "projects",
188
- encodeProjectPath(projectDir),
189
- `${sessionId}.jsonl`
190
- );
191
- return existsSync(path);
192
- }
193
- function sessionTranscriptDir(projectDir) {
194
- return join(homedir(), ".claude", "projects", encodeProjectPath(projectDir));
195
- }
196
- function sessionFilePath(projectDir, sessionId) {
197
- return join(sessionTranscriptDir(projectDir), `${sessionId}.jsonl`);
198
- }
199
- function transcriptActivityAgeSeconds(projectDir, sessionId, now = /* @__PURE__ */ new Date()) {
200
- if (!sessionId) return null;
201
- try {
202
- const mtimeMs = statSync(sessionFilePath(projectDir, sessionId)).mtimeMs;
203
- return Math.max(0, Math.floor((now.getTime() - mtimeMs) / 1e3));
204
- } catch {
205
- return null;
206
- }
207
- }
208
- function subagentActivityAgeSeconds(projectDir, sessionId, now = /* @__PURE__ */ new Date()) {
209
- if (!sessionId) return null;
210
- const dir = join(sessionTranscriptDir(projectDir), sessionId, "subagents");
211
- try {
212
- let freshestMtimeMs = null;
213
- for (const name of readdirSync(dir)) {
214
- if (!name.endsWith(".jsonl")) continue;
215
- try {
216
- const mtimeMs = statSync(join(dir, name)).mtimeMs;
217
- if (freshestMtimeMs === null || mtimeMs > freshestMtimeMs) freshestMtimeMs = mtimeMs;
218
- } catch {
219
- }
220
- }
221
- if (freshestMtimeMs === null) return null;
222
- return Math.max(0, Math.floor((now.getTime() - freshestMtimeMs) / 1e3));
223
- } catch {
224
- return null;
225
- }
226
- }
227
- function isAgentIdle(projectDir, sessionId, idleSeconds = 60, now = /* @__PURE__ */ new Date()) {
228
- const path = sessionFilePath(projectDir, sessionId);
229
- if (!existsSync(path)) return true;
230
- try {
231
- const mtimeMs = statSync(path).mtimeMs;
232
- return now.getTime() - mtimeMs >= idleSeconds * 1e3;
233
- } catch {
234
- return false;
235
- }
236
- }
237
- function isStaleForToday(codeName, now = /* @__PURE__ */ new Date(), timezone) {
238
- const file = readFile(codeName);
239
- if (!file.current) return false;
240
- return file.current.date !== todayLocalIso(now, timezone);
241
- }
242
- function peekCurrentSession(codeName) {
243
- return readFile(codeName).current;
244
- }
245
-
246
- // ../../packages/core/dist/runtime/session-probe.js
247
- import { execFileSync } from "child_process";
248
- function escapePgrepRegex(value) {
249
- return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
250
- }
251
- function probeClaudeProcessInTmux(tmuxSession) {
252
- const escapedSession = escapePgrepRegex(tmuxSession);
253
- const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
254
- try {
255
- const out = execFileSync("pgrep", ["-f", "--", pattern], {
256
- encoding: "utf-8",
257
- timeout: 3e3
258
- }).trim();
259
- return out.length > 0 ? "alive" : "dead";
260
- } catch (err) {
261
- const e = err;
262
- if (e?.code === "ENOENT")
263
- return "unknown";
264
- return e?.status === 1 ? "dead" : "unknown";
265
- }
266
- }
267
-
268
- // src/lib/claude-dialogs.ts
269
- import { execFileSync as execFileSync2 } from "child_process";
270
- function isLoginPickerVisible(screen) {
271
- return screen.includes("Select login method") || screen.includes("Claude account with subscription") && screen.includes("Anthropic Console account");
272
- }
273
- function isResumeModeDialogVisible(screen) {
274
- return screen.includes("Resume from summary") && screen.includes("Don't ask me again");
275
- }
276
- function isSessionFeedbackDialogVisible(screen) {
277
- return screen.includes("How is Claude doing this session") && screen.includes("0: Dismiss");
278
- }
279
- function sweepDialogs(screen) {
280
- if (screen.includes("Choose the text style") || screen.includes("Dark mode") && screen.includes("Light mode")) {
281
- return {
282
- kind: "theme-picker",
283
- keys: ["Enter"],
284
- interKeyDelayMs: 0,
285
- logMessage: "Auto-accepted theme picker"
286
- };
287
- }
288
- if (screen.includes("Yes, I trust this folder")) {
289
- return {
290
- kind: "folder-trust",
291
- keys: ["Enter"],
292
- interKeyDelayMs: 0,
293
- logMessage: "Auto-accepted folder trust"
294
- };
295
- }
296
- if (isResumeModeDialogVisible(screen)) {
297
- return {
298
- kind: "resume-mode",
299
- keys: ["3", "Enter"],
300
- interKeyDelayMs: 300,
301
- logMessage: "Auto-dismissed resume-mode dialog (picked 'Don't ask me again')"
302
- };
303
- }
304
- if (screen.includes("I am using this for local development")) {
305
- return {
306
- kind: "dev-channels",
307
- keys: ["Enter"],
308
- interKeyDelayMs: 0,
309
- logMessage: "Auto-accepted dev channels"
310
- };
311
- }
312
- if (screen.includes("Enter to confirm") && screen.includes("MCP")) {
313
- return {
314
- kind: "mcp-servers",
315
- keys: ["Enter"],
316
- interKeyDelayMs: 0,
317
- logMessage: "Auto-accepted MCP servers"
318
- };
319
- }
320
- if (screen.includes("Yes, I accept") && screen.includes("Bypass Permissions")) {
321
- return {
322
- kind: "bypass-permissions",
323
- keys: ["2", "Enter"],
324
- interKeyDelayMs: 300,
325
- logMessage: "Auto-accepted bypass permissions"
326
- };
327
- }
328
- if (isSessionFeedbackDialogVisible(screen)) {
329
- return {
330
- kind: "session-feedback",
331
- keys: ["0"],
332
- interKeyDelayMs: 0,
333
- logMessage: "Auto-dismissed session-feedback dialog"
334
- };
335
- }
336
- return null;
337
- }
338
- async function sendDialogKeys(tmuxSession, action) {
339
- for (let i = 0; i < action.keys.length; i++) {
340
- if (i > 0 && action.interKeyDelayMs > 0) {
341
- await new Promise((r) => setTimeout(r, action.interKeyDelayMs));
342
- }
343
- execFileSync2("tmux", ["send-keys", "-t", tmuxSession, action.keys[i]], {
344
- stdio: "ignore"
345
- });
346
- }
347
- }
348
- function simpleTextHash(s) {
349
- let h = 0;
350
- for (let i = 0; i < s.length; i++) {
351
- h = (h << 5) - h + s.charCodeAt(i) | 0;
352
- }
353
- return h.toString(16);
354
- }
355
-
356
- // src/lib/channel-input-watchdog.ts
357
- var STUCK_THRESHOLD_MS = 5e3;
358
- var MAX_ENTER_FIRES = 3;
359
- var DISTURB_HEAL_KEYS = ["x", "BSpace", "Enter"];
360
- var DISTURB_INTER_KEY_DELAY_MS = 200;
361
- var ATTACHED_STUCK_THRESHOLD_MS = 15e3;
362
- var INPUT_BOX_DIVIDER = /^[─━]{10,}/;
363
- var PROMPT_PREFIX = "\u276F ";
364
- function selectFireKeys(attempt, healMode = "disturb") {
365
- if (healMode === "disturb" && attempt >= 2) return DISTURB_HEAL_KEYS;
366
- return ["Enter"];
367
- }
368
- function decide(pane, prev, now, config = {}) {
369
- const threshold = config.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
370
- const maxFires = config.maxEnterFires ?? MAX_ENTER_FIRES;
371
- const dialogAction = sweepDialogs(pane);
372
- if (dialogAction) {
373
- return { fire: false, dialog: dialogAction, next: prev };
374
- }
375
- const inputText = extractInputBoxText(pane);
376
- if (!inputText) {
377
- return { fire: false, next: void 0 };
378
- }
379
- if (inputText === "\u2026") {
380
- return { fire: false, next: void 0 };
381
- }
382
- const hash = simpleTextHash(inputText);
383
- if (!prev || prev.lastInputHash !== hash) {
384
- return {
385
- fire: false,
386
- next: { lastInputHash: hash, firstSeenAt: now, fires: 0, lastFireAt: 0, gaveUpLogged: false }
387
- };
388
- }
389
- if (prev.fires >= maxFires) {
390
- if (!prev.gaveUpLogged) {
391
- return { fire: false, gaveUp: true, next: { ...prev, gaveUpLogged: true } };
392
- }
393
- return { fire: false, next: prev };
394
- }
395
- const sinceLastAttempt = prev.fires === 0 ? now - prev.firstSeenAt : now - prev.lastFireAt;
396
- if (sinceLastAttempt < threshold) return { fire: false, next: prev };
397
- return {
398
- fire: true,
399
- next: { ...prev, fires: prev.fires + 1, lastFireAt: now }
400
- };
401
- }
402
- function extractInputBoxText(pane) {
403
- const lines = pane.split("\n");
404
- for (let i = 1; i < lines.length; i++) {
405
- const line = lines[i] ?? "";
406
- if (!line.startsWith(PROMPT_PREFIX)) continue;
407
- let j = i - 1;
408
- while (j >= 0 && (lines[j] ?? "").trim() === "") j--;
409
- if (j < 0) continue;
410
- if (!INPUT_BOX_DIVIDER.test((lines[j] ?? "").trim())) continue;
411
- const text = line.slice(PROMPT_PREFIX.length).trim();
412
- return text.length > 0 ? text : null;
413
- }
414
- return null;
415
- }
416
- var SPINNER_GLYPHS = ["\u273B", "\u273D", "\u2736", "\u2733", "\u2722"];
417
- function isActivelyProcessing(pane) {
418
- const lines = pane.split("\n");
419
- for (let i = lines.length - 1; i >= 0; i--) {
420
- const line = (lines[i] ?? "").trim();
421
- if (!SPINNER_GLYPHS.some((g) => line.startsWith(g))) continue;
422
- if (/\bfor\s+\d+s\s*$/.test(line)) return false;
423
- if (/\b\w+ing[…\.]{0,3}(\s*\([^)]*\))?\s*$/i.test(line)) return true;
424
- return false;
425
- }
426
- return false;
427
- }
428
- function checkChannelInputs(codeNames, io, config = {}, states = sharedStates) {
429
- const live = new Set(codeNames);
430
- for (const codeName of codeNames) {
431
- try {
432
- checkOne(codeName, io, config, states);
433
- } catch (err) {
434
- io.log(`[channel-input-watchdog] '${codeName}': ${err.message}`);
435
- }
436
- }
437
- for (const key of [...states.keys()]) {
438
- if (!live.has(key)) states.delete(key);
439
- }
440
- for (const key of [...giveUpCounts.keys()]) {
441
- if (!live.has(key)) giveUpCounts.delete(key);
442
- }
443
- }
444
- function checkOne(codeName, io, config, states) {
445
- const pane = io.capturePane(codeName);
446
- if (!pane) {
447
- states.delete(codeName);
448
- return;
449
- }
450
- const attached = io.isClientAttached(codeName);
451
- const effectiveConfig = attached ? {
452
- ...config,
453
- stuckThresholdMs: config.attachedStuckThresholdMs ?? ATTACHED_STUCK_THRESHOLD_MS
454
- } : config;
455
- const prev = states.get(codeName);
456
- const { fire, dialog, gaveUp, next } = decide(pane, prev, io.now(), effectiveConfig);
457
- if (next === void 0) {
458
- states.delete(codeName);
459
- if (prev && prev.fires > 0) {
460
- io.log(
461
- `[channel-input-watchdog] '${codeName}': recovered after ${prev.fires} fire(s) \u2014 input submitted (input_hash=${prev.lastInputHash})`
462
- );
463
- }
464
- } else {
465
- states.set(codeName, next);
466
- }
467
- if (dialog) {
468
- io.log(
469
- `[channel-input-watchdog] '${codeName}': ${dialog.logMessage} (dialog was blocking the input box)`
470
- );
471
- io.sendKeys(codeName, dialog.keys, dialog.interKeyDelayMs);
472
- return;
473
- }
474
- const text = extractInputBoxText(pane) ?? "";
475
- const hash = next?.lastInputHash ?? simpleTextHash(text);
476
- if (gaveUp) {
477
- const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;
478
- io.log(
479
- `[channel-input-watchdog] '${codeName}': GIVING UP after ${maxFires} Enter attempts \u2014 input remains unsubmitted (input_hash=${hash}, len=${text.length})`
480
- );
481
- giveUpCounts.set(codeName, (giveUpCounts.get(codeName) ?? 0) + 1);
482
- try {
483
- io.signalGiveUp?.(codeName);
484
- } catch (err) {
485
- io.log(
486
- `[channel-input-watchdog] '${codeName}': give-up signal write failed: ${err.message}`
487
- );
488
- }
489
- return;
490
- }
491
- if (fire) {
492
- const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;
493
- const attempt = next?.fires ?? 1;
494
- const busy = isActivelyProcessing(pane);
495
- const keys = selectFireKeys(attempt, effectiveConfig.healMode);
496
- if (keys.length === 1 && keys[0] === "Enter") {
497
- io.log(
498
- `[channel-input-watchdog] '${codeName}': stuck channel input \u2014 firing Enter (attempt ${attempt}/${maxFires}, busy=${busy}, input_hash=${hash}, len=${text.length})`
499
- );
500
- io.sendEnter(codeName);
501
- } else {
502
- io.log(
503
- `[channel-input-watchdog] '${codeName}': stuck channel input \u2014 escalating to disturb sequence ${keys.join("\u2192")} (attempt ${attempt}/${maxFires}, busy=${busy}, input_hash=${hash}, len=${text.length})`
504
- );
505
- io.sendKeys(codeName, keys, DISTURB_INTER_KEY_DELAY_MS);
506
- }
507
- }
508
- }
509
- var sharedStates = /* @__PURE__ */ new Map();
510
- var giveUpCounts = /* @__PURE__ */ new Map();
511
- function takeWatchdogGiveUpCount(codeName) {
512
- const count = giveUpCounts.get(codeName) ?? 0;
513
- giveUpCounts.delete(codeName);
514
- return count;
515
- }
516
- function creditWatchdogGiveUpCount(codeName, count) {
517
- if (count <= 0) return;
518
- giveUpCounts.set(codeName, (giveUpCounts.get(codeName) ?? 0) + count);
519
- }
520
-
521
- // src/lib/persistent-session.ts
522
- var OPENROUTER_ANTHROPIC_BASE_URL = "https://openrouter.ai/api";
523
- function syncClaudeCredsToRoot() {
524
- if (platform() !== "linux") return true;
525
- if (typeof process.getuid !== "function" || process.getuid() !== 0) return true;
526
- for (const filename of [".credentials.json", "credentials.json"]) {
527
- if (existsSync2(join2("/root/.claude", filename))) return true;
528
- }
529
- let sourcePath = null;
530
- try {
531
- const entries = readdirSync2("/home", { withFileTypes: true });
532
- outer: for (const entry of entries) {
533
- if (!entry.isDirectory()) continue;
534
- for (const filename of [".credentials.json", "credentials.json"]) {
535
- const candidate = join2("/home", entry.name, ".claude", filename);
536
- if (existsSync2(candidate)) {
537
- sourcePath = candidate;
538
- break outer;
539
- }
540
- }
541
- }
542
- } catch {
543
- }
544
- if (!sourcePath) return false;
545
- const targetDir = "/root/.claude";
546
- const sourceFilename = sourcePath.endsWith("credentials.json") && !sourcePath.endsWith(".credentials.json") ? "credentials.json" : ".credentials.json";
547
- const targetPath = join2(targetDir, sourceFilename);
548
- try {
549
- if (!existsSync2(targetDir)) mkdirSync2(targetDir, { recursive: true, mode: 448 });
550
- copyFileSync(sourcePath, targetPath);
551
- chmodSync(targetPath, 384);
552
- return true;
553
- } catch {
554
- return false;
555
- }
556
- }
557
- function resolveOAuthCredAction(claudeAuthMode, openRouterMode) {
558
- if (openRouterMode) return "sync";
559
- return claudeAuthMode === "api_key" ? "purge" : "sync";
560
- }
561
- var cachedClaudePath = null;
562
- function resolveClaudeBinary() {
563
- if (cachedClaudePath) return cachedClaudePath;
564
- const override = process.env.CLAUDE_PATH;
565
- if (override && existsSync2(override)) {
566
- cachedClaudePath = override;
567
- return override;
568
- }
569
- try {
570
- const out = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
571
- if (out && existsSync2(out)) {
572
- cachedClaudePath = out;
573
- return out;
574
- }
575
- } catch {
576
- }
577
- const candidates = [
578
- "/home/linuxbrew/.linuxbrew/bin/claude",
579
- "/opt/homebrew/bin/claude",
580
- "/usr/local/bin/claude"
581
- ];
582
- for (const p of candidates) {
583
- if (existsSync2(p)) {
584
- cachedClaudePath = p;
585
- return p;
586
- }
587
- }
588
- return "claude";
589
- }
590
- function isolationMode(codeName) {
591
- if (process.env.AGT_ISOLATION !== "docker") return "none";
592
- const allow = (process.env.AGT_ISOLATION_AGENTS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
593
- if (allow.length > 0 && (!codeName || !allow.includes(codeName))) return "none";
594
- return "docker";
595
- }
596
- var EGRESS_BASELINE_DOMAINS = [
597
- ".anthropic.com",
598
- // claude API
599
- "claude.ai",
600
- // subscription auth
601
- ".augmented.team",
602
- // the host/control-plane API
603
- ".slack.com",
604
- // channels (incl. wss)
605
- ".composio.dev"
606
- // composio MCP
607
- ];
608
- function egressMode(codeName) {
609
- if (process.env.AGT_EGRESS !== "allowlist") return "none";
610
- if (isolationMode(codeName) !== "docker") return "none";
611
- return "allowlist";
612
- }
613
- var VALID_EGRESS_DOMAIN = /^\.?([a-z0-9-]+\.)+[a-z0-9-]+$/;
614
- function buildEgressAllowlist(toolsFrontmatter) {
615
- const domains = new Set(EGRESS_BASELINE_DOMAINS);
616
- for (const tool of toolsFrontmatter?.tools ?? []) {
617
- for (const d of tool.network?.allowlist_domains ?? []) {
618
- if (typeof d !== "string") continue;
619
- const norm = d.trim().toLowerCase();
620
- if (VALID_EGRESS_DOMAIN.test(norm)) domains.add(norm);
621
- }
622
- }
623
- return [...domains].sort();
624
- }
625
- function egressAllowlistHostPath(codeName, homeDir) {
626
- const home = homeDir ?? (process.env.HOME?.trim() || homedir2());
627
- return join2(home, ".augmented", "_egress", `${codeName}.txt`);
628
- }
629
- function writeEgressAllowlist(codeName, domains, homeDir) {
630
- const p = egressAllowlistHostPath(codeName, homeDir);
631
- mkdirSync2(dirname(p), { recursive: true });
632
- writeFileSync3(p, domains.join("\n") + "\n", { mode: 420 });
633
- return p;
634
- }
635
- var EGRESS_DOCKER_TIMEOUT_MS = 1e4;
636
- function isNoSuchContainer(err) {
637
- const e = err;
638
- const text = `${e?.stderr?.toString() ?? ""}${e?.message ?? ""}`;
639
- return /no such container|is not running/i.test(text);
640
- }
641
- function reloadEgressSidecar(codeName) {
642
- try {
643
- execFileSync3("docker", ["kill", "--signal=HUP", `agt-squid-${codeName}`], {
644
- stdio: "pipe",
645
- timeout: EGRESS_DOCKER_TIMEOUT_MS
646
- });
647
- return true;
648
- } catch (err) {
649
- if (!isNoSuchContainer(err)) throw err;
650
- return false;
651
- }
652
- }
653
- function restartEgressSidecar(codeName) {
654
- try {
655
- execFileSync3("docker", ["restart", `agt-squid-${codeName}`], {
656
- stdio: "pipe",
657
- timeout: EGRESS_DOCKER_TIMEOUT_MS
658
- });
659
- return true;
660
- } catch (err) {
661
- if (!isNoSuchContainer(err)) throw err;
662
- return false;
663
- }
664
- }
665
- function buildDockerRunCommand(args) {
666
- const { codeName, agentId, wrapperPath, projectDir, homeDir, runId, passApiKey, passOpenRouter, egress } = args;
667
- const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
668
- const agentDir = join2(homeDir, ".augmented", codeName);
669
- const agentIdDir = join2(homeDir, ".augmented", agentId);
670
- const mcpDir = join2(homeDir, ".augmented", "_mcp");
671
- const claudeHome = join2(homeDir, ".claude");
672
- const claudeJson = join2(homeDir, ".claude.json");
673
- const mounts = [
674
- `-v ${q(`${agentDir}:${agentDir}`)}`,
675
- `-v ${q(`${agentIdDir}:${agentIdDir}`)}`,
676
- `-v ${q(`${mcpDir}:${mcpDir}:ro`)}`,
677
- `-v ${q(`${claudeHome}:${claudeHome}`)}`,
678
- `-v ${q(`${claudeJson}:${claudeJson}`)}`
679
- ];
680
- const image = process.env.AGT_ISOLATION_IMAGE || "agt-runtime:latest";
681
- const memory = process.env.AGT_ISOLATION_MEMORY || "2g";
682
- const cpus = process.env.AGT_ISOLATION_CPUS || "1.0";
683
- const pids = process.env.AGT_ISOLATION_PIDS || "512";
684
- const envArgs = [`-e ${q(`HOME=${homeDir}`)}`];
685
- envArgs.push(`-e ${q(`npm_config_cache=${join2(agentDir, ".npm-cache")}`)}`);
686
- if (passApiKey) envArgs.push("-e ANTHROPIC_API_KEY");
687
- if (passOpenRouter) {
688
- envArgs.push("-e ANTHROPIC_BASE_URL");
689
- envArgs.push("-e ANTHROPIC_AUTH_TOKEN");
690
- envArgs.push("-e ANTHROPIC_MODEL");
691
- envArgs.push("-e ANTHROPIC_SMALL_FAST_MODEL");
692
- }
693
- if (runId) envArgs.push(`-e ${q(`AGT_RUN_ID=${runId}`)}`);
694
- envArgs.push("-e AGT_API_KEY");
695
- envArgs.push("-e AGT_HOST");
696
- envArgs.push(`-e ${q(`AGT_AGENT_ID=${agentId}`)}`);
697
- envArgs.push(`-e AGT_DIRECT_CHAT_DOORBELL_ENABLED=true`);
698
- envArgs.push(`-e AGT_IN_CONTAINER=true`);
699
- const egressImage = process.env.AGT_EGRESS_IMAGE || "agt-squid:latest";
700
- const internalNet = `agt-net-${codeName}`;
701
- const squidName = `agt-squid-${codeName}`;
702
- const networkArgs = [];
703
- let egressSetup = "";
704
- if (egress) {
705
- networkArgs.push(`--network ${internalNet}`);
706
- const proxyUrl = `http://${squidName}:3128`;
707
- envArgs.push(`-e ${q(`HTTPS_PROXY=${proxyUrl}`)}`);
708
- envArgs.push(`-e ${q(`HTTP_PROXY=${proxyUrl}`)}`);
709
- envArgs.push(`-e ${q(`NO_PROXY=${squidName},localhost,127.0.0.1`)}`);
710
- egressSetup = [
711
- `docker rm -f ${squidName} agt-${codeName} >/dev/null 2>&1 || true`,
712
- `docker network create agt-egress >/dev/null 2>&1 || true`,
713
- // FAIL-CLOSED: a stale or hand-created `agt-net-<codeName>` that is NOT
714
- // internal would give the agent a route off-net, bypassing the proxy.
715
- // Don't trust create-if-absent - verify the Internal flag and rebuild the
716
- // network when it's missing or wrong.
717
- `{ [ "$(docker network inspect -f '{{.Internal}}' ${internalNet} 2>/dev/null)" = "true" ] || { docker network rm ${internalNet} >/dev/null 2>&1 || true; docker network create --internal ${internalNet} >/dev/null; }; }`,
718
- // squid processes untrusted agent traffic - give it the same hardening as
719
- // the agent container (drop all caps, block privilege escalation). squid
720
- // binds 3128 (>1024) and needs no capabilities; verified it still boots.
721
- `docker run -d --name ${squidName} --network ${internalNet} --restart unless-stopped --memory 128m --cap-drop ALL --security-opt no-new-privileges -v ${q(`${egress.allowlistHostPath}:/etc/squid/allowlist.txt:ro`)} ${q(egressImage)} >/dev/null`,
722
- `docker network connect agt-egress ${squidName} >/dev/null 2>&1`
723
- ].join(" && ");
724
- }
725
- const runCmd = [
726
- "exec docker run --rm -it",
727
- `--name agt-${codeName}`,
728
- `--memory ${memory}`,
729
- `--cpus ${cpus}`,
730
- `--pids-limit ${pids}`,
731
- // Defence in depth (red-team 2026-06-16): drop all Linux capabilities and
732
- // block privilege escalation. claude + node MCP servers need none - verified
733
- // booting clean under these. The mount namespace is the primary boundary;
734
- // these shrink what a container-escape CVE could reach if it ever landed.
735
- "--cap-drop ALL",
736
- "--security-opt no-new-privileges",
737
- ...networkArgs,
738
- ...mounts,
739
- `-w ${q(projectDir)}`,
740
- ...envArgs,
741
- q(image),
742
- q(wrapperPath)
743
- ].join(" ");
744
- return egress ? `${egressSetup} && ${runCmd}` : `docker rm -f agt-${codeName} >/dev/null 2>&1; ${runCmd}`;
745
- }
746
- function writePersistentClaudeWrapper(args) {
747
- const { projectDir, claudeBin, initPrompt, claudeArgsJoined } = args;
748
- const envIntegrationsPath = join2(projectDir, ".env.integrations");
749
- const wrapperPath = join2(projectDir, ".claude", "persistent-claude.sh");
750
- const wrapperLines = [
751
- "#!/usr/bin/env bash",
752
- "set -e",
753
- // IS_SANDBOX=1 lets claude run under root/sudo with
754
- // --dangerously-skip-permissions on dedicated EC2 hosts.
755
- "export IS_SANDBOX=1"
756
- ];
757
- if (existsSync2(envIntegrationsPath)) {
758
- wrapperLines.push(
759
- "set -a",
760
- `source ${JSON.stringify(envIntegrationsPath)}`,
761
- "set +a"
762
- );
763
- }
764
- const initPromptArg = initPrompt ? `${JSON.stringify(initPrompt)} ` : "";
765
- wrapperLines.push(
766
- `exec ${JSON.stringify(claudeBin)} ${initPromptArg}${claudeArgsJoined}`
767
- );
768
- mkdirSync2(join2(projectDir, ".claude"), { recursive: true });
769
- writeFileSync3(wrapperPath, wrapperLines.join("\n") + "\n", { mode: 448 });
770
- chmodSync(wrapperPath, 448);
771
- return wrapperPath;
772
- }
773
- function collectMcpServerNames(mcpConfigPath) {
774
- if (!existsSync2(mcpConfigPath)) return [];
775
- try {
776
- const data = JSON.parse(readFileSync3(mcpConfigPath, "utf-8"));
777
- const servers = data.mcpServers;
778
- return servers ? Object.keys(servers) : [];
779
- } catch {
780
- return [];
781
- }
782
- }
783
- var sessions = /* @__PURE__ */ new Map();
784
- var PANE_LOG_DIR = join2(homedir2(), ".augmented");
785
- var PANE_TAIL_LINES = 20;
786
- function paneLogPath(codeName) {
787
- return join2(PANE_LOG_DIR, codeName, "pane.log");
788
- }
789
- function setupPaneLog(tmuxSession, codeName, log) {
790
- const logPath = paneLogPath(codeName);
791
- try {
792
- mkdirSync2(dirname(logPath), { recursive: true });
793
- appendFileSync(
794
- logPath,
795
- `
796
- --- spawn ${(/* @__PURE__ */ new Date()).toISOString()} (session ${tmuxSession}) ---
797
- `,
798
- "utf-8"
799
- );
800
- execSync(
801
- `tmux pipe-pane -o -t ${tmuxSession} 'cat >> ${logPath.replace(/'/g, `'\\''`)}'`,
802
- { stdio: "ignore" }
803
- );
804
- } catch (err) {
805
- log(`[persistent-session] pipe-pane setup failed for '${codeName}': ${err.message}`);
806
- }
807
- }
808
- function readPaneLogTail(codeName, lines = PANE_TAIL_LINES) {
809
- const logPath = paneLogPath(codeName);
810
- if (!existsSync2(logPath)) return null;
811
- try {
812
- const raw = readFileSync3(logPath, "utf-8");
813
- if (!raw) return null;
814
- const stripped = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
815
- const all = stripped.split("\n").filter((l) => l.length > 0);
816
- return all.slice(-lines).join("\n");
817
- } catch {
818
- return null;
819
- }
820
- }
821
- function detectFailureSignature(tail) {
822
- if (!tail) return "unknown";
823
- if (/Session ID .* is already in use/i.test(tail)) return "session_id_in_use";
824
- return "unknown";
825
- }
826
- function prepareForRespawn(codeName) {
827
- const session = sessions.get(codeName);
828
- if (!session) return null;
829
- const signature = detectFailureSignature(session.lastFailureTail);
830
- if (session.consecutiveSameUuidFailures >= 2) {
831
- const failureCount = session.consecutiveSameUuidFailures;
832
- const newId = rotateDailySession(
833
- codeName,
834
- /* @__PURE__ */ new Date(),
835
- session.agentTimezone ?? void 0
836
- );
837
- session.consecutiveSameUuidFailures = 0;
838
- session.lastFailureSessionId = null;
839
- return `rotated daily-session UUID to ${newId} after ${failureCount} consecutive failures on the same UUID (signature=${signature})`;
840
- }
841
- return null;
842
- }
843
- function rotateSessionForWedge(codeName, now = /* @__PURE__ */ new Date()) {
844
- const session = sessions.get(codeName);
845
- const newId = rotateDailySession(codeName, now, session?.agentTimezone ?? void 0);
846
- if (session) {
847
- session.consecutiveSameUuidFailures = 0;
848
- session.lastFailureSessionId = null;
849
- }
850
- return newId;
851
- }
852
- function getLastFailureContext(codeName) {
853
- const session = sessions.get(codeName);
854
- return {
855
- tail: session?.lastFailureTail ?? null,
856
- signature: detectFailureSignature(session?.lastFailureTail ?? null),
857
- consecutiveSameUuid: session?.consecutiveSameUuidFailures ?? 0,
858
- restartCount: session?.restartCount ?? 0
859
- };
860
- }
861
- function resolveSessionSpawnDecision(args) {
862
- const { codeName, projectDir, agentTimezone } = args;
863
- const now = args.now ?? /* @__PURE__ */ new Date();
864
- const disableFlag = process.env["AGT_DISABLE_SESSION_RESUME"];
865
- const resumeDisabled = disableFlag === "1" || disableFlag?.toLowerCase() === "true";
866
- if (resumeDisabled) {
867
- return { flag: "--session-id", sessionId: randomUUID2(), reason: "resume-disabled" };
868
- }
869
- const daily = getOrCreateDailySession(codeName, now, agentTimezone);
870
- if (!daily.isNew && sessionFileExists(projectDir, daily.sessionId)) {
871
- return { flag: "--resume", sessionId: daily.sessionId, reason: "resume-today" };
872
- }
873
- if (daily.isNew) {
874
- return { flag: "--session-id", sessionId: daily.sessionId, reason: "fresh-new-day" };
875
- }
876
- return {
877
- flag: "--session-id",
878
- sessionId: rotateDailySession(codeName, now, agentTimezone),
879
- reason: "rotated-missing-transcript"
880
- };
881
- }
882
- function directChatSessionStatePath(agentId) {
883
- return join2(homedir2(), ".augmented", agentId, "direct-chat-session.json");
884
- }
885
- function writeDirectChatSessionState(agentId, state) {
886
- const p = directChatSessionStatePath(agentId);
887
- mkdirSync2(dirname(p), { recursive: true });
888
- writeFileSync3(p, JSON.stringify(state));
889
- }
890
- function startPersistentSession(config) {
891
- const existing = sessions.get(config.codeName);
892
- if (existing && existing.status === "running") {
893
- return existing;
894
- }
895
- const restartCount = existing?.restartCount ?? 0;
896
- if (existing?.status === "crashed" && existing.startedAt) {
897
- const backoffMs = Math.min(5e3 * Math.pow(2, restartCount), 6e4);
898
- if (Date.now() - existing.startedAt < backoffMs) {
899
- return existing;
900
- }
901
- }
902
- const session = {
903
- codeName: config.codeName,
904
- startedAt: null,
905
- restartCount,
906
- status: "starting",
907
- currentSessionId: existing?.currentSessionId ?? null,
908
- lastFailureTail: existing?.lastFailureTail ?? null,
909
- lastFailureSessionId: existing?.lastFailureSessionId ?? null,
910
- consecutiveSameUuidFailures: existing?.consecutiveSameUuidFailures ?? 0,
911
- agentTimezone: config.agentTimezone ?? null
912
- };
913
- sessions.set(config.codeName, session);
914
- spawnSession(config, session);
915
- return session;
916
- }
917
- function spawnSession(config, session) {
918
- const { codeName, projectDir, mcpConfigPath, claudeMdPath, channels, devChannels, apiHost, log } = config;
919
- const claudeAuthMode = config.claudeAuthMode ?? "subscription";
920
- const openRouterMode = !!config.openRouter;
921
- const tmuxSession = `agt-${codeName}`;
922
- log(
923
- `[persistent-session] Starting tmux session '${tmuxSession}' for '${codeName}' (auth=${openRouterMode ? "openrouter" : claudeAuthMode})`
924
- );
925
- try {
926
- sanitizeMcpJson(mcpConfigPath, apiHost);
927
- try {
928
- execSync(`tmux kill-session -t ${tmuxSession} 2>/dev/null`, { stdio: "ignore" });
929
- } catch {
930
- }
931
- if (resolveOAuthCredAction(claudeAuthMode, openRouterMode) === "sync") {
932
- const credsSynced = syncClaudeCredsToRoot();
933
- const onLinuxRoot = platform() === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
934
- if (openRouterMode) {
935
- if (credsSynced) {
936
- log(`[persistent-session] OpenRouter mode for '${codeName}' - model=${config.openRouter.model}; inference via OpenRouter, claude.ai OAuth retained for channels.`);
937
- } else if (onLinuxRoot) {
938
- log(`[persistent-session] OpenRouter mode for '${codeName}' - model=${config.openRouter.model}; no claude.ai OAuth creds under /root/.claude or /home/*, so channels (Telegram/Slack/direct-chat) will not load. Inference still works via OpenRouter. Run 'claude /login' on the host to enable channels.`);
939
- }
940
- } else if (!credsSynced && onLinuxRoot) {
941
- log(`[persistent-session] No Claude Code credentials found under /root/.claude or /home/*. Pair via browser from the host page, or run 'claude /login' on the host.`);
942
- }
943
- } else {
944
- const claudeDir = join2(homedir2(), ".claude");
945
- for (const filename of [".credentials.json", "credentials.json"]) {
946
- const p = join2(claudeDir, filename);
947
- if (existsSync2(p)) {
948
- try {
949
- rmSync(p, { force: true });
950
- log(`[persistent-session] Removed ${p} (api_key mode active \u2014 preventing OAuth fallback)`);
951
- } catch {
952
- }
953
- }
954
- }
955
- if (!config.anthropicApiKey) {
956
- log(`[persistent-session] api_key mode but no anthropicApiKey passed. Session will fail auth.`);
957
- }
958
- }
959
- const args = [];
960
- const decision = resolveSessionSpawnDecision({
961
- codeName,
962
- projectDir,
963
- agentTimezone: config.agentTimezone ?? void 0
964
- });
965
- const sessionId = decision.sessionId;
966
- const resuming = decision.flag === "--resume";
967
- args.push(decision.flag, sessionId);
968
- log(
969
- `[persistent-session] ${resuming ? "Resuming" : "Starting"} session ${sessionId} for '${codeName}' (${decision.reason})`
970
- );
971
- try {
972
- markDailySessionSpawn(codeName, sessionId, /* @__PURE__ */ new Date(), config.agentTimezone ?? void 0);
973
- } catch (err) {
974
- log(
975
- `[persistent-session] Failed to update daily-session marker for '${codeName}': ${err.message}`
976
- );
977
- }
978
- try {
979
- writeDirectChatSessionState(config.agentId, {
980
- fresh: !resuming,
981
- sessionId,
982
- startedAtMs: Date.now()
983
- });
984
- } catch (err) {
985
- log(
986
- `[persistent-session] Failed to write direct-chat session state for '${codeName}': ${err.message}`
987
- );
988
- }
989
- if (channels.length > 0) args.push("--channels", ...channels);
990
- if (devChannels.length > 0) args.push("--dangerously-load-development-channels", ...devChannels);
991
- args.push("--mcp-config", mcpConfigPath);
992
- if (existsSync2(claudeMdPath)) args.push("--system-prompt-file", claudeMdPath);
993
- if (!openRouterMode) {
994
- const modelAlias = claudeModelAlias(config.primaryModel);
995
- if (modelAlias) args.push("--model", modelAlias);
996
- }
997
- args.push("--allow-dangerously-skip-permissions");
998
- args.push("--dangerously-skip-permissions");
999
- args.push("--strict-mcp-config");
1000
- args.push("--name", tmuxSession);
1001
- const mcpServerNames = collectMcpServerNames(mcpConfigPath);
1002
- args.push("--allowedTools", buildAllowedTools(mcpServerNames));
1003
- const initPrompt = resuming ? "" : 'You are now online. Say "Ready." and wait for incoming messages. Do not run any tools or load any data until a message arrives.';
1004
- const claudeBin = resolveClaudeBinary();
1005
- const claudeArgsJoined = args.map((a) => a.includes(" ") || a.includes("*") ? JSON.stringify(a) : a).join(" ");
1006
- const wrapperPath = writePersistentClaudeWrapper({
1007
- projectDir,
1008
- claudeBin,
1009
- initPrompt,
1010
- claudeArgsJoined
1011
- });
1012
- const tmuxSessionEnvArgs = [];
1013
- if (openRouterMode) {
1014
- const or = config.openRouter;
1015
- tmuxSessionEnvArgs.push("-e", `ANTHROPIC_BASE_URL=${OPENROUTER_ANTHROPIC_BASE_URL}`);
1016
- tmuxSessionEnvArgs.push("-e", `ANTHROPIC_AUTH_TOKEN=${or.authToken}`);
1017
- tmuxSessionEnvArgs.push("-e", `ANTHROPIC_MODEL=${or.model}`);
1018
- if (or.smallFastModel) {
1019
- tmuxSessionEnvArgs.push("-e", `ANTHROPIC_SMALL_FAST_MODEL=${or.smallFastModel}`);
1020
- }
1021
- } else if (claudeAuthMode === "api_key" && config.anthropicApiKey) {
1022
- tmuxSessionEnvArgs.push("-e", `ANTHROPIC_API_KEY=${config.anthropicApiKey}`);
1023
- }
1024
- const sessionHomeDir = process.env.HOME?.trim() || homedir2();
1025
- let egress;
1026
- if (egressMode(codeName) === "allowlist") {
1027
- const allowlist = config.egressAllowlist ?? buildEgressAllowlist(null);
1028
- const allowlistHostPath = writeEgressAllowlist(codeName, allowlist, sessionHomeDir);
1029
- egress = { allowlistHostPath };
1030
- log(`[persistent-session] egress allowlist for '${codeName}': ${allowlist.length} domains (deny-by-default)`);
1031
- }
1032
- const claudeCmd = isolationMode(codeName) === "docker" ? buildDockerRunCommand({
1033
- codeName,
1034
- agentId: config.agentId,
1035
- wrapperPath,
1036
- projectDir,
1037
- homeDir: sessionHomeDir,
1038
- runId: config.runId ?? void 0,
1039
- passApiKey: !openRouterMode && claudeAuthMode === "api_key" && !!config.anthropicApiKey,
1040
- // ENG-7152: forward the OpenRouter ANTHROPIC_* vars (by name) from the
1041
- // session-shell env into the container, same posture as passApiKey.
1042
- passOpenRouter: openRouterMode,
1043
- egress
1044
- }) : JSON.stringify(wrapperPath);
1045
- const tmuxEnv = {
1046
- ...process.env,
1047
- // Treat empty-string as missing too — `HOME=""` makes ~ resolve
1048
- // to cwd, which is the same broken outcome as no HOME, just
1049
- // better hidden.
1050
- HOME: process.env.HOME?.trim() || homedir2(),
1051
- USER: process.env.USER?.trim() || userInfo().username
1052
- };
1053
- if (config.runId) {
1054
- tmuxEnv["AGT_RUN_ID"] = config.runId;
1055
- }
1056
- const apiKeyEnv = !openRouterMode && claudeAuthMode === "api_key" && config.anthropicApiKey ? { ANTHROPIC_API_KEY: config.anthropicApiKey } : {};
1057
- const openRouterEnv = openRouterMode ? {
1058
- ANTHROPIC_BASE_URL: OPENROUTER_ANTHROPIC_BASE_URL,
1059
- ANTHROPIC_AUTH_TOKEN: config.openRouter.authToken,
1060
- ANTHROPIC_MODEL: config.openRouter.model,
1061
- ...config.openRouter.smallFastModel ? { ANTHROPIC_SMALL_FAST_MODEL: config.openRouter.smallFastModel } : {}
1062
- } : {};
1063
- const probeBaseEnv = isolationMode(codeName) === "docker" ? {
1064
- HOME: tmuxEnv["HOME"],
1065
- ...apiKeyEnv,
1066
- ...openRouterEnv,
1067
- ...config.runId ? { AGT_RUN_ID: config.runId } : {},
1068
- AGT_API_KEY: tmuxEnv["AGT_API_KEY"],
1069
- AGT_HOST: tmuxEnv["AGT_HOST"],
1070
- AGT_AGENT_ID: config.agentId
1071
- } : { ...tmuxEnv, ...apiKeyEnv, ...openRouterEnv };
1072
- for (const f of probeMcpEnvSubstitution({
1073
- mcpConfigPath,
1074
- envIntegrationsPath: join2(projectDir, ".env.integrations"),
1075
- baseEnv: probeBaseEnv
1076
- })) {
1077
- log(`[persistent-session] ${formatMissingVar(f)} agent=${codeName}`);
1078
- }
1079
- const child = spawn("tmux", [
1080
- "new-session",
1081
- "-d",
1082
- "-s",
1083
- tmuxSession,
1084
- "-c",
1085
- projectDir,
1086
- ...tmuxSessionEnvArgs,
1087
- claudeCmd
1088
- ], {
1089
- cwd: projectDir,
1090
- stdio: ["ignore", "pipe", "pipe"],
1091
- env: tmuxEnv
1092
- });
1093
- child.on("close", (code) => {
1094
- if (code !== 0) {
1095
- log(`[persistent-session] Failed to create tmux session for '${codeName}' (exit ${code})`);
1096
- session.status = "crashed";
1097
- session.startedAt = Date.now();
1098
- session.restartCount++;
1099
- return;
1100
- }
1101
- log(`[persistent-session] tmux session '${tmuxSession}' created for '${codeName}'`);
1102
- setupPaneLog(tmuxSession, codeName, log);
1103
- session.currentSessionId = sessionId;
1104
- acceptDialogs(tmuxSession, codeName, log, config.primaryModel ?? null, sessionId).catch(() => {
1105
- });
1106
- });
1107
- child.on("error", (err) => {
1108
- log(`[persistent-session] Failed to start tmux for '${codeName}': ${err.message}`);
1109
- session.status = "crashed";
1110
- session.startedAt = Date.now();
1111
- session.restartCount++;
1112
- });
1113
- session.startedAt = Date.now();
1114
- session.status = "running";
1115
- session.restartCount = 0;
1116
- } catch (err) {
1117
- log(`[persistent-session] Failed to start session for '${codeName}': ${err.message}`);
1118
- session.status = "crashed";
1119
- session.startedAt = Date.now();
1120
- session.restartCount++;
1121
- }
1122
- }
1123
- function hasMcpChildren(tmuxSession) {
1124
- try {
1125
- const claudePidOut = execSync(
1126
- `pgrep -f -- "--name ${tmuxSession}" 2>/dev/null || true`,
1127
- { encoding: "utf-8" }
1128
- ).trim();
1129
- if (!claudePidOut) return false;
1130
- const pids = claudePidOut.split("\n").map((p) => Number(p)).filter((p) => p > 0);
1131
- if (pids.length === 0) return false;
1132
- const claudePid = Math.max(...pids);
1133
- const childrenOut = execSync(
1134
- `pgrep -P ${claudePid} 2>/dev/null || true`,
1135
- { encoding: "utf-8" }
1136
- ).trim();
1137
- if (!childrenOut) return false;
1138
- const childPids = childrenOut.split("\n").map((p) => p.trim()).filter(Boolean);
1139
- for (const cp of childPids) {
1140
- const cmdline = execSync(
1141
- `cat /proc/${cp}/cmdline 2>/dev/null | tr '\\0' ' ' || ps -p ${cp} -o args= 2>/dev/null || true`,
1142
- { encoding: "utf-8" }
1143
- );
1144
- if (/slack-channel\.js|telegram-channel\.js|direct-chat-channel\.js|composio_/i.test(cmdline)) {
1145
- return true;
1146
- }
1147
- }
1148
- return false;
1149
- } catch {
1150
- return false;
1151
- }
1152
- }
1153
- async function acceptDialogs(tmuxSession, codeName, log, primaryModel = null, sessionId = null) {
1154
- let loginPickerReported = false;
1155
- let dialogIterations = 0;
1156
- const MAX_DIALOG_ITERATIONS = 15;
1157
- let loginPickerIterations = 0;
1158
- const MAX_LOGIN_PICKER_ITERATIONS = 450;
1159
- while (dialogIterations < MAX_DIALOG_ITERATIONS && loginPickerIterations < MAX_LOGIN_PICKER_ITERATIONS) {
1160
- await new Promise((r) => setTimeout(r, 2e3));
1161
- try {
1162
- const screen = execSync(`tmux capture-pane -t ${tmuxSession} -p 2>/dev/null`, { encoding: "utf-8" });
1163
- if (isLoginPickerVisible(screen)) {
1164
- if (!loginPickerReported) {
1165
- log(`[persistent-session] CLAUDE LOGIN REQUIRED for '${codeName}' \u2014 agent cannot start until ~/.claude.json is provisioned. Pair via the Hosts page or run 'claude /login' on the host.`);
1166
- loginPickerReported = true;
1167
- }
1168
- loginPickerIterations++;
1169
- continue;
1170
- }
1171
- dialogIterations++;
1172
- const dialogAction = sweepDialogs(screen);
1173
- if (dialogAction) {
1174
- await sendDialogKeys(tmuxSession, dialogAction);
1175
- log(`[persistent-session] ${dialogAction.logMessage} for '${codeName}'`);
1176
- continue;
1177
- }
1178
- if (screen.includes("\u276F") && !screen.includes("Enter to confirm")) {
1179
- if (hasMcpChildren(tmuxSession)) {
1180
- log(`[persistent-session] Session ready for '${codeName}' \u2014 MCP servers spawned`);
1181
- await maybeSendFastMode({
1182
- tmuxSession,
1183
- codeName,
1184
- primaryModel,
1185
- sessionId,
1186
- screen,
1187
- log
1188
- });
1189
- break;
1190
- }
1191
- }
1192
- } catch {
1193
- break;
1194
- }
1195
- }
1196
- }
1197
- async function maybeSendFastMode(ctx) {
1198
- if (!isClaudeFastMode(ctx.primaryModel)) return;
1199
- const sid = ctx.sessionId ?? "unknown";
1200
- const banner = ctx.screen.toLowerCase();
1201
- const hasOpus = banner.includes("opus");
1202
- const hasNonOpus = banner.includes("sonnet") || banner.includes("haiku");
1203
- if (hasNonOpus && !hasOpus) {
1204
- ctx.log(
1205
- `[fast-mode] skip /fast for agent=${ctx.codeName} session=${sid} \u2014 banner shows non-Opus model`
1206
- );
1207
- return;
1208
- }
1209
- if (!hasOpus) {
1210
- ctx.log(
1211
- `[fast-mode] skip /fast for agent=${ctx.codeName} session=${sid} \u2014 Opus not visible in banner`
1212
- );
1213
- return;
1214
- }
1215
- const ok = sendToAgent(ctx.tmuxSession, "/fast");
1216
- if (ok) {
1217
- ctx.log(`[fast-mode] sent /fast for agent=${ctx.codeName} session=${sid}`);
1218
- } else {
1219
- ctx.log(`[fast-mode] failed to send /fast for agent=${ctx.codeName} session=${sid} \u2014 tmux send-keys errored`);
1220
- }
1221
- }
1222
- async function waitForPromptReady(tmuxSession) {
1223
- const deadline = Date.now() + 1e4;
1224
- while (Date.now() < deadline) {
1225
- try {
1226
- const screen = execFileSync3(
1227
- "tmux",
1228
- ["capture-pane", "-t", tmuxSession, "-p"],
1229
- { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
1230
- );
1231
- if (screen.includes("\u276F ")) return true;
1232
- } catch {
1233
- }
1234
- await new Promise((r) => setTimeout(r, 500));
1235
- }
1236
- return false;
1237
- }
1238
- var SEND_KEYS_ENTER_DELAY_MS = 50;
1239
- function sleepBlockingMs(ms) {
1240
- const view = new Int32Array(new SharedArrayBuffer(4));
1241
- Atomics.wait(view, 0, 0, ms);
1242
- }
1243
- function defaultArmSender(tmuxSession, command) {
1244
- try {
1245
- execFileSync3("tmux", ["send-keys", "-t", tmuxSession, "-l", command], {
1246
- stdio: ["ignore", "ignore", "pipe"]
1247
- });
1248
- sleepBlockingMs(SEND_KEYS_ENTER_DELAY_MS);
1249
- execFileSync3("tmux", ["send-keys", "-t", tmuxSession, "Enter"], {
1250
- stdio: ["ignore", "ignore", "pipe"]
1251
- });
1252
- return true;
1253
- } catch {
1254
- return false;
1255
- }
1256
- }
1257
- var armSender = defaultArmSender;
1258
- function defaultPaneCapture(tmuxSession) {
1259
- try {
1260
- return execFileSync3("tmux", ["capture-pane", "-t", tmuxSession, "-p"], {
1261
- encoding: "utf-8",
1262
- stdio: ["ignore", "pipe", "ignore"],
1263
- timeout: 2e3
1264
- });
1265
- } catch {
1266
- return null;
1267
- }
1268
- }
1269
- var paneCapture = defaultPaneCapture;
1270
- var defaultHygieneKeySender = async (tmuxSession, keys, interKeyDelayMs) => {
1271
- for (let i = 0; i < keys.length; i++) {
1272
- if (i > 0 && interKeyDelayMs > 0) {
1273
- await new Promise((r) => setTimeout(r, interKeyDelayMs));
1274
- }
1275
- execFileSync3("tmux", ["send-keys", "-t", tmuxSession, keys[i]], {
1276
- stdio: "ignore"
1277
- });
1278
- }
1279
- };
1280
- var hygieneKeySender = defaultHygieneKeySender;
1281
- async function preSendPaneHygiene(tmuxSession, codeName, log) {
1282
- try {
1283
- let screen = paneCapture(tmuxSession);
1284
- if (screen === null) return;
1285
- const action = sweepDialogs(screen);
1286
- if (action) {
1287
- await hygieneKeySender(tmuxSession, action.keys, action.interKeyDelayMs);
1288
- log(`[inject] ${action.logMessage} for '${codeName}' before injection`);
1289
- await new Promise((r) => setTimeout(r, 300));
1290
- screen = paneCapture(tmuxSession) ?? "";
1291
- }
1292
- const orphan = extractInputBoxText(screen);
1293
- if (orphan) {
1294
- log(
1295
- `[inject] clearing orphaned input for '${codeName}' before injection (input_hash=${simpleTextHash(orphan)}, len=${orphan.length})`
1296
- );
1297
- await hygieneKeySender(tmuxSession, ["C-u"], 0);
1298
- }
1299
- } catch {
1300
- }
1301
- }
1302
- function sendToAgent(tmuxSession, command) {
1303
- return armSender(tmuxSession, command);
1304
- }
1305
- async function isAgentPromptReady(tmuxSession) {
1306
- return waitForPromptReady(tmuxSession);
1307
- }
1308
- var _internals = {
1309
- isLoginPickerVisible,
1310
- isResumeModeDialogVisible,
1311
- detectFailureSignature,
1312
- // ENG-6039 test seams: seed/clear the module-private session map so
1313
- // prepareForRespawn's rotation gate can be exercised without tmux.
1314
- __seedSession(session) {
1315
- sessions.set(session.codeName, session);
1316
- },
1317
- __clearSessions() {
1318
- sessions.clear();
1319
- },
1320
- isClaudeProcessAliveInTmux,
1321
- waitForPromptReady,
1322
- // ENG-5770: exported so the unit test in claude-model-alias.test.ts can
1323
- // exercise the send/skip decision without spawning a real tmux session.
1324
- maybeSendFastMode,
1325
- // Test seam: swap the tmux send-keys path so tests don't have to
1326
- // spawn a real tmux server. ESM module-binding makes
1327
- // `vi.spyOn(module, 'execFileSync')` ineffective for in-module
1328
- // callers, so we route through this shim.
1329
- __setArmSender(fn) {
1330
- armSender = fn ?? defaultArmSender;
1331
- },
1332
- // ENG-6017 test seam: swap the pane capture used by the inject-time
1333
- // hygiene so unit tests can simulate dialog overlays / orphaned input
1334
- // without a tmux server. null restores the real capture.
1335
- __setPaneCapture(fn) {
1336
- paneCapture = fn ?? defaultPaneCapture;
1337
- },
1338
- // ENG-6017 test seam: swap the hygiene key sender (dialog dismissal +
1339
- // C-u clear) so unit tests can assert keystrokes without tmux.
1340
- __setHygieneKeySender(fn) {
1341
- hygieneKeySender = fn ?? defaultHygieneKeySender;
1342
- },
1343
- // Test-only resets so each test starts from a clean slate.
1344
- __resetZombieState() {
1345
- zombieProbeCache.clear();
1346
- pendingZombieDetections.clear();
1347
- },
1348
- __getSessionsMap() {
1349
- return sessions;
1350
- },
1351
- __peekPendingZombie(codeName) {
1352
- return pendingZombieDetections.get(codeName) ?? null;
1353
- }
1354
- };
1355
- async function injectMessage(codeName, type, content, meta, log) {
1356
- return (await injectMessageWithStatus(codeName, type, content, meta, log)).delivered;
1357
- }
1358
- async function injectMessageWithStatus(codeName, type, content, meta, log) {
1359
- const _log = log ?? ((_) => {
1360
- });
1361
- const session = sessions.get(codeName);
1362
- if (!session || session.status !== "running") {
1363
- _log(`[inject] SKIP '${codeName}' \u2014 session ${session ? `status=${session.status}` : "not found in Map"}`);
1364
- return { delivered: false, fallbackUsed: false };
1365
- }
1366
- const prefix = meta?.task_name ? `[Task: ${meta.task_name}] ` : "";
1367
- const text = prefix + content;
1368
- const singleLineText = text.replace(/\s*\n+\s*/g, " ").trim();
1369
- await preSendPaneHygiene(`agt-${codeName}`, codeName, _log);
1370
- const sent = sendToAgent(`agt-${codeName}`, singleLineText);
1371
- if (sent) {
1372
- _log(`[inject] tmux send-keys sent for '${codeName}' \u2014 unverified (delivered=false, fallbackUsed=true)`);
1373
- return { delivered: false, fallbackUsed: true };
1374
- }
1375
- _log(`[inject] tmux send-keys failed for '${codeName}'`);
1376
- return { delivered: false, fallbackUsed: false };
1377
- }
1378
- function stopPersistentSession(codeName, log) {
1379
- const session = sessions.get(codeName);
1380
- if (!session) return;
1381
- log(`[persistent-session] Stopping session for '${codeName}'`);
1382
- session.status = "stopped";
1383
- try {
1384
- execFileSync3("tmux", ["kill-session", "-t", `agt-${codeName}`], { stdio: ["ignore", "ignore", "pipe"] });
1385
- } catch (err) {
1386
- const stderr = (err.stderr ?? "").toString().trim();
1387
- const alreadyGone = /can't find session|no server running/i.test(stderr);
1388
- if (!alreadyGone) {
1389
- log(
1390
- `[persistent-session] WARN tmux kill-session for '${codeName}' failed unexpectedly: ${stderr || err.message} \u2014 the session may still be running (ENG-6174)`
1391
- );
1392
- }
1393
- }
1394
- sessions.delete(codeName);
1395
- setTimeout(() => {
1396
- reapOrphanChannelMcps({ log });
1397
- }, 3e3).unref();
1398
- }
1399
- function getSessionState(codeName) {
1400
- return sessions.get(codeName) ?? null;
1401
- }
1402
- var ZOMBIE_PROBE_TTL_MS = 3e4;
1403
- var ZOMBIE_STARTUP_GRACE_MS = 6e4;
1404
- var zombieProbeCache = /* @__PURE__ */ new Map();
1405
- var pendingZombieDetections = /* @__PURE__ */ new Map();
1406
- function isClaudeProcessAliveInTmux(tmuxSession) {
1407
- const codeName = tmuxSession.replace(/^agt-/, "");
1408
- if (isolationMode(codeName) === "docker") {
1409
- try {
1410
- const out = execFileSync3("docker", ["exec", `agt-${codeName}`, "pgrep", "-f", "claude"], {
1411
- encoding: "utf-8",
1412
- timeout: 8e3
1413
- }).trim();
1414
- return out.length > 0;
1415
- } catch (err) {
1416
- const e = err;
1417
- if (e?.status === 1 || e?.status === 125 || e?.status === 126) return false;
1418
- return true;
1419
- }
1420
- }
1421
- return probeClaudeProcessInTmux(tmuxSession) === "alive";
1422
- }
1423
- function takeZombieDetection(codeName) {
1424
- const record = pendingZombieDetections.get(codeName);
1425
- if (record) pendingZombieDetections.delete(codeName);
1426
- return record ?? null;
1427
- }
1428
- function isSessionHealthy(codeName) {
1429
- const tmuxSession = `agt-${codeName}`;
1430
- try {
1431
- execSync(`tmux has-session -t ${tmuxSession} 2>/dev/null`, { stdio: "ignore" });
1432
- } catch {
1433
- const session2 = sessions.get(codeName);
1434
- if (session2 && session2.status === "running") {
1435
- session2.status = "crashed";
1436
- session2.lastFailureTail = readPaneLogTail(codeName);
1437
- const failedUuid = session2.currentSessionId;
1438
- if (failedUuid && failedUuid === session2.lastFailureSessionId) {
1439
- session2.consecutiveSameUuidFailures += 1;
1440
- } else {
1441
- session2.consecutiveSameUuidFailures = 1;
1442
- }
1443
- session2.lastFailureSessionId = failedUuid;
1444
- }
1445
- return false;
1446
- }
1447
- if (!sessions.has(codeName)) {
1448
- sessions.set(codeName, {
1449
- codeName,
1450
- startedAt: Date.now(),
1451
- restartCount: 0,
1452
- status: "running",
1453
- currentSessionId: null,
1454
- lastFailureTail: null,
1455
- lastFailureSessionId: null,
1456
- consecutiveSameUuidFailures: 0,
1457
- agentTimezone: null
1458
- });
1459
- }
1460
- const session = sessions.get(codeName);
1461
- if (session.status !== "running") {
1462
- session.status = "running";
1463
- }
1464
- const startedAt = session.startedAt;
1465
- const withinGrace = startedAt != null && Date.now() - startedAt < ZOMBIE_STARTUP_GRACE_MS;
1466
- if (!withinGrace) {
1467
- const cached = zombieProbeCache.get(codeName);
1468
- const cacheFresh = cached !== void 0 && Date.now() - cached.at < ZOMBIE_PROBE_TTL_MS;
1469
- const claudeAlive = cacheFresh ? cached.alive : isClaudeProcessAliveInTmux(tmuxSession);
1470
- if (!cacheFresh) {
1471
- zombieProbeCache.set(codeName, { at: Date.now(), alive: claudeAlive });
1472
- }
1473
- if (!claudeAlive) {
1474
- const paneTail = readPaneLogTail(codeName);
1475
- try {
1476
- execFileSync3("tmux", ["kill-session", "-t", tmuxSession], { stdio: "ignore" });
1477
- } catch {
1478
- }
1479
- session.status = "crashed";
1480
- session.lastFailureTail = paneTail;
1481
- const failedUuid = session.currentSessionId;
1482
- if (failedUuid && failedUuid === session.lastFailureSessionId) {
1483
- session.consecutiveSameUuidFailures += 1;
1484
- } else {
1485
- session.consecutiveSameUuidFailures = 1;
1486
- }
1487
- session.lastFailureSessionId = failedUuid;
1488
- if (!pendingZombieDetections.has(codeName)) {
1489
- pendingZombieDetections.set(codeName, {
1490
- codeName,
1491
- tmuxSession,
1492
- detectedAt: Date.now(),
1493
- // Cap pane tail to keep the audit payload bounded.
1494
- paneTail: paneTail ? paneTail.slice(-1e3) : null
1495
- });
1496
- }
1497
- zombieProbeCache.delete(codeName);
1498
- return false;
1499
- }
1500
- }
1501
- return true;
1502
- }
1503
- function resetRestartCount(codeName) {
1504
- const session = sessions.get(codeName);
1505
- if (session) session.restartCount = 0;
1506
- }
1507
- function collectDiagnostics(codeNames) {
1508
- return codeNames.map((codeName) => {
1509
- const session = sessions.get(codeName);
1510
- const tmuxSession = `agt-${codeName}`;
1511
- let tmuxAlive = false;
1512
- let screenCapture = null;
1513
- let launchArgs = null;
1514
- let channelStatus = null;
1515
- try {
1516
- execFileSync3("tmux", ["has-session", "-t", tmuxSession], { stdio: "ignore" });
1517
- tmuxAlive = true;
1518
- } catch {
1519
- }
1520
- if (tmuxAlive) {
1521
- try {
1522
- screenCapture = execFileSync3("tmux", ["capture-pane", "-t", tmuxSession, "-p", "-S", "-30"], {
1523
- encoding: "utf-8",
1524
- timeout: 3e3
1525
- }).trim();
1526
- } catch {
1527
- }
1528
- }
1529
- try {
1530
- const psOutput = execFileSync3("ps", ["aux"], { encoding: "utf-8", timeout: 3e3 });
1531
- const line = psOutput.split("\n").find((l) => l.includes(`agt-${codeName}`) && !l.includes("grep"));
1532
- if (line) {
1533
- const match = line.match(/claude\s+.*/);
1534
- launchArgs = match ? match[0].slice(0, 500) : null;
1535
- }
1536
- } catch {
1537
- }
1538
- if (screenCapture) {
1539
- const recentLines = screenCapture.split("\n").slice(-5).join("\n");
1540
- const isIdle = recentLines.includes("\u276F");
1541
- if (isIdle) {
1542
- if (screenCapture.includes("Channels require claude.ai authentication")) {
1543
- channelStatus = "error: auth required";
1544
- } else {
1545
- channelStatus = "ok";
1546
- }
1547
- } else if (recentLines.includes("CHANNEL_ERROR") || recentLines.includes("CLOSED")) {
1548
- channelStatus = "error: disconnected";
1549
- } else if (recentLines.includes("no MCP server configured")) {
1550
- channelStatus = "error: MCP server not found";
1551
- } else if (recentLines.includes("ignored")) {
1552
- channelStatus = "error: channels ignored";
1553
- } else {
1554
- channelStatus = "ok";
1555
- }
1556
- }
1557
- return {
1558
- codeName,
1559
- status: tmuxAlive ? session?.status ?? "running" : session?.status === "running" ? "crashed" : session?.status ?? "unknown",
1560
- startedAt: session?.startedAt ? new Date(session.startedAt).toISOString() : null,
1561
- restartCount: session?.restartCount ?? 0,
1562
- tmuxAlive,
1563
- screenCapture: screenCapture ? screenCapture.slice(-2e3) : null,
1564
- // limit size
1565
- launchArgs,
1566
- channelStatus
1567
- };
1568
- });
1569
- }
1570
- function stopAllSessions(log) {
1571
- for (const codeName of sessions.keys()) {
1572
- stopPersistentSession(codeName, log);
1573
- }
1574
- }
1575
- async function stopAllSessionsAndWait(log, opts) {
1576
- const codeNames = [...sessions.keys()];
1577
- if (codeNames.length === 0) return;
1578
- for (const codeName of codeNames) {
1579
- stopPersistentSession(codeName, log);
1580
- }
1581
- await new Promise((resolve) => setTimeout(resolve, Math.min(opts.timeoutMs, 2e3)));
1582
- }
1583
- function getProjectDir(codeName) {
1584
- return join2(homedir2(), ".augmented", codeName, "project");
1585
- }
1586
-
1587
- export {
1588
- sessionTranscriptDir,
1589
- transcriptActivityAgeSeconds,
1590
- subagentActivityAgeSeconds,
1591
- isAgentIdle,
1592
- isStaleForToday,
1593
- peekCurrentSession,
1594
- checkChannelInputs,
1595
- takeWatchdogGiveUpCount,
1596
- creditWatchdogGiveUpCount,
1597
- resolveOAuthCredAction,
1598
- resolveClaudeBinary,
1599
- isolationMode,
1600
- EGRESS_BASELINE_DOMAINS,
1601
- egressMode,
1602
- buildEgressAllowlist,
1603
- egressAllowlistHostPath,
1604
- writeEgressAllowlist,
1605
- reloadEgressSidecar,
1606
- restartEgressSidecar,
1607
- buildDockerRunCommand,
1608
- writePersistentClaudeWrapper,
1609
- paneLogPath,
1610
- readPaneLogTail,
1611
- prepareForRespawn,
1612
- rotateSessionForWedge,
1613
- getLastFailureContext,
1614
- resolveSessionSpawnDecision,
1615
- directChatSessionStatePath,
1616
- writeDirectChatSessionState,
1617
- startPersistentSession,
1618
- SEND_KEYS_ENTER_DELAY_MS,
1619
- sendToAgent,
1620
- isAgentPromptReady,
1621
- _internals,
1622
- injectMessage,
1623
- injectMessageWithStatus,
1624
- stopPersistentSession,
1625
- getSessionState,
1626
- takeZombieDetection,
1627
- isSessionHealthy,
1628
- resetRestartCount,
1629
- collectDiagnostics,
1630
- stopAllSessions,
1631
- stopAllSessionsAndWait,
1632
- getProjectDir
1633
- };
1634
- //# sourceMappingURL=chunk-GWBOA7ZY.js.map