@integrity-labs/agt-cli 0.28.208 → 0.28.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agt.js +230 -146
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-3MHPMBN4.js → chunk-G42GLXB6.js} +792 -38
- package/dist/chunk-G42GLXB6.js.map +1 -0
- package/dist/{chunk-ELBYWACS.js → chunk-WFCCBTA6.js} +8463 -6842
- package/dist/chunk-WFCCBTA6.js.map +1 -0
- package/dist/{claude-pair-runtime-Q4Q2OTTV.js → claude-pair-runtime-UUTPSE2V.js} +2 -2
- package/dist/lib/manager-worker.js +90 -796
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/direct-chat-channel.js +11 -3
- package/dist/{persistent-session-6662WBSH.js → persistent-session-Z46SCCQM.js} +2 -3
- package/dist/{responsiveness-probe-TOLTTG6I.js → responsiveness-probe-4MUX7FBH.js} +2 -3
- package/dist/{responsiveness-probe-TOLTTG6I.js.map → responsiveness-probe-4MUX7FBH.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3MHPMBN4.js.map +0 -1
- package/dist/chunk-ELBYWACS.js.map +0 -1
- package/dist/chunk-GWBOA7ZY.js +0 -1634
- package/dist/chunk-GWBOA7ZY.js.map +0 -1
- /package/dist/{claude-pair-runtime-Q4Q2OTTV.js.map → claude-pair-runtime-UUTPSE2V.js.map} +0 -0
- /package/dist/{persistent-session-6662WBSH.js.map → persistent-session-Z46SCCQM.js.map} +0 -0
package/dist/chunk-GWBOA7ZY.js
DELETED
|
@@ -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
|