@integrity-labs/agt-cli 0.27.8-test.9 → 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.
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  claudeModelAlias,
3
3
  isClaudeFastMode
4
- } from "./chunk-HT6EETEL.js";
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 as execFileSync2 } 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 existsSync2, readFileSync as readFileSync3, readdirSync, writeFileSync as writeFileSync3, appendFileSync, mkdirSync as mkdirSync2, chmodSync, copyFileSync, rmSync } from "fs";
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 readFileSync2, renameSync, statSync, writeFileSync as writeFileSync2 } from "fs";
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 (!existsSync(path)) return { current: null, history: [] };
176
+ if (!existsSync2(path)) return { current: null, history: [] };
101
177
  try {
102
- const raw = readFileSync2(path, "utf-8");
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 (!existsSync(path)) return true;
276
+ if (!existsSync2(path)) return true;
172
277
  try {
173
278
  const mtimeMs = statSync(path).mtimeMs;
174
279
  return now.getTime() - mtimeMs >= idleSeconds * 1e3;
@@ -207,12 +312,265 @@ function probeClaudeProcessInTmux(tmuxSession) {
207
312
  }
208
313
  }
209
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
+
210
568
  // src/lib/persistent-session.ts
211
569
  function syncClaudeCredsToRoot() {
212
570
  if (platform() !== "linux") return true;
213
571
  if (typeof process.getuid !== "function" || process.getuid() !== 0) return true;
214
572
  for (const filename of [".credentials.json", "credentials.json"]) {
215
- if (existsSync2(join2("/root/.claude", filename))) return true;
573
+ if (existsSync3(join2("/root/.claude", filename))) return true;
216
574
  }
217
575
  let sourcePath = null;
218
576
  try {
@@ -221,7 +579,7 @@ function syncClaudeCredsToRoot() {
221
579
  if (!entry.isDirectory()) continue;
222
580
  for (const filename of [".credentials.json", "credentials.json"]) {
223
581
  const candidate = join2("/home", entry.name, ".claude", filename);
224
- if (existsSync2(candidate)) {
582
+ if (existsSync3(candidate)) {
225
583
  sourcePath = candidate;
226
584
  break outer;
227
585
  }
@@ -234,7 +592,7 @@ function syncClaudeCredsToRoot() {
234
592
  const sourceFilename = sourcePath.endsWith("credentials.json") && !sourcePath.endsWith(".credentials.json") ? "credentials.json" : ".credentials.json";
235
593
  const targetPath = join2(targetDir, sourceFilename);
236
594
  try {
237
- if (!existsSync2(targetDir)) mkdirSync2(targetDir, { recursive: true, mode: 448 });
595
+ if (!existsSync3(targetDir)) mkdirSync2(targetDir, { recursive: true, mode: 448 });
238
596
  copyFileSync(sourcePath, targetPath);
239
597
  chmodSync(targetPath, 384);
240
598
  return true;
@@ -246,13 +604,13 @@ var cachedClaudePath = null;
246
604
  function resolveClaudeBinary() {
247
605
  if (cachedClaudePath) return cachedClaudePath;
248
606
  const override = process.env.CLAUDE_PATH;
249
- if (override && existsSync2(override)) {
607
+ if (override && existsSync3(override)) {
250
608
  cachedClaudePath = override;
251
609
  return override;
252
610
  }
253
611
  try {
254
612
  const out = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
255
- if (out && existsSync2(out)) {
613
+ if (out && existsSync3(out)) {
256
614
  cachedClaudePath = out;
257
615
  return out;
258
616
  }
@@ -264,7 +622,7 @@ function resolveClaudeBinary() {
264
622
  "/usr/local/bin/claude"
265
623
  ];
266
624
  for (const p of candidates) {
267
- if (existsSync2(p)) {
625
+ if (existsSync3(p)) {
268
626
  cachedClaudePath = p;
269
627
  return p;
270
628
  }
@@ -282,7 +640,7 @@ function writePersistentClaudeWrapper(args) {
282
640
  // --dangerously-skip-permissions on dedicated EC2 hosts.
283
641
  "export IS_SANDBOX=1"
284
642
  ];
285
- if (existsSync2(envIntegrationsPath)) {
643
+ if (existsSync3(envIntegrationsPath)) {
286
644
  wrapperLines.push(
287
645
  "set -a",
288
646
  `source ${JSON.stringify(envIntegrationsPath)}`,
@@ -299,9 +657,9 @@ function writePersistentClaudeWrapper(args) {
299
657
  return wrapperPath;
300
658
  }
301
659
  function collectMcpServerNames(mcpConfigPath) {
302
- if (!existsSync2(mcpConfigPath)) return [];
660
+ if (!existsSync3(mcpConfigPath)) return [];
303
661
  try {
304
- const data = JSON.parse(readFileSync3(mcpConfigPath, "utf-8"));
662
+ const data = JSON.parse(readFileSync4(mcpConfigPath, "utf-8"));
305
663
  const servers = data.mcpServers;
306
664
  return servers ? Object.keys(servers) : [];
307
665
  } catch {
@@ -315,7 +673,7 @@ function getAcpxBin() {
315
673
  let dir = moduleDir;
316
674
  for (let i = 0; i < 6; i++) {
317
675
  const candidate = join2(dir, "node_modules", ".bin", "acpx");
318
- if (existsSync2(candidate)) {
676
+ if (existsSync3(candidate)) {
319
677
  _acpxBin = candidate;
320
678
  return _acpxBin;
321
679
  }
@@ -333,7 +691,7 @@ function getAcpxBin() {
333
691
  }
334
692
  function defaultAcpxSessionProbe(acpxBin, projectDir) {
335
693
  try {
336
- execFileSync2(acpxBin, ["claude", "list-sessions"], {
694
+ execFileSync3(acpxBin, ["claude", "list-sessions"], {
337
695
  cwd: projectDir,
338
696
  timeout: 5e3,
339
697
  stdio: "ignore"
@@ -379,9 +737,9 @@ function setupPaneLog(tmuxSession, codeName, log) {
379
737
  }
380
738
  function readPaneLogTail(codeName, lines = PANE_TAIL_LINES) {
381
739
  const logPath = paneLogPath(codeName);
382
- if (!existsSync2(logPath)) return null;
740
+ if (!existsSync3(logPath)) return null;
383
741
  try {
384
- const raw = readFileSync3(logPath, "utf-8");
742
+ const raw = readFileSync4(logPath, "utf-8");
385
743
  if (!raw) return null;
386
744
  const stripped = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
387
745
  const all = stripped.split("\n").filter((l) => l.length > 0);
@@ -399,7 +757,7 @@ function prepareForRespawn(codeName) {
399
757
  const session = sessions.get(codeName);
400
758
  if (!session) return null;
401
759
  const signature = detectFailureSignature(session.lastFailureTail);
402
- if (signature === "session_id_in_use" && session.consecutiveSameUuidFailures >= 2) {
760
+ if (session.consecutiveSameUuidFailures >= 2) {
403
761
  const failureCount = session.consecutiveSameUuidFailures;
404
762
  const newId = rotateDailySession(
405
763
  codeName,
@@ -408,7 +766,7 @@ function prepareForRespawn(codeName) {
408
766
  );
409
767
  session.consecutiveSameUuidFailures = 0;
410
768
  session.lastFailureSessionId = null;
411
- return `rotated daily-session UUID to ${newId} after ${failureCount}+ "Session ID already in use" failures`;
769
+ return `rotated daily-session UUID to ${newId} after ${failureCount} consecutive failures on the same UUID (signature=${signature})`;
412
770
  }
413
771
  return null;
414
772
  }
@@ -421,6 +779,27 @@ function getLastFailureContext(codeName) {
421
779
  restartCount: session?.restartCount ?? 0
422
780
  };
423
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
+ }
424
803
  function startPersistentSession(config) {
425
804
  const existing = sessions.get(config.codeName);
426
805
  if (existing && existing.status === "running") {
@@ -469,7 +848,7 @@ function spawnSession(config, session) {
469
848
  const claudeDir = join2(homedir2(), ".claude");
470
849
  for (const filename of [".credentials.json", "credentials.json"]) {
471
850
  const p = join2(claudeDir, filename);
472
- if (existsSync2(p)) {
851
+ if (existsSync3(p)) {
473
852
  try {
474
853
  rmSync(p, { force: true });
475
854
  log(`[persistent-session] Removed ${p} (api_key mode active \u2014 preventing OAuth fallback)`);
@@ -482,9 +861,17 @@ function spawnSession(config, session) {
482
861
  }
483
862
  }
484
863
  const args = [];
485
- const sessionId = randomUUID2();
486
- args.push("--session-id", sessionId);
487
- log(`[persistent-session] Starting fresh session ${sessionId} for '${codeName}'`);
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
+ );
488
875
  try {
489
876
  markDailySessionSpawn(codeName, sessionId, /* @__PURE__ */ new Date(), config.agentTimezone ?? void 0);
490
877
  } catch (err) {
@@ -495,7 +882,7 @@ function spawnSession(config, session) {
495
882
  if (channels.length > 0) args.push("--channels", ...channels);
496
883
  if (devChannels.length > 0) args.push("--dangerously-load-development-channels", ...devChannels);
497
884
  args.push("--mcp-config", mcpConfigPath);
498
- if (existsSync2(claudeMdPath)) args.push("--system-prompt-file", claudeMdPath);
885
+ if (existsSync3(claudeMdPath)) args.push("--system-prompt-file", claudeMdPath);
499
886
  const modelAlias = claudeModelAlias(config.primaryModel);
500
887
  if (modelAlias) args.push("--model", modelAlias);
501
888
  args.push("--allow-dangerously-skip-permissions");
@@ -504,7 +891,7 @@ function spawnSession(config, session) {
504
891
  args.push("--name", tmuxSession);
505
892
  const mcpServerNames = collectMcpServerNames(mcpConfigPath);
506
893
  args.push("--allowedTools", buildAllowedTools(mcpServerNames));
507
- 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.';
508
895
  const claudeBin = resolveClaudeBinary();
509
896
  const claudeArgsJoined = args.map((a) => a.includes(" ") || a.includes("*") ? JSON.stringify(a) : a).join(" ");
510
897
  const wrapperPath = writePersistentClaudeWrapper({
@@ -529,6 +916,16 @@ function spawnSession(config, session) {
529
916
  if (config.runId) {
530
917
  tmuxEnv["AGT_RUN_ID"] = config.runId;
531
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
+ }
532
929
  const child = spawn("tmux", [
533
930
  "new-session",
534
931
  "-d",
@@ -573,12 +970,6 @@ function spawnSession(config, session) {
573
970
  session.restartCount++;
574
971
  }
575
972
  }
576
- function isLoginPickerVisible(screen) {
577
- return screen.includes("Select login method") || screen.includes("Claude account with subscription") && screen.includes("Anthropic Console account");
578
- }
579
- function isResumeModeDialogVisible(screen) {
580
- return screen.includes("Resume from summary") && screen.includes("Don't ask me again");
581
- }
582
973
  function hasMcpChildren(tmuxSession) {
583
974
  try {
584
975
  const claudePidOut = execSync(
@@ -628,40 +1019,10 @@ async function acceptDialogs(tmuxSession, codeName, log, primaryModel = null, se
628
1019
  continue;
629
1020
  }
630
1021
  dialogIterations++;
631
- if (screen.includes("Choose the text style") || screen.includes("Dark mode") && screen.includes("Light mode")) {
632
- execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
633
- log(`[persistent-session] Auto-accepted theme picker for '${codeName}'`);
634
- continue;
635
- }
636
- if (screen.includes("Yes, I trust this folder")) {
637
- execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
638
- log(`[persistent-session] Auto-accepted workspace trust for '${codeName}'`);
639
- continue;
640
- }
641
- if (isResumeModeDialogVisible(screen)) {
642
- execFileSync2("tmux", ["send-keys", "-t", tmuxSession, "3"], { stdio: "ignore" });
643
- await new Promise((r) => setTimeout(r, 300));
644
- execFileSync2("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { stdio: "ignore" });
645
- log(
646
- `[persistent-session] Auto-dismissed resume-mode dialog for '${codeName}' (picked 'Don't ask me again')`
647
- );
648
- continue;
649
- }
650
- if (screen.includes("I am using this for local development")) {
651
- execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
652
- log(`[persistent-session] Auto-accepted dev channels for '${codeName}'`);
653
- continue;
654
- }
655
- if (screen.includes("Enter to confirm") && screen.includes("MCP")) {
656
- execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
657
- log(`[persistent-session] Auto-accepted MCP servers for '${codeName}'`);
658
- continue;
659
- }
660
- if (screen.includes("Yes, I accept") && screen.includes("Bypass Permissions")) {
661
- execSync(`tmux send-keys -t ${tmuxSession} 2`, { stdio: "ignore" });
662
- await new Promise((r) => setTimeout(r, 300));
663
- execSync(`tmux send-keys -t ${tmuxSession} Enter`, { stdio: "ignore" });
664
- 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}'`);
665
1026
  continue;
666
1027
  }
667
1028
  if (screen.includes("\u276F") && !screen.includes("Enter to confirm")) {
@@ -712,7 +1073,7 @@ async function waitForPromptReady(tmuxSession) {
712
1073
  const deadline = Date.now() + 1e4;
713
1074
  while (Date.now() < deadline) {
714
1075
  try {
715
- const screen = execFileSync2(
1076
+ const screen = execFileSync3(
716
1077
  "tmux",
717
1078
  ["capture-pane", "-t", tmuxSession, "-p"],
718
1079
  { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
@@ -731,11 +1092,11 @@ function sleepBlockingMs(ms) {
731
1092
  }
732
1093
  function defaultArmSender(tmuxSession, command) {
733
1094
  try {
734
- execFileSync2("tmux", ["send-keys", "-t", tmuxSession, "-l", command], {
1095
+ execFileSync3("tmux", ["send-keys", "-t", tmuxSession, "-l", command], {
735
1096
  stdio: ["ignore", "ignore", "pipe"]
736
1097
  });
737
1098
  sleepBlockingMs(SEND_KEYS_ENTER_DELAY_MS);
738
- execFileSync2("tmux", ["send-keys", "-t", tmuxSession, "Enter"], {
1099
+ execFileSync3("tmux", ["send-keys", "-t", tmuxSession, "Enter"], {
739
1100
  stdio: ["ignore", "ignore", "pipe"]
740
1101
  });
741
1102
  return true;
@@ -744,6 +1105,50 @@ function defaultArmSender(tmuxSession, command) {
744
1105
  }
745
1106
  }
746
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
+ }
747
1152
  function sendToAgent(tmuxSession, command) {
748
1153
  return armSender(tmuxSession, command);
749
1154
  }
@@ -754,6 +1159,14 @@ var _internals = {
754
1159
  isLoginPickerVisible,
755
1160
  isResumeModeDialogVisible,
756
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
+ },
757
1170
  isClaudeProcessAliveInTmux,
758
1171
  waitForPromptReady,
759
1172
  // ENG-5770: exported so the unit test in claude-model-alias.test.ts can
@@ -766,6 +1179,17 @@ var _internals = {
766
1179
  __setArmSender(fn) {
767
1180
  armSender = fn ?? defaultArmSender;
768
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
+ },
769
1193
  // ENG-5758 test seam: swap the acpx session probe so unit tests can
770
1194
  // simulate "session present" / "no session" without spawning acpx.
771
1195
  __setAcpxSessionProbe(fn) {
@@ -840,6 +1264,7 @@ async function injectMessageWithStatus(codeName, type, content, meta, log) {
840
1264
  _log(`[inject] acpx binary not found \u2014 falling back to tmux send-keys`);
841
1265
  }
842
1266
  const singleLineText = text.replace(/\s*\n+\s*/g, " ").trim();
1267
+ await preSendPaneHygiene(`agt-${codeName}`, codeName, _log);
843
1268
  const sent = sendToAgent(`agt-${codeName}`, singleLineText);
844
1269
  if (sent) {
845
1270
  _log(`[inject] tmux send-keys sent for '${codeName}' \u2014 unverified (delivered=false, fallbackUsed=true)`);
@@ -860,7 +1285,7 @@ function stopPersistentSession(codeName, log) {
860
1285
  try {
861
1286
  const acpx = getAcpxBin();
862
1287
  if (acpx) {
863
- execFileSync2(acpx, ["claude", "sessions", "close", `agt-${codeName}`], {
1288
+ execFileSync3(acpx, ["claude", "sessions", "close", `agt-${codeName}`], {
864
1289
  cwd: getProjectDir(codeName),
865
1290
  timeout: 5e3,
866
1291
  stdio: "ignore"
@@ -937,7 +1362,7 @@ function isSessionHealthy(codeName) {
937
1362
  if (!claudeAlive) {
938
1363
  const paneTail = readPaneLogTail(codeName);
939
1364
  try {
940
- execFileSync2("tmux", ["kill-session", "-t", tmuxSession], { stdio: "ignore" });
1365
+ execFileSync3("tmux", ["kill-session", "-t", tmuxSession], { stdio: "ignore" });
941
1366
  } catch {
942
1367
  }
943
1368
  session.status = "crashed";
@@ -977,13 +1402,13 @@ function collectDiagnostics(codeNames) {
977
1402
  let launchArgs = null;
978
1403
  let channelStatus = null;
979
1404
  try {
980
- execFileSync2("tmux", ["has-session", "-t", tmuxSession], { stdio: "ignore" });
1405
+ execFileSync3("tmux", ["has-session", "-t", tmuxSession], { stdio: "ignore" });
981
1406
  tmuxAlive = true;
982
1407
  } catch {
983
1408
  }
984
1409
  if (tmuxAlive) {
985
1410
  try {
986
- screenCapture = execFileSync2("tmux", ["capture-pane", "-t", tmuxSession, "-p", "-S", "-30"], {
1411
+ screenCapture = execFileSync3("tmux", ["capture-pane", "-t", tmuxSession, "-p", "-S", "-30"], {
987
1412
  encoding: "utf-8",
988
1413
  timeout: 3e3
989
1414
  }).trim();
@@ -991,7 +1416,7 @@ function collectDiagnostics(codeNames) {
991
1416
  }
992
1417
  }
993
1418
  try {
994
- const psOutput = execFileSync2("ps", ["aux"], { encoding: "utf-8", timeout: 3e3 });
1419
+ const psOutput = execFileSync3("ps", ["aux"], { encoding: "utf-8", timeout: 3e3 });
995
1420
  const line = psOutput.split("\n").find((l) => l.includes(`agt-${codeName}`) && !l.includes("grep"));
996
1421
  if (line) {
997
1422
  const match = line.match(/claude\s+.*/);
@@ -1061,7 +1486,7 @@ function writeAcpxConfig(config) {
1061
1486
  if (channels.length > 0) claudeArgs.push("--channels", ...channels);
1062
1487
  if (devChannels.length > 0) claudeArgs.push("--dangerously-load-development-channels", ...devChannels);
1063
1488
  claudeArgs.push("--mcp-config", mcpConfigPath);
1064
- if (existsSync2(claudeMdPath)) claudeArgs.push("--system-prompt-file", claudeMdPath);
1489
+ if (existsSync3(claudeMdPath)) claudeArgs.push("--system-prompt-file", claudeMdPath);
1065
1490
  const acpModelAlias = claudeModelAlias(config.primaryModel);
1066
1491
  if (acpModelAlias) claudeArgs.push("--model", acpModelAlias);
1067
1492
  claudeArgs.push("--allow-dangerously-skip-permissions");
@@ -1073,7 +1498,7 @@ function writeAcpxConfig(config) {
1073
1498
  const envIntegrationsPath = join2(projectDir, ".env.integrations");
1074
1499
  const wrapperPath = join2(projectDir, ".claude", "acpx-agent.sh");
1075
1500
  const wrapperLines = ["#!/usr/bin/env bash"];
1076
- if (existsSync2(envIntegrationsPath)) {
1501
+ if (existsSync3(envIntegrationsPath)) {
1077
1502
  wrapperLines.push(`set -a`, `source ${JSON.stringify(envIntegrationsPath)}`, `set +a`);
1078
1503
  }
1079
1504
  if (claudeAuthMode === "api_key" && anthropicApiKey) {
@@ -1096,18 +1521,25 @@ function writeAcpxConfig(config) {
1096
1521
  }
1097
1522
 
1098
1523
  export {
1524
+ formatMissingVar,
1525
+ parseEnvIntegrations,
1526
+ probeMcpEnvSubstitution,
1099
1527
  sanitizeMcpJson,
1100
1528
  buildAllowedTools,
1101
1529
  sessionTranscriptDir,
1102
1530
  isAgentIdle,
1103
1531
  isStaleForToday,
1104
1532
  peekCurrentSession,
1533
+ checkChannelInputs,
1534
+ takeWatchdogGiveUpCount,
1535
+ creditWatchdogGiveUpCount,
1105
1536
  resolveClaudeBinary,
1106
1537
  writePersistentClaudeWrapper,
1107
1538
  paneLogPath,
1108
1539
  readPaneLogTail,
1109
1540
  prepareForRespawn,
1110
1541
  getLastFailureContext,
1542
+ resolveSessionSpawnDecision,
1111
1543
  startPersistentSession,
1112
1544
  SEND_KEYS_ENTER_DELAY_MS,
1113
1545
  sendToAgent,
@@ -1125,4 +1557,4 @@ export {
1125
1557
  stopAllSessionsAndWait,
1126
1558
  getProjectDir
1127
1559
  };
1128
- //# sourceMappingURL=chunk-F4NG4EXD.js.map
1560
+ //# sourceMappingURL=chunk-SJPAXXTW.js.map