@integrity-labs/agt-cli 0.27.8 → 0.27.9-test.11
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/assets/impersonate-statusline.sh +7 -2
- package/dist/bin/agt.js +946 -129
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-YSBGIXJG.js → chunk-45I2X4M7.js} +361 -26
- package/dist/chunk-45I2X4M7.js.map +1 -0
- package/dist/{chunk-IB655E5U.js → chunk-N2EBL4PZ.js} +1086 -285
- package/dist/chunk-N2EBL4PZ.js.map +1 -0
- package/dist/{chunk-GN4XPQWJ.js → chunk-SJPAXXTW.js} +535 -94
- package/dist/chunk-SJPAXXTW.js.map +1 -0
- package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-OUGV74LY.js} +2 -2
- package/dist/lib/manager-worker.js +2071 -517
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/direct-chat-channel.js +49 -455
- package/dist/mcp/index.js +232 -137
- package/dist/mcp/slack-channel.js +1379 -856
- package/dist/mcp/teams-channel.js +16018 -0
- package/dist/mcp/telegram-channel.js +1264 -618
- package/dist/{persistent-session-ICYFLUAM.js → persistent-session-G3MR7JVI.js} +5 -3
- package/dist/responsiveness-probe-UHSBETBR.js +74 -0
- package/dist/responsiveness-probe-UHSBETBR.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-GN4XPQWJ.js.map +0 -1
- package/dist/chunk-IB655E5U.js.map +0 -1
- package/dist/chunk-YSBGIXJG.js.map +0 -1
- package/dist/responsiveness-probe-WZNQ2762.js +0 -33
- package/dist/responsiveness-probe-WZNQ2762.js.map +0 -1
- /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-OUGV74LY.js.map} +0 -0
- /package/dist/{persistent-session-ICYFLUAM.js.map → persistent-session-G3MR7JVI.js.map} +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
claudeModelAlias,
|
|
3
3
|
isClaudeFastMode
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-45I2X4M7.js";
|
|
5
5
|
import {
|
|
6
6
|
reapOrphanChannelMcps
|
|
7
7
|
} from "./chunk-XWVM4KPK.js";
|
|
8
8
|
|
|
9
9
|
// src/lib/persistent-session.ts
|
|
10
|
-
import { spawn, execSync, execFileSync } from "child_process";
|
|
10
|
+
import { spawn, execSync, execFileSync as execFileSync3 } from "child_process";
|
|
11
11
|
import { join as join2, dirname } from "path";
|
|
12
12
|
import { homedir as homedir2, platform, userInfo } from "os";
|
|
13
|
-
import { existsSync as
|
|
13
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync3, appendFileSync, mkdirSync as mkdirSync2, chmodSync, copyFileSync, rmSync } from "fs";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
15
|
|
|
16
16
|
// src/lib/mcp-sanitize.ts
|
|
@@ -56,18 +56,94 @@ function sanitizeMcpJson(mcpConfigPath, apiHost) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/lib/claude-tools.ts
|
|
59
|
-
var BASE_TOOLS = ["Bash", "Read", "Write", "Edit", "Grep", "Glob", "Agent", "Skill"];
|
|
59
|
+
var BASE_TOOLS = ["Bash", "Read", "Write", "Edit", "Grep", "Glob", "Agent", "Skill", "ToolSearch"];
|
|
60
60
|
function buildAllowedTools(mcpServerNames) {
|
|
61
61
|
const mcpPatterns = mcpServerNames.map((name) => `mcp__${name.replace(/-/g, "_")}__*`);
|
|
62
62
|
return [...mcpPatterns, ...BASE_TOOLS].join(",");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// src/lib/mcp-env-probe.ts
|
|
66
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
67
|
+
var LATE_BOUND_VARS = /* @__PURE__ */ new Set([
|
|
68
|
+
"AGT_RUN_ID",
|
|
69
|
+
"AGT_TOKEN",
|
|
70
|
+
"ANCHOR_BROWSER_SESSION_ID"
|
|
71
|
+
]);
|
|
72
|
+
var TEMPLATE_VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
73
|
+
function collectVarsFromValue(value, into) {
|
|
74
|
+
if (typeof value === "string") {
|
|
75
|
+
for (const m of value.matchAll(TEMPLATE_VAR_RE)) into.add(m[1]);
|
|
76
|
+
} else if (Array.isArray(value)) {
|
|
77
|
+
for (const v of value) collectVarsFromValue(v, into);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function findMissingSubstitutionVars(mcpConfig, env) {
|
|
81
|
+
const findings = [];
|
|
82
|
+
if (typeof mcpConfig !== "object" || mcpConfig === null) return findings;
|
|
83
|
+
const servers = mcpConfig.mcpServers;
|
|
84
|
+
if (typeof servers !== "object" || servers === null) return findings;
|
|
85
|
+
for (const [server, raw] of Object.entries(servers)) {
|
|
86
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
87
|
+
const entry = raw;
|
|
88
|
+
const vars = /* @__PURE__ */ new Set();
|
|
89
|
+
collectVarsFromValue(entry["command"], vars);
|
|
90
|
+
collectVarsFromValue(entry["args"], vars);
|
|
91
|
+
collectVarsFromValue(entry["url"], vars);
|
|
92
|
+
for (const block of [entry["env"], entry["headers"]]) {
|
|
93
|
+
if (typeof block !== "object" || block === null) continue;
|
|
94
|
+
for (const v of Object.values(block)) collectVarsFromValue(v, vars);
|
|
95
|
+
}
|
|
96
|
+
for (const varName of vars) {
|
|
97
|
+
if (LATE_BOUND_VARS.has(varName)) continue;
|
|
98
|
+
const value = env[varName];
|
|
99
|
+
if (value === void 0) {
|
|
100
|
+
findings.push({ varName, server, state: "unset" });
|
|
101
|
+
} else if (value.trim() === "") {
|
|
102
|
+
findings.push({ varName, server, state: "empty" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return findings;
|
|
107
|
+
}
|
|
108
|
+
function formatMissingVar(f) {
|
|
109
|
+
return `[mcp-env-substitution] missing var=${f.varName} server=${f.server} state=${f.state}`;
|
|
110
|
+
}
|
|
111
|
+
function parseEnvIntegrations(content) {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const line of content.split("\n")) {
|
|
114
|
+
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
115
|
+
const eqIdx = line.indexOf("=");
|
|
116
|
+
const key = line.slice(0, eqIdx);
|
|
117
|
+
let value = line.slice(eqIdx + 1);
|
|
118
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
119
|
+
value = value.slice(1, -1).replaceAll("'\\''", "'");
|
|
120
|
+
}
|
|
121
|
+
out[key] = value;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function probeMcpEnvSubstitution(args) {
|
|
126
|
+
try {
|
|
127
|
+
const config = JSON.parse(readFileSync2(args.mcpConfigPath, "utf-8"));
|
|
128
|
+
let env = args.baseEnv;
|
|
129
|
+
if (args.envIntegrationsPath && existsSync(args.envIntegrationsPath)) {
|
|
130
|
+
env = {
|
|
131
|
+
...args.baseEnv,
|
|
132
|
+
...parseEnvIntegrations(readFileSync2(args.envIntegrationsPath, "utf-8"))
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return findMissingSubstitutionVars(config, env);
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
65
141
|
// src/lib/persistent-session.ts
|
|
66
142
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
67
143
|
|
|
68
144
|
// src/lib/daily-session.ts
|
|
69
145
|
import { randomUUID } from "crypto";
|
|
70
|
-
import { existsSync, mkdirSync, readFileSync as
|
|
146
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
71
147
|
import { homedir } from "os";
|
|
72
148
|
import { join } from "path";
|
|
73
149
|
var HISTORY_DAYS = 7;
|
|
@@ -97,9 +173,9 @@ function todayLocalIso(now = /* @__PURE__ */ new Date(), timezone) {
|
|
|
97
173
|
}
|
|
98
174
|
function readFile(codeName) {
|
|
99
175
|
const path = dailySessionPath(codeName);
|
|
100
|
-
if (!
|
|
176
|
+
if (!existsSync2(path)) return { current: null, history: [] };
|
|
101
177
|
try {
|
|
102
|
-
const raw =
|
|
178
|
+
const raw = readFileSync3(path, "utf-8");
|
|
103
179
|
const parsed = JSON.parse(raw);
|
|
104
180
|
return {
|
|
105
181
|
current: parsed.current ?? null,
|
|
@@ -123,6 +199,25 @@ function trimHistory(history, now, timezone) {
|
|
|
123
199
|
const cutoffIso = todayLocalIso(cutoff, timezone);
|
|
124
200
|
return history.filter((h) => h.date >= cutoffIso).slice(0, HISTORY_DAYS);
|
|
125
201
|
}
|
|
202
|
+
function getOrCreateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone) {
|
|
203
|
+
const today = todayLocalIso(now, timezone);
|
|
204
|
+
const file = readFile(codeName);
|
|
205
|
+
if (file.current && file.current.date === today) {
|
|
206
|
+
return { sessionId: file.current.sessionId, isNew: false };
|
|
207
|
+
}
|
|
208
|
+
const next = {
|
|
209
|
+
date: today,
|
|
210
|
+
sessionId: randomUUID(),
|
|
211
|
+
startedAt: now.toISOString()
|
|
212
|
+
};
|
|
213
|
+
const history = trimHistory(
|
|
214
|
+
[...file.current ? [file.current] : [], ...file.history],
|
|
215
|
+
now,
|
|
216
|
+
timezone
|
|
217
|
+
);
|
|
218
|
+
writeFile(codeName, { current: next, history });
|
|
219
|
+
return { sessionId: next.sessionId, isNew: true };
|
|
220
|
+
}
|
|
126
221
|
function markDailySessionSpawn(codeName, sessionId, now = /* @__PURE__ */ new Date(), timezone) {
|
|
127
222
|
const today = todayLocalIso(now, timezone);
|
|
128
223
|
const file = readFile(codeName);
|
|
@@ -160,6 +255,16 @@ function rotateDailySession(codeName, now = /* @__PURE__ */ new Date(), timezone
|
|
|
160
255
|
function encodeProjectPath(projectDir) {
|
|
161
256
|
return "-" + projectDir.replace(/^\//, "").replace(/[/.]/g, "-");
|
|
162
257
|
}
|
|
258
|
+
function sessionFileExists(projectDir, sessionId) {
|
|
259
|
+
const path = join(
|
|
260
|
+
homedir(),
|
|
261
|
+
".claude",
|
|
262
|
+
"projects",
|
|
263
|
+
encodeProjectPath(projectDir),
|
|
264
|
+
`${sessionId}.jsonl`
|
|
265
|
+
);
|
|
266
|
+
return existsSync2(path);
|
|
267
|
+
}
|
|
163
268
|
function sessionTranscriptDir(projectDir) {
|
|
164
269
|
return join(homedir(), ".claude", "projects", encodeProjectPath(projectDir));
|
|
165
270
|
}
|
|
@@ -168,7 +273,7 @@ function sessionFilePath(projectDir, sessionId) {
|
|
|
168
273
|
}
|
|
169
274
|
function isAgentIdle(projectDir, sessionId, idleSeconds = 60, now = /* @__PURE__ */ new Date()) {
|
|
170
275
|
const path = sessionFilePath(projectDir, sessionId);
|
|
171
|
-
if (!
|
|
276
|
+
if (!existsSync2(path)) return true;
|
|
172
277
|
try {
|
|
173
278
|
const mtimeMs = statSync(path).mtimeMs;
|
|
174
279
|
return now.getTime() - mtimeMs >= idleSeconds * 1e3;
|
|
@@ -185,12 +290,287 @@ function peekCurrentSession(codeName) {
|
|
|
185
290
|
return readFile(codeName).current;
|
|
186
291
|
}
|
|
187
292
|
|
|
293
|
+
// ../../packages/core/dist/runtime/session-probe.js
|
|
294
|
+
import { execFileSync } from "child_process";
|
|
295
|
+
function escapePgrepRegex(value) {
|
|
296
|
+
return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
|
|
297
|
+
}
|
|
298
|
+
function probeClaudeProcessInTmux(tmuxSession) {
|
|
299
|
+
const escapedSession = escapePgrepRegex(tmuxSession);
|
|
300
|
+
const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
|
|
301
|
+
try {
|
|
302
|
+
const out = execFileSync("pgrep", ["-f", "--", pattern], {
|
|
303
|
+
encoding: "utf-8",
|
|
304
|
+
timeout: 3e3
|
|
305
|
+
}).trim();
|
|
306
|
+
return out.length > 0 ? "alive" : "dead";
|
|
307
|
+
} catch (err) {
|
|
308
|
+
const e = err;
|
|
309
|
+
if (e?.code === "ENOENT")
|
|
310
|
+
return "unknown";
|
|
311
|
+
return e?.status === 1 ? "dead" : "unknown";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/lib/claude-dialogs.ts
|
|
316
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
317
|
+
function isLoginPickerVisible(screen) {
|
|
318
|
+
return screen.includes("Select login method") || screen.includes("Claude account with subscription") && screen.includes("Anthropic Console account");
|
|
319
|
+
}
|
|
320
|
+
function isResumeModeDialogVisible(screen) {
|
|
321
|
+
return screen.includes("Resume from summary") && screen.includes("Don't ask me again");
|
|
322
|
+
}
|
|
323
|
+
function isSessionFeedbackDialogVisible(screen) {
|
|
324
|
+
return screen.includes("How is Claude doing this session") && screen.includes("0: Dismiss");
|
|
325
|
+
}
|
|
326
|
+
function sweepDialogs(screen) {
|
|
327
|
+
if (screen.includes("Choose the text style") || screen.includes("Dark mode") && screen.includes("Light mode")) {
|
|
328
|
+
return {
|
|
329
|
+
kind: "theme-picker",
|
|
330
|
+
keys: ["Enter"],
|
|
331
|
+
interKeyDelayMs: 0,
|
|
332
|
+
logMessage: "Auto-accepted theme picker"
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (screen.includes("Yes, I trust this folder")) {
|
|
336
|
+
return {
|
|
337
|
+
kind: "folder-trust",
|
|
338
|
+
keys: ["Enter"],
|
|
339
|
+
interKeyDelayMs: 0,
|
|
340
|
+
logMessage: "Auto-accepted folder trust"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (isResumeModeDialogVisible(screen)) {
|
|
344
|
+
return {
|
|
345
|
+
kind: "resume-mode",
|
|
346
|
+
keys: ["3", "Enter"],
|
|
347
|
+
interKeyDelayMs: 300,
|
|
348
|
+
logMessage: "Auto-dismissed resume-mode dialog (picked 'Don't ask me again')"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (screen.includes("I am using this for local development")) {
|
|
352
|
+
return {
|
|
353
|
+
kind: "dev-channels",
|
|
354
|
+
keys: ["Enter"],
|
|
355
|
+
interKeyDelayMs: 0,
|
|
356
|
+
logMessage: "Auto-accepted dev channels"
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (screen.includes("Enter to confirm") && screen.includes("MCP")) {
|
|
360
|
+
return {
|
|
361
|
+
kind: "mcp-servers",
|
|
362
|
+
keys: ["Enter"],
|
|
363
|
+
interKeyDelayMs: 0,
|
|
364
|
+
logMessage: "Auto-accepted MCP servers"
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (screen.includes("Yes, I accept") && screen.includes("Bypass Permissions")) {
|
|
368
|
+
return {
|
|
369
|
+
kind: "bypass-permissions",
|
|
370
|
+
keys: ["2", "Enter"],
|
|
371
|
+
interKeyDelayMs: 300,
|
|
372
|
+
logMessage: "Auto-accepted bypass permissions"
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (isSessionFeedbackDialogVisible(screen)) {
|
|
376
|
+
return {
|
|
377
|
+
kind: "session-feedback",
|
|
378
|
+
keys: ["0"],
|
|
379
|
+
interKeyDelayMs: 0,
|
|
380
|
+
logMessage: "Auto-dismissed session-feedback dialog"
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
async function sendDialogKeys(tmuxSession, action) {
|
|
386
|
+
for (let i = 0; i < action.keys.length; i++) {
|
|
387
|
+
if (i > 0 && action.interKeyDelayMs > 0) {
|
|
388
|
+
await new Promise((r) => setTimeout(r, action.interKeyDelayMs));
|
|
389
|
+
}
|
|
390
|
+
execFileSync2("tmux", ["send-keys", "-t", tmuxSession, action.keys[i]], {
|
|
391
|
+
stdio: "ignore"
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function simpleTextHash(s) {
|
|
396
|
+
let h = 0;
|
|
397
|
+
for (let i = 0; i < s.length; i++) {
|
|
398
|
+
h = (h << 5) - h + s.charCodeAt(i) | 0;
|
|
399
|
+
}
|
|
400
|
+
return h.toString(16);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/lib/channel-input-watchdog.ts
|
|
404
|
+
var STUCK_THRESHOLD_MS = 5e3;
|
|
405
|
+
var MAX_ENTER_FIRES = 3;
|
|
406
|
+
var DISTURB_HEAL_KEYS = ["x", "BSpace", "Enter"];
|
|
407
|
+
var DISTURB_INTER_KEY_DELAY_MS = 200;
|
|
408
|
+
var ATTACHED_STUCK_THRESHOLD_MS = 15e3;
|
|
409
|
+
var INPUT_BOX_DIVIDER = /^[─━]{10,}/;
|
|
410
|
+
var PROMPT_PREFIX = "\u276F ";
|
|
411
|
+
function selectFireKeys(attempt, healMode = "disturb") {
|
|
412
|
+
if (healMode === "disturb" && attempt >= 2) return DISTURB_HEAL_KEYS;
|
|
413
|
+
return ["Enter"];
|
|
414
|
+
}
|
|
415
|
+
function decide(pane, prev, now, config = {}) {
|
|
416
|
+
const threshold = config.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
|
|
417
|
+
const maxFires = config.maxEnterFires ?? MAX_ENTER_FIRES;
|
|
418
|
+
const dialogAction = sweepDialogs(pane);
|
|
419
|
+
if (dialogAction) {
|
|
420
|
+
return { fire: false, dialog: dialogAction, next: prev };
|
|
421
|
+
}
|
|
422
|
+
const inputText = extractInputBoxText(pane);
|
|
423
|
+
if (!inputText) {
|
|
424
|
+
return { fire: false, next: void 0 };
|
|
425
|
+
}
|
|
426
|
+
if (inputText === "\u2026") {
|
|
427
|
+
return { fire: false, next: void 0 };
|
|
428
|
+
}
|
|
429
|
+
const hash = simpleTextHash(inputText);
|
|
430
|
+
if (!prev || prev.lastInputHash !== hash) {
|
|
431
|
+
return {
|
|
432
|
+
fire: false,
|
|
433
|
+
next: { lastInputHash: hash, firstSeenAt: now, fires: 0, lastFireAt: 0, gaveUpLogged: false }
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (prev.fires >= maxFires) {
|
|
437
|
+
if (!prev.gaveUpLogged) {
|
|
438
|
+
return { fire: false, gaveUp: true, next: { ...prev, gaveUpLogged: true } };
|
|
439
|
+
}
|
|
440
|
+
return { fire: false, next: prev };
|
|
441
|
+
}
|
|
442
|
+
const sinceLastAttempt = prev.fires === 0 ? now - prev.firstSeenAt : now - prev.lastFireAt;
|
|
443
|
+
if (sinceLastAttempt < threshold) return { fire: false, next: prev };
|
|
444
|
+
return {
|
|
445
|
+
fire: true,
|
|
446
|
+
next: { ...prev, fires: prev.fires + 1, lastFireAt: now }
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function extractInputBoxText(pane) {
|
|
450
|
+
const lines = pane.split("\n");
|
|
451
|
+
for (let i = 1; i < lines.length; i++) {
|
|
452
|
+
const line = lines[i] ?? "";
|
|
453
|
+
if (!line.startsWith(PROMPT_PREFIX)) continue;
|
|
454
|
+
let j = i - 1;
|
|
455
|
+
while (j >= 0 && (lines[j] ?? "").trim() === "") j--;
|
|
456
|
+
if (j < 0) continue;
|
|
457
|
+
if (!INPUT_BOX_DIVIDER.test((lines[j] ?? "").trim())) continue;
|
|
458
|
+
const text = line.slice(PROMPT_PREFIX.length).trim();
|
|
459
|
+
return text.length > 0 ? text : null;
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
var SPINNER_GLYPHS = ["\u273B", "\u273D", "\u2736", "\u2733", "\u2722"];
|
|
464
|
+
function isActivelyProcessing(pane) {
|
|
465
|
+
const lines = pane.split("\n");
|
|
466
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
467
|
+
const line = (lines[i] ?? "").trim();
|
|
468
|
+
if (!SPINNER_GLYPHS.some((g) => line.startsWith(g))) continue;
|
|
469
|
+
if (/\bfor\s+\d+s\s*$/.test(line)) return false;
|
|
470
|
+
if (/\b\w+ing[…\.]{0,3}(\s*\([^)]*\))?\s*$/i.test(line)) return true;
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
function checkChannelInputs(codeNames, io, config = {}, states = sharedStates) {
|
|
476
|
+
const live = new Set(codeNames);
|
|
477
|
+
for (const codeName of codeNames) {
|
|
478
|
+
try {
|
|
479
|
+
checkOne(codeName, io, config, states);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
io.log(`[channel-input-watchdog] '${codeName}': ${err.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
for (const key of [...states.keys()]) {
|
|
485
|
+
if (!live.has(key)) states.delete(key);
|
|
486
|
+
}
|
|
487
|
+
for (const key of [...giveUpCounts.keys()]) {
|
|
488
|
+
if (!live.has(key)) giveUpCounts.delete(key);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function checkOne(codeName, io, config, states) {
|
|
492
|
+
const pane = io.capturePane(codeName);
|
|
493
|
+
if (!pane) {
|
|
494
|
+
states.delete(codeName);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const attached = io.isClientAttached(codeName);
|
|
498
|
+
const effectiveConfig = attached ? {
|
|
499
|
+
...config,
|
|
500
|
+
stuckThresholdMs: config.attachedStuckThresholdMs ?? ATTACHED_STUCK_THRESHOLD_MS
|
|
501
|
+
} : config;
|
|
502
|
+
const prev = states.get(codeName);
|
|
503
|
+
const { fire, dialog, gaveUp, next } = decide(pane, prev, io.now(), effectiveConfig);
|
|
504
|
+
if (next === void 0) {
|
|
505
|
+
states.delete(codeName);
|
|
506
|
+
if (prev && prev.fires > 0) {
|
|
507
|
+
io.log(
|
|
508
|
+
`[channel-input-watchdog] '${codeName}': recovered after ${prev.fires} fire(s) \u2014 input submitted (input_hash=${prev.lastInputHash})`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
states.set(codeName, next);
|
|
513
|
+
}
|
|
514
|
+
if (dialog) {
|
|
515
|
+
io.log(
|
|
516
|
+
`[channel-input-watchdog] '${codeName}': ${dialog.logMessage} (dialog was blocking the input box)`
|
|
517
|
+
);
|
|
518
|
+
io.sendKeys(codeName, dialog.keys, dialog.interKeyDelayMs);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const text = extractInputBoxText(pane) ?? "";
|
|
522
|
+
const hash = next?.lastInputHash ?? simpleTextHash(text);
|
|
523
|
+
if (gaveUp) {
|
|
524
|
+
const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;
|
|
525
|
+
io.log(
|
|
526
|
+
`[channel-input-watchdog] '${codeName}': GIVING UP after ${maxFires} Enter attempts \u2014 input remains unsubmitted (input_hash=${hash}, len=${text.length})`
|
|
527
|
+
);
|
|
528
|
+
giveUpCounts.set(codeName, (giveUpCounts.get(codeName) ?? 0) + 1);
|
|
529
|
+
try {
|
|
530
|
+
io.signalGiveUp?.(codeName);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
io.log(
|
|
533
|
+
`[channel-input-watchdog] '${codeName}': give-up signal write failed: ${err.message}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (fire) {
|
|
539
|
+
const maxFires = effectiveConfig.maxEnterFires ?? MAX_ENTER_FIRES;
|
|
540
|
+
const attempt = next?.fires ?? 1;
|
|
541
|
+
const busy = isActivelyProcessing(pane);
|
|
542
|
+
const keys = selectFireKeys(attempt, effectiveConfig.healMode);
|
|
543
|
+
if (keys.length === 1 && keys[0] === "Enter") {
|
|
544
|
+
io.log(
|
|
545
|
+
`[channel-input-watchdog] '${codeName}': stuck channel input \u2014 firing Enter (attempt ${attempt}/${maxFires}, busy=${busy}, input_hash=${hash}, len=${text.length})`
|
|
546
|
+
);
|
|
547
|
+
io.sendEnter(codeName);
|
|
548
|
+
} else {
|
|
549
|
+
io.log(
|
|
550
|
+
`[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})`
|
|
551
|
+
);
|
|
552
|
+
io.sendKeys(codeName, keys, DISTURB_INTER_KEY_DELAY_MS);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
var sharedStates = /* @__PURE__ */ new Map();
|
|
557
|
+
var giveUpCounts = /* @__PURE__ */ new Map();
|
|
558
|
+
function takeWatchdogGiveUpCount(codeName) {
|
|
559
|
+
const count = giveUpCounts.get(codeName) ?? 0;
|
|
560
|
+
giveUpCounts.delete(codeName);
|
|
561
|
+
return count;
|
|
562
|
+
}
|
|
563
|
+
function creditWatchdogGiveUpCount(codeName, count) {
|
|
564
|
+
if (count <= 0) return;
|
|
565
|
+
giveUpCounts.set(codeName, (giveUpCounts.get(codeName) ?? 0) + count);
|
|
566
|
+
}
|
|
567
|
+
|
|
188
568
|
// src/lib/persistent-session.ts
|
|
189
569
|
function syncClaudeCredsToRoot() {
|
|
190
570
|
if (platform() !== "linux") return true;
|
|
191
571
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) return true;
|
|
192
572
|
for (const filename of [".credentials.json", "credentials.json"]) {
|
|
193
|
-
if (
|
|
573
|
+
if (existsSync3(join2("/root/.claude", filename))) return true;
|
|
194
574
|
}
|
|
195
575
|
let sourcePath = null;
|
|
196
576
|
try {
|
|
@@ -199,7 +579,7 @@ function syncClaudeCredsToRoot() {
|
|
|
199
579
|
if (!entry.isDirectory()) continue;
|
|
200
580
|
for (const filename of [".credentials.json", "credentials.json"]) {
|
|
201
581
|
const candidate = join2("/home", entry.name, ".claude", filename);
|
|
202
|
-
if (
|
|
582
|
+
if (existsSync3(candidate)) {
|
|
203
583
|
sourcePath = candidate;
|
|
204
584
|
break outer;
|
|
205
585
|
}
|
|
@@ -212,7 +592,7 @@ function syncClaudeCredsToRoot() {
|
|
|
212
592
|
const sourceFilename = sourcePath.endsWith("credentials.json") && !sourcePath.endsWith(".credentials.json") ? "credentials.json" : ".credentials.json";
|
|
213
593
|
const targetPath = join2(targetDir, sourceFilename);
|
|
214
594
|
try {
|
|
215
|
-
if (!
|
|
595
|
+
if (!existsSync3(targetDir)) mkdirSync2(targetDir, { recursive: true, mode: 448 });
|
|
216
596
|
copyFileSync(sourcePath, targetPath);
|
|
217
597
|
chmodSync(targetPath, 384);
|
|
218
598
|
return true;
|
|
@@ -224,13 +604,13 @@ var cachedClaudePath = null;
|
|
|
224
604
|
function resolveClaudeBinary() {
|
|
225
605
|
if (cachedClaudePath) return cachedClaudePath;
|
|
226
606
|
const override = process.env.CLAUDE_PATH;
|
|
227
|
-
if (override &&
|
|
607
|
+
if (override && existsSync3(override)) {
|
|
228
608
|
cachedClaudePath = override;
|
|
229
609
|
return override;
|
|
230
610
|
}
|
|
231
611
|
try {
|
|
232
612
|
const out = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
233
|
-
if (out &&
|
|
613
|
+
if (out && existsSync3(out)) {
|
|
234
614
|
cachedClaudePath = out;
|
|
235
615
|
return out;
|
|
236
616
|
}
|
|
@@ -242,7 +622,7 @@ function resolveClaudeBinary() {
|
|
|
242
622
|
"/usr/local/bin/claude"
|
|
243
623
|
];
|
|
244
624
|
for (const p of candidates) {
|
|
245
|
-
if (
|
|
625
|
+
if (existsSync3(p)) {
|
|
246
626
|
cachedClaudePath = p;
|
|
247
627
|
return p;
|
|
248
628
|
}
|
|
@@ -260,7 +640,7 @@ function writePersistentClaudeWrapper(args) {
|
|
|
260
640
|
// --dangerously-skip-permissions on dedicated EC2 hosts.
|
|
261
641
|
"export IS_SANDBOX=1"
|
|
262
642
|
];
|
|
263
|
-
if (
|
|
643
|
+
if (existsSync3(envIntegrationsPath)) {
|
|
264
644
|
wrapperLines.push(
|
|
265
645
|
"set -a",
|
|
266
646
|
`source ${JSON.stringify(envIntegrationsPath)}`,
|
|
@@ -277,9 +657,9 @@ function writePersistentClaudeWrapper(args) {
|
|
|
277
657
|
return wrapperPath;
|
|
278
658
|
}
|
|
279
659
|
function collectMcpServerNames(mcpConfigPath) {
|
|
280
|
-
if (!
|
|
660
|
+
if (!existsSync3(mcpConfigPath)) return [];
|
|
281
661
|
try {
|
|
282
|
-
const data = JSON.parse(
|
|
662
|
+
const data = JSON.parse(readFileSync4(mcpConfigPath, "utf-8"));
|
|
283
663
|
const servers = data.mcpServers;
|
|
284
664
|
return servers ? Object.keys(servers) : [];
|
|
285
665
|
} catch {
|
|
@@ -293,7 +673,7 @@ function getAcpxBin() {
|
|
|
293
673
|
let dir = moduleDir;
|
|
294
674
|
for (let i = 0; i < 6; i++) {
|
|
295
675
|
const candidate = join2(dir, "node_modules", ".bin", "acpx");
|
|
296
|
-
if (
|
|
676
|
+
if (existsSync3(candidate)) {
|
|
297
677
|
_acpxBin = candidate;
|
|
298
678
|
return _acpxBin;
|
|
299
679
|
}
|
|
@@ -311,7 +691,7 @@ function getAcpxBin() {
|
|
|
311
691
|
}
|
|
312
692
|
function defaultAcpxSessionProbe(acpxBin, projectDir) {
|
|
313
693
|
try {
|
|
314
|
-
|
|
694
|
+
execFileSync3(acpxBin, ["claude", "list-sessions"], {
|
|
315
695
|
cwd: projectDir,
|
|
316
696
|
timeout: 5e3,
|
|
317
697
|
stdio: "ignore"
|
|
@@ -357,9 +737,9 @@ function setupPaneLog(tmuxSession, codeName, log) {
|
|
|
357
737
|
}
|
|
358
738
|
function readPaneLogTail(codeName, lines = PANE_TAIL_LINES) {
|
|
359
739
|
const logPath = paneLogPath(codeName);
|
|
360
|
-
if (!
|
|
740
|
+
if (!existsSync3(logPath)) return null;
|
|
361
741
|
try {
|
|
362
|
-
const raw =
|
|
742
|
+
const raw = readFileSync4(logPath, "utf-8");
|
|
363
743
|
if (!raw) return null;
|
|
364
744
|
const stripped = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
365
745
|
const all = stripped.split("\n").filter((l) => l.length > 0);
|
|
@@ -377,7 +757,7 @@ function prepareForRespawn(codeName) {
|
|
|
377
757
|
const session = sessions.get(codeName);
|
|
378
758
|
if (!session) return null;
|
|
379
759
|
const signature = detectFailureSignature(session.lastFailureTail);
|
|
380
|
-
if (
|
|
760
|
+
if (session.consecutiveSameUuidFailures >= 2) {
|
|
381
761
|
const failureCount = session.consecutiveSameUuidFailures;
|
|
382
762
|
const newId = rotateDailySession(
|
|
383
763
|
codeName,
|
|
@@ -386,7 +766,7 @@ function prepareForRespawn(codeName) {
|
|
|
386
766
|
);
|
|
387
767
|
session.consecutiveSameUuidFailures = 0;
|
|
388
768
|
session.lastFailureSessionId = null;
|
|
389
|
-
return `rotated daily-session UUID to ${newId} after ${failureCount}
|
|
769
|
+
return `rotated daily-session UUID to ${newId} after ${failureCount} consecutive failures on the same UUID (signature=${signature})`;
|
|
390
770
|
}
|
|
391
771
|
return null;
|
|
392
772
|
}
|
|
@@ -399,6 +779,27 @@ function getLastFailureContext(codeName) {
|
|
|
399
779
|
restartCount: session?.restartCount ?? 0
|
|
400
780
|
};
|
|
401
781
|
}
|
|
782
|
+
function resolveSessionSpawnDecision(args) {
|
|
783
|
+
const { codeName, projectDir, agentTimezone } = args;
|
|
784
|
+
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
785
|
+
const disableFlag = process.env["AGT_DISABLE_SESSION_RESUME"];
|
|
786
|
+
const resumeDisabled = disableFlag === "1" || disableFlag?.toLowerCase() === "true";
|
|
787
|
+
if (resumeDisabled) {
|
|
788
|
+
return { flag: "--session-id", sessionId: randomUUID2(), reason: "resume-disabled" };
|
|
789
|
+
}
|
|
790
|
+
const daily = getOrCreateDailySession(codeName, now, agentTimezone);
|
|
791
|
+
if (!daily.isNew && sessionFileExists(projectDir, daily.sessionId)) {
|
|
792
|
+
return { flag: "--resume", sessionId: daily.sessionId, reason: "resume-today" };
|
|
793
|
+
}
|
|
794
|
+
if (daily.isNew) {
|
|
795
|
+
return { flag: "--session-id", sessionId: daily.sessionId, reason: "fresh-new-day" };
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
flag: "--session-id",
|
|
799
|
+
sessionId: rotateDailySession(codeName, now, agentTimezone),
|
|
800
|
+
reason: "rotated-missing-transcript"
|
|
801
|
+
};
|
|
802
|
+
}
|
|
402
803
|
function startPersistentSession(config) {
|
|
403
804
|
const existing = sessions.get(config.codeName);
|
|
404
805
|
if (existing && existing.status === "running") {
|
|
@@ -447,7 +848,7 @@ function spawnSession(config, session) {
|
|
|
447
848
|
const claudeDir = join2(homedir2(), ".claude");
|
|
448
849
|
for (const filename of [".credentials.json", "credentials.json"]) {
|
|
449
850
|
const p = join2(claudeDir, filename);
|
|
450
|
-
if (
|
|
851
|
+
if (existsSync3(p)) {
|
|
451
852
|
try {
|
|
452
853
|
rmSync(p, { force: true });
|
|
453
854
|
log(`[persistent-session] Removed ${p} (api_key mode active \u2014 preventing OAuth fallback)`);
|
|
@@ -460,9 +861,17 @@ function spawnSession(config, session) {
|
|
|
460
861
|
}
|
|
461
862
|
}
|
|
462
863
|
const args = [];
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
864
|
+
const decision = resolveSessionSpawnDecision({
|
|
865
|
+
codeName,
|
|
866
|
+
projectDir,
|
|
867
|
+
agentTimezone: config.agentTimezone ?? void 0
|
|
868
|
+
});
|
|
869
|
+
const sessionId = decision.sessionId;
|
|
870
|
+
const resuming = decision.flag === "--resume";
|
|
871
|
+
args.push(decision.flag, sessionId);
|
|
872
|
+
log(
|
|
873
|
+
`[persistent-session] ${resuming ? "Resuming" : "Starting"} session ${sessionId} for '${codeName}' (${decision.reason})`
|
|
874
|
+
);
|
|
466
875
|
try {
|
|
467
876
|
markDailySessionSpawn(codeName, sessionId, /* @__PURE__ */ new Date(), config.agentTimezone ?? void 0);
|
|
468
877
|
} catch (err) {
|
|
@@ -473,7 +882,7 @@ function spawnSession(config, session) {
|
|
|
473
882
|
if (channels.length > 0) args.push("--channels", ...channels);
|
|
474
883
|
if (devChannels.length > 0) args.push("--dangerously-load-development-channels", ...devChannels);
|
|
475
884
|
args.push("--mcp-config", mcpConfigPath);
|
|
476
|
-
if (
|
|
885
|
+
if (existsSync3(claudeMdPath)) args.push("--system-prompt-file", claudeMdPath);
|
|
477
886
|
const modelAlias = claudeModelAlias(config.primaryModel);
|
|
478
887
|
if (modelAlias) args.push("--model", modelAlias);
|
|
479
888
|
args.push("--allow-dangerously-skip-permissions");
|
|
@@ -482,7 +891,7 @@ function spawnSession(config, session) {
|
|
|
482
891
|
args.push("--name", tmuxSession);
|
|
483
892
|
const mcpServerNames = collectMcpServerNames(mcpConfigPath);
|
|
484
893
|
args.push("--allowedTools", buildAllowedTools(mcpServerNames));
|
|
485
|
-
const initPrompt = 'You are now online. Say "Ready." and wait for incoming messages. Do not run any tools or load any data until a message arrives.';
|
|
894
|
+
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.';
|
|
486
895
|
const claudeBin = resolveClaudeBinary();
|
|
487
896
|
const claudeArgsJoined = args.map((a) => a.includes(" ") || a.includes("*") ? JSON.stringify(a) : a).join(" ");
|
|
488
897
|
const wrapperPath = writePersistentClaudeWrapper({
|
|
@@ -507,6 +916,16 @@ function spawnSession(config, session) {
|
|
|
507
916
|
if (config.runId) {
|
|
508
917
|
tmuxEnv["AGT_RUN_ID"] = config.runId;
|
|
509
918
|
}
|
|
919
|
+
for (const f of probeMcpEnvSubstitution({
|
|
920
|
+
mcpConfigPath,
|
|
921
|
+
envIntegrationsPath: join2(projectDir, ".env.integrations"),
|
|
922
|
+
baseEnv: {
|
|
923
|
+
...tmuxEnv,
|
|
924
|
+
...claudeAuthMode === "api_key" && config.anthropicApiKey ? { ANTHROPIC_API_KEY: config.anthropicApiKey } : {}
|
|
925
|
+
}
|
|
926
|
+
})) {
|
|
927
|
+
log(`[persistent-session] ${formatMissingVar(f)} agent=${codeName}`);
|
|
928
|
+
}
|
|
510
929
|
const child = spawn("tmux", [
|
|
511
930
|
"new-session",
|
|
512
931
|
"-d",
|
|
@@ -551,12 +970,6 @@ function spawnSession(config, session) {
|
|
|
551
970
|
session.restartCount++;
|
|
552
971
|
}
|
|
553
972
|
}
|
|
554
|
-
function isLoginPickerVisible(screen) {
|
|
555
|
-
return screen.includes("Select login method") || screen.includes("Claude account with subscription") && screen.includes("Anthropic Console account");
|
|
556
|
-
}
|
|
557
|
-
function isResumeModeDialogVisible(screen) {
|
|
558
|
-
return screen.includes("Resume from summary") && screen.includes("Don't ask me again");
|
|
559
|
-
}
|
|
560
973
|
function hasMcpChildren(tmuxSession) {
|
|
561
974
|
try {
|
|
562
975
|
const claudePidOut = execSync(
|
|
@@ -606,40 +1019,10 @@ async function acceptDialogs(tmuxSession, codeName, log, primaryModel = null, se
|
|
|
606
1019
|
continue;
|
|
607
1020
|
}
|
|
608
1021
|
dialogIterations++;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
if (screen.includes("Yes, I trust this folder")) {
|
|
615
|
-
execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
|
|
616
|
-
log(`[persistent-session] Auto-accepted workspace trust for '${codeName}'`);
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
if (isResumeModeDialogVisible(screen)) {
|
|
620
|
-
execFileSync("tmux", ["send-keys", "-t", tmuxSession, "3"], { stdio: "ignore" });
|
|
621
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
622
|
-
execFileSync("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { stdio: "ignore" });
|
|
623
|
-
log(
|
|
624
|
-
`[persistent-session] Auto-dismissed resume-mode dialog for '${codeName}' (picked 'Don't ask me again')`
|
|
625
|
-
);
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
if (screen.includes("I am using this for local development")) {
|
|
629
|
-
execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
|
|
630
|
-
log(`[persistent-session] Auto-accepted dev channels for '${codeName}'`);
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
if (screen.includes("Enter to confirm") && screen.includes("MCP")) {
|
|
634
|
-
execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
|
|
635
|
-
log(`[persistent-session] Auto-accepted MCP servers for '${codeName}'`);
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
if (screen.includes("Yes, I accept") && screen.includes("Bypass Permissions")) {
|
|
639
|
-
execSync(`tmux send-keys -t ${tmuxSession} 2`, { stdio: "ignore" });
|
|
640
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
641
|
-
execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
|
|
642
|
-
log(`[persistent-session] Auto-accepted bypass permissions for '${codeName}'`);
|
|
1022
|
+
const dialogAction = sweepDialogs(screen);
|
|
1023
|
+
if (dialogAction) {
|
|
1024
|
+
await sendDialogKeys(tmuxSession, dialogAction);
|
|
1025
|
+
log(`[persistent-session] ${dialogAction.logMessage} for '${codeName}'`);
|
|
643
1026
|
continue;
|
|
644
1027
|
}
|
|
645
1028
|
if (screen.includes("\u276F") && !screen.includes("Enter to confirm")) {
|
|
@@ -690,7 +1073,7 @@ async function waitForPromptReady(tmuxSession) {
|
|
|
690
1073
|
const deadline = Date.now() + 1e4;
|
|
691
1074
|
while (Date.now() < deadline) {
|
|
692
1075
|
try {
|
|
693
|
-
const screen =
|
|
1076
|
+
const screen = execFileSync3(
|
|
694
1077
|
"tmux",
|
|
695
1078
|
["capture-pane", "-t", tmuxSession, "-p"],
|
|
696
1079
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
@@ -709,11 +1092,11 @@ function sleepBlockingMs(ms) {
|
|
|
709
1092
|
}
|
|
710
1093
|
function defaultArmSender(tmuxSession, command) {
|
|
711
1094
|
try {
|
|
712
|
-
|
|
1095
|
+
execFileSync3("tmux", ["send-keys", "-t", tmuxSession, "-l", command], {
|
|
713
1096
|
stdio: ["ignore", "ignore", "pipe"]
|
|
714
1097
|
});
|
|
715
1098
|
sleepBlockingMs(SEND_KEYS_ENTER_DELAY_MS);
|
|
716
|
-
|
|
1099
|
+
execFileSync3("tmux", ["send-keys", "-t", tmuxSession, "Enter"], {
|
|
717
1100
|
stdio: ["ignore", "ignore", "pipe"]
|
|
718
1101
|
});
|
|
719
1102
|
return true;
|
|
@@ -722,6 +1105,50 @@ function defaultArmSender(tmuxSession, command) {
|
|
|
722
1105
|
}
|
|
723
1106
|
}
|
|
724
1107
|
var armSender = defaultArmSender;
|
|
1108
|
+
function defaultPaneCapture(tmuxSession) {
|
|
1109
|
+
try {
|
|
1110
|
+
return execFileSync3("tmux", ["capture-pane", "-t", tmuxSession, "-p"], {
|
|
1111
|
+
encoding: "utf-8",
|
|
1112
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1113
|
+
timeout: 2e3
|
|
1114
|
+
});
|
|
1115
|
+
} catch {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
var paneCapture = defaultPaneCapture;
|
|
1120
|
+
var defaultHygieneKeySender = async (tmuxSession, keys, interKeyDelayMs) => {
|
|
1121
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1122
|
+
if (i > 0 && interKeyDelayMs > 0) {
|
|
1123
|
+
await new Promise((r) => setTimeout(r, interKeyDelayMs));
|
|
1124
|
+
}
|
|
1125
|
+
execFileSync3("tmux", ["send-keys", "-t", tmuxSession, keys[i]], {
|
|
1126
|
+
stdio: "ignore"
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
var hygieneKeySender = defaultHygieneKeySender;
|
|
1131
|
+
async function preSendPaneHygiene(tmuxSession, codeName, log) {
|
|
1132
|
+
try {
|
|
1133
|
+
let screen = paneCapture(tmuxSession);
|
|
1134
|
+
if (screen === null) return;
|
|
1135
|
+
const action = sweepDialogs(screen);
|
|
1136
|
+
if (action) {
|
|
1137
|
+
await hygieneKeySender(tmuxSession, action.keys, action.interKeyDelayMs);
|
|
1138
|
+
log(`[inject] ${action.logMessage} for '${codeName}' before injection`);
|
|
1139
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1140
|
+
screen = paneCapture(tmuxSession) ?? "";
|
|
1141
|
+
}
|
|
1142
|
+
const orphan = extractInputBoxText(screen);
|
|
1143
|
+
if (orphan) {
|
|
1144
|
+
log(
|
|
1145
|
+
`[inject] clearing orphaned input for '${codeName}' before injection (input_hash=${simpleTextHash(orphan)}, len=${orphan.length})`
|
|
1146
|
+
);
|
|
1147
|
+
await hygieneKeySender(tmuxSession, ["C-u"], 0);
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
725
1152
|
function sendToAgent(tmuxSession, command) {
|
|
726
1153
|
return armSender(tmuxSession, command);
|
|
727
1154
|
}
|
|
@@ -732,6 +1159,14 @@ var _internals = {
|
|
|
732
1159
|
isLoginPickerVisible,
|
|
733
1160
|
isResumeModeDialogVisible,
|
|
734
1161
|
detectFailureSignature,
|
|
1162
|
+
// ENG-6039 test seams: seed/clear the module-private session map so
|
|
1163
|
+
// prepareForRespawn's rotation gate can be exercised without tmux.
|
|
1164
|
+
__seedSession(session) {
|
|
1165
|
+
sessions.set(session.codeName, session);
|
|
1166
|
+
},
|
|
1167
|
+
__clearSessions() {
|
|
1168
|
+
sessions.clear();
|
|
1169
|
+
},
|
|
735
1170
|
isClaudeProcessAliveInTmux,
|
|
736
1171
|
waitForPromptReady,
|
|
737
1172
|
// ENG-5770: exported so the unit test in claude-model-alias.test.ts can
|
|
@@ -744,6 +1179,17 @@ var _internals = {
|
|
|
744
1179
|
__setArmSender(fn) {
|
|
745
1180
|
armSender = fn ?? defaultArmSender;
|
|
746
1181
|
},
|
|
1182
|
+
// ENG-6017 test seam: swap the pane capture used by the inject-time
|
|
1183
|
+
// hygiene so unit tests can simulate dialog overlays / orphaned input
|
|
1184
|
+
// without a tmux server. null restores the real capture.
|
|
1185
|
+
__setPaneCapture(fn) {
|
|
1186
|
+
paneCapture = fn ?? defaultPaneCapture;
|
|
1187
|
+
},
|
|
1188
|
+
// ENG-6017 test seam: swap the hygiene key sender (dialog dismissal +
|
|
1189
|
+
// C-u clear) so unit tests can assert keystrokes without tmux.
|
|
1190
|
+
__setHygieneKeySender(fn) {
|
|
1191
|
+
hygieneKeySender = fn ?? defaultHygieneKeySender;
|
|
1192
|
+
},
|
|
747
1193
|
// ENG-5758 test seam: swap the acpx session probe so unit tests can
|
|
748
1194
|
// simulate "session present" / "no session" without spawning acpx.
|
|
749
1195
|
__setAcpxSessionProbe(fn) {
|
|
@@ -818,6 +1264,7 @@ async function injectMessageWithStatus(codeName, type, content, meta, log) {
|
|
|
818
1264
|
_log(`[inject] acpx binary not found \u2014 falling back to tmux send-keys`);
|
|
819
1265
|
}
|
|
820
1266
|
const singleLineText = text.replace(/\s*\n+\s*/g, " ").trim();
|
|
1267
|
+
await preSendPaneHygiene(`agt-${codeName}`, codeName, _log);
|
|
821
1268
|
const sent = sendToAgent(`agt-${codeName}`, singleLineText);
|
|
822
1269
|
if (sent) {
|
|
823
1270
|
_log(`[inject] tmux send-keys sent for '${codeName}' \u2014 unverified (delivered=false, fallbackUsed=true)`);
|
|
@@ -838,7 +1285,7 @@ function stopPersistentSession(codeName, log) {
|
|
|
838
1285
|
try {
|
|
839
1286
|
const acpx = getAcpxBin();
|
|
840
1287
|
if (acpx) {
|
|
841
|
-
|
|
1288
|
+
execFileSync3(acpx, ["claude", "sessions", "close", `agt-${codeName}`], {
|
|
842
1289
|
cwd: getProjectDir(codeName),
|
|
843
1290
|
timeout: 5e3,
|
|
844
1291
|
stdio: "ignore"
|
|
@@ -859,21 +1306,8 @@ var ZOMBIE_PROBE_TTL_MS = 3e4;
|
|
|
859
1306
|
var ZOMBIE_STARTUP_GRACE_MS = 6e4;
|
|
860
1307
|
var zombieProbeCache = /* @__PURE__ */ new Map();
|
|
861
1308
|
var pendingZombieDetections = /* @__PURE__ */ new Map();
|
|
862
|
-
function escapePgrepRegex(value) {
|
|
863
|
-
return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
|
|
864
|
-
}
|
|
865
1309
|
function isClaudeProcessAliveInTmux(tmuxSession) {
|
|
866
|
-
|
|
867
|
-
const escapedSession = escapePgrepRegex(tmuxSession);
|
|
868
|
-
const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
|
|
869
|
-
const out = execFileSync("pgrep", ["-f", "--", pattern], {
|
|
870
|
-
encoding: "utf-8",
|
|
871
|
-
timeout: 3e3
|
|
872
|
-
}).trim();
|
|
873
|
-
return out.length > 0;
|
|
874
|
-
} catch {
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
1310
|
+
return probeClaudeProcessInTmux(tmuxSession) === "alive";
|
|
877
1311
|
}
|
|
878
1312
|
function takeZombieDetection(codeName) {
|
|
879
1313
|
const record = pendingZombieDetections.get(codeName);
|
|
@@ -928,7 +1362,7 @@ function isSessionHealthy(codeName) {
|
|
|
928
1362
|
if (!claudeAlive) {
|
|
929
1363
|
const paneTail = readPaneLogTail(codeName);
|
|
930
1364
|
try {
|
|
931
|
-
|
|
1365
|
+
execFileSync3("tmux", ["kill-session", "-t", tmuxSession], { stdio: "ignore" });
|
|
932
1366
|
} catch {
|
|
933
1367
|
}
|
|
934
1368
|
session.status = "crashed";
|
|
@@ -968,13 +1402,13 @@ function collectDiagnostics(codeNames) {
|
|
|
968
1402
|
let launchArgs = null;
|
|
969
1403
|
let channelStatus = null;
|
|
970
1404
|
try {
|
|
971
|
-
|
|
1405
|
+
execFileSync3("tmux", ["has-session", "-t", tmuxSession], { stdio: "ignore" });
|
|
972
1406
|
tmuxAlive = true;
|
|
973
1407
|
} catch {
|
|
974
1408
|
}
|
|
975
1409
|
if (tmuxAlive) {
|
|
976
1410
|
try {
|
|
977
|
-
screenCapture =
|
|
1411
|
+
screenCapture = execFileSync3("tmux", ["capture-pane", "-t", tmuxSession, "-p", "-S", "-30"], {
|
|
978
1412
|
encoding: "utf-8",
|
|
979
1413
|
timeout: 3e3
|
|
980
1414
|
}).trim();
|
|
@@ -982,7 +1416,7 @@ function collectDiagnostics(codeNames) {
|
|
|
982
1416
|
}
|
|
983
1417
|
}
|
|
984
1418
|
try {
|
|
985
|
-
const psOutput =
|
|
1419
|
+
const psOutput = execFileSync3("ps", ["aux"], { encoding: "utf-8", timeout: 3e3 });
|
|
986
1420
|
const line = psOutput.split("\n").find((l) => l.includes(`agt-${codeName}`) && !l.includes("grep"));
|
|
987
1421
|
if (line) {
|
|
988
1422
|
const match = line.match(/claude\s+.*/);
|
|
@@ -1052,7 +1486,7 @@ function writeAcpxConfig(config) {
|
|
|
1052
1486
|
if (channels.length > 0) claudeArgs.push("--channels", ...channels);
|
|
1053
1487
|
if (devChannels.length > 0) claudeArgs.push("--dangerously-load-development-channels", ...devChannels);
|
|
1054
1488
|
claudeArgs.push("--mcp-config", mcpConfigPath);
|
|
1055
|
-
if (
|
|
1489
|
+
if (existsSync3(claudeMdPath)) claudeArgs.push("--system-prompt-file", claudeMdPath);
|
|
1056
1490
|
const acpModelAlias = claudeModelAlias(config.primaryModel);
|
|
1057
1491
|
if (acpModelAlias) claudeArgs.push("--model", acpModelAlias);
|
|
1058
1492
|
claudeArgs.push("--allow-dangerously-skip-permissions");
|
|
@@ -1064,7 +1498,7 @@ function writeAcpxConfig(config) {
|
|
|
1064
1498
|
const envIntegrationsPath = join2(projectDir, ".env.integrations");
|
|
1065
1499
|
const wrapperPath = join2(projectDir, ".claude", "acpx-agent.sh");
|
|
1066
1500
|
const wrapperLines = ["#!/usr/bin/env bash"];
|
|
1067
|
-
if (
|
|
1501
|
+
if (existsSync3(envIntegrationsPath)) {
|
|
1068
1502
|
wrapperLines.push(`set -a`, `source ${JSON.stringify(envIntegrationsPath)}`, `set +a`);
|
|
1069
1503
|
}
|
|
1070
1504
|
if (claudeAuthMode === "api_key" && anthropicApiKey) {
|
|
@@ -1087,18 +1521,25 @@ function writeAcpxConfig(config) {
|
|
|
1087
1521
|
}
|
|
1088
1522
|
|
|
1089
1523
|
export {
|
|
1524
|
+
formatMissingVar,
|
|
1525
|
+
parseEnvIntegrations,
|
|
1526
|
+
probeMcpEnvSubstitution,
|
|
1090
1527
|
sanitizeMcpJson,
|
|
1091
1528
|
buildAllowedTools,
|
|
1092
1529
|
sessionTranscriptDir,
|
|
1093
1530
|
isAgentIdle,
|
|
1094
1531
|
isStaleForToday,
|
|
1095
1532
|
peekCurrentSession,
|
|
1533
|
+
checkChannelInputs,
|
|
1534
|
+
takeWatchdogGiveUpCount,
|
|
1535
|
+
creditWatchdogGiveUpCount,
|
|
1096
1536
|
resolveClaudeBinary,
|
|
1097
1537
|
writePersistentClaudeWrapper,
|
|
1098
1538
|
paneLogPath,
|
|
1099
1539
|
readPaneLogTail,
|
|
1100
1540
|
prepareForRespawn,
|
|
1101
1541
|
getLastFailureContext,
|
|
1542
|
+
resolveSessionSpawnDecision,
|
|
1102
1543
|
startPersistentSession,
|
|
1103
1544
|
SEND_KEYS_ENTER_DELAY_MS,
|
|
1104
1545
|
sendToAgent,
|
|
@@ -1116,4 +1557,4 @@ export {
|
|
|
1116
1557
|
stopAllSessionsAndWait,
|
|
1117
1558
|
getProjectDir
|
|
1118
1559
|
};
|
|
1119
|
-
//# sourceMappingURL=chunk-
|
|
1560
|
+
//# sourceMappingURL=chunk-SJPAXXTW.js.map
|