@evident-ai/cli 3.4.1-dev.c03b461 → 3.4.1-dev.c2c5e11

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/index.js CHANGED
@@ -11,8 +11,8 @@ import chalk2 from "chalk";
11
11
 
12
12
  // src/lib/config.ts
13
13
  import Conf from "conf";
14
- import { chmodSync, existsSync, statSync } from "fs";
15
- import { dirname } from "path";
14
+ import { chmodSync, existsSync, statSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
16
  var PRODUCTION_API_URL = "https://api.production.evident.run/v1";
17
17
  var PRODUCTION_TUNNEL_URL = "wss://tunnel.production.evident.run";
18
18
  var defaults = {
@@ -623,6 +623,19 @@ function isInteractive(jsonOutput) {
623
623
  return true;
624
624
  }
625
625
 
626
+ // src/lib/subscription-usage-report.ts
627
+ function toReportedSubscription(collected) {
628
+ if (!collected) return null;
629
+ if (collected.ownerEmail === null && collected.planType === null && collected.organizationName === null) {
630
+ return null;
631
+ }
632
+ return {
633
+ owner_email: collected.ownerEmail,
634
+ plan_type: collected.planType,
635
+ organization_name: collected.organizationName
636
+ };
637
+ }
638
+
626
639
  // src/commands/agent-lookup.ts
627
640
  async function readErrorMessage(response) {
628
641
  const text = await response.text().catch(() => "");
@@ -722,14 +735,6 @@ function toReportedWindow(window) {
722
735
  if (!window) return null;
723
736
  return { utilization: window.utilization, resets_at: window.resetsAt };
724
737
  }
725
- function toReportedOwner(snapshot) {
726
- if (!snapshot.owner) return null;
727
- return {
728
- email: snapshot.owner.email,
729
- organization_name: snapshot.owner.organizationName,
730
- rate_limit_tier: snapshot.owner.rateLimitTier
731
- };
732
- }
733
738
  async function reportClaudeUsage(agentId, authHeader, snapshot) {
734
739
  try {
735
740
  const apiUrl = getApiUrlConfig();
@@ -739,7 +744,7 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
739
744
  body: JSON.stringify({
740
745
  five_hour: toReportedWindow(snapshot.fiveHour),
741
746
  seven_day: toReportedWindow(snapshot.sevenDay),
742
- owner: toReportedOwner(snapshot)
747
+ subscription: toReportedSubscription(snapshot.subscription)
743
748
  }),
744
749
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
745
750
  });
@@ -773,7 +778,8 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
773
778
  primary: toReportedOpenAiWindow(snapshot.primary),
774
779
  secondary: toReportedOpenAiWindow(snapshot.secondary),
775
780
  has_credits: snapshot.hasCredits,
776
- credits_unlimited: snapshot.creditsUnlimited
781
+ credits_unlimited: snapshot.creditsUnlimited,
782
+ subscription: toReportedSubscription(snapshot.subscription)
777
783
  }),
778
784
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
779
785
  });
@@ -797,6 +803,7 @@ async function reportResourceUsage(agentId, authHeader, usage) {
797
803
  headers: { Authorization: authHeader, "Content-Type": "application/json" },
798
804
  body: JSON.stringify({
799
805
  cpu_percent: usage.cpuPercent,
806
+ cpu_peak_percent: usage.cpuPeakPercent,
800
807
  cpu_count: usage.cpuCount,
801
808
  memory_total_bytes: usage.memoryTotalBytes,
802
809
  memory_available_bytes: usage.memoryAvailableBytes,
@@ -1000,10 +1007,10 @@ async function status(options = {}) {
1000
1007
  }
1001
1008
 
1002
1009
  // src/lib/claude-usage.ts
1003
- import { execFileSync } from "child_process";
1004
- import { readFileSync } from "fs";
1005
- import { homedir } from "os";
1006
- import { join } from "path";
1010
+ import { execFileSync } from "node:child_process";
1011
+ import { readFileSync } from "node:fs";
1012
+ import { homedir } from "node:os";
1013
+ import { join } from "node:path";
1007
1014
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1008
1015
  var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
1009
1016
  var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
@@ -1088,7 +1095,7 @@ function ownerLookupFailure(error2) {
1088
1095
  }
1089
1096
  async function getClaudeUsageOwner(accessToken) {
1090
1097
  if (cachedOwner?.accessToken === accessToken) {
1091
- return { owner: cachedOwner.owner, ownerLookupError: null };
1098
+ return { subscription: cachedOwner.owner, ownerLookupError: null };
1092
1099
  }
1093
1100
  try {
1094
1101
  const response = await fetch(CLAUDE_PROFILE_URL, {
@@ -1100,27 +1107,27 @@ async function getClaudeUsageOwner(accessToken) {
1100
1107
  signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1101
1108
  });
1102
1109
  if (!response.ok) {
1103
- return { owner: null, ownerLookupError: `HTTP ${response.status}` };
1110
+ return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
1104
1111
  }
1105
1112
  let body;
1106
1113
  try {
1107
1114
  body = await response.json();
1108
1115
  } catch (error2) {
1109
- return { owner: null, ownerLookupError: "malformed response" };
1116
+ return { subscription: null, ownerLookupError: "malformed response" };
1110
1117
  }
1111
1118
  const profile = body;
1112
1119
  if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
1113
- return { owner: null, ownerLookupError: "malformed response" };
1120
+ return { subscription: null, ownerLookupError: "malformed response" };
1114
1121
  }
1115
- const owner = {
1116
- email: profile.account.email,
1122
+ const subscription = {
1123
+ ownerEmail: profile.account.email,
1117
1124
  organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
1118
- rateLimitTier: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1125
+ planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1119
1126
  };
1120
- cachedOwner = { accessToken, owner };
1121
- return { owner, ownerLookupError: null };
1127
+ cachedOwner = { accessToken, owner: subscription };
1128
+ return { subscription, ownerLookupError: null };
1122
1129
  } catch (error2) {
1123
- return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
1130
+ return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
1124
1131
  }
1125
1132
  }
1126
1133
  async function getClaudeUsage() {
@@ -1149,11 +1156,11 @@ async function getClaudeUsage() {
1149
1156
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1150
1157
  }
1151
1158
  const body = await res.json();
1152
- const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1159
+ const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1153
1160
  return {
1154
1161
  fiveHour: toWindow(body.five_hour),
1155
1162
  sevenDay: toWindow(body.seven_day),
1156
- owner,
1163
+ subscription,
1157
1164
  ownerLookupError
1158
1165
  };
1159
1166
  }
@@ -1183,10 +1190,10 @@ async function claudeUsage() {
1183
1190
  }
1184
1191
 
1185
1192
  // src/commands/run.ts
1186
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1187
- import { homedir as homedir5 } from "os";
1188
- import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
1189
- import chalk6 from "chalk";
1193
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
1194
+ import { homedir as homedir6 } from "node:os";
1195
+ import { isAbsolute as isAbsolute3, join as join10, parse, resolve as resolvePath2 } from "node:path";
1196
+ import chalk7 from "chalk";
1190
1197
 
1191
1198
  // ../../packages/types/src/agents/index.ts
1192
1199
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
@@ -1207,6 +1214,7 @@ var TelemetryEventTypes = {
1207
1214
  // ../../packages/types/src/tunnel/index.ts
1208
1215
  var MAX_FRAME_BYTES = 256 * 1024;
1209
1216
  var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
1217
+ var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
1210
1218
 
1211
1219
  // ../../packages/types/src/runner-files.ts
1212
1220
  var MAX_FILE_PUSH_BYTES = 64 * 1024;
@@ -1244,7 +1252,7 @@ function stripQuery(url) {
1244
1252
 
1245
1253
  // src/commands/run.ts
1246
1254
  import ora3 from "ora";
1247
- import { select as select3 } from "@inquirer/prompts";
1255
+ import { select as select4 } from "@inquirer/prompts";
1248
1256
 
1249
1257
  // src/lib/telemetry.ts
1250
1258
  var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
@@ -1417,12 +1425,50 @@ var SEVERITY_BY_LEVEL = {
1417
1425
  warn: "warning",
1418
1426
  error: "error"
1419
1427
  };
1428
+ function parseOpenCodeLogLine(line) {
1429
+ const normalisedLine = line.replace(/\r$/, "");
1430
+ const levelMatch = normalisedLine.match(/(?:^|\s)level=(\w+)/i);
1431
+ if (!levelMatch) return null;
1432
+ const level = levelMatch[1].toUpperCase();
1433
+ if (level !== "WARN" && level !== "ERROR") return null;
1434
+ const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
1435
+ return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
1436
+ }
1437
+ var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1438
+ function createOpenCodeActivityForwarder(getContext) {
1439
+ let buffer = Buffer.alloc(0);
1440
+ const flushLine = (line) => {
1441
+ const parsed = parseOpenCodeLogLine(line);
1442
+ if (!parsed) return;
1443
+ forwardRunnerActivity(
1444
+ {
1445
+ level: parsed.level,
1446
+ error: line,
1447
+ metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
1448
+ source: "opencode"
1449
+ },
1450
+ getContext()
1451
+ );
1452
+ };
1453
+ return (chunk) => {
1454
+ buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
1455
+ let newlineIndex;
1456
+ while ((newlineIndex = buffer.indexOf(10)) !== -1) {
1457
+ flushLine(buffer.subarray(0, newlineIndex).toString("utf-8").replace(/\r$/, ""));
1458
+ buffer = buffer.subarray(newlineIndex + 1);
1459
+ }
1460
+ if (buffer.length > MAX_LINE_BUFFER_BYTES) {
1461
+ flushLine(buffer.toString("utf-8"));
1462
+ buffer = Buffer.alloc(0);
1463
+ }
1464
+ };
1465
+ }
1420
1466
  var MAX_MESSAGE_LENGTH = 500;
1421
1467
  var MAX_METADATA_VALUE_LENGTH = 200;
1422
1468
  var MAX_METADATA_ENTRIES = 20;
1423
1469
  var TRUNCATION_MARKER = "\u2026";
1424
1470
  function redact(message) {
1425
- return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
1471
+ return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
1426
1472
  }
1427
1473
  function truncate(message) {
1428
1474
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
@@ -1448,43 +1494,47 @@ function sanitiseMetadata(metadata) {
1448
1494
  }
1449
1495
  var RATE_LIMIT_WINDOW_MS = 6e4;
1450
1496
  var RATE_LIMIT_MAX_EVENTS = 30;
1451
- var windowStartedAt = 0;
1452
- var windowCount = 0;
1453
- var windowDroppedCount = 0;
1454
- function admitUnderRateLimit(now) {
1455
- if (now - windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1456
- if (windowDroppedCount > 0) {
1497
+ var rateWindows = /* @__PURE__ */ new Map();
1498
+ function admitUnderRateLimit(source, now) {
1499
+ let window = rateWindows.get(source);
1500
+ if (!window) {
1501
+ window = { windowStartedAt: 0, windowCount: 0, windowDroppedCount: 0 };
1502
+ rateWindows.set(source, window);
1503
+ }
1504
+ if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1505
+ if (window.windowDroppedCount > 0) {
1457
1506
  console.error(
1458
- `[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ${windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`
1507
+ `[runner-activity-telemetry] rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min) for source "${source}"`
1459
1508
  );
1460
1509
  }
1461
- windowStartedAt = now;
1462
- windowCount = 0;
1463
- windowDroppedCount = 0;
1510
+ window.windowStartedAt = now;
1511
+ window.windowCount = 0;
1512
+ window.windowDroppedCount = 0;
1464
1513
  }
1465
- if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
1466
- windowDroppedCount++;
1467
- if (windowDroppedCount === 1) {
1514
+ if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1515
+ window.windowDroppedCount++;
1516
+ if (window.windowDroppedCount === 1) {
1468
1517
  console.error(
1469
- `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
1518
+ `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window for source "${source}"`
1470
1519
  );
1471
1520
  }
1472
1521
  return false;
1473
1522
  }
1474
- windowCount++;
1523
+ window.windowCount++;
1475
1524
  return true;
1476
1525
  }
1477
1526
  function forwardRunnerActivity(entry, context) {
1478
1527
  try {
1479
1528
  if (!FORWARDED_LEVELS.has(entry.level)) return;
1480
1529
  if (!context.agentId || !context.authHeader) return;
1481
- if (!admitUnderRateLimit(Date.now())) return;
1530
+ const source = entry.source ?? "cli.run";
1531
+ if (!admitUnderRateLimit(source, Date.now())) return;
1482
1532
  const rawMessage = entry.error ?? entry.message ?? "";
1483
1533
  const message = truncate(redact(rawMessage));
1484
1534
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1485
1535
  severity: SEVERITY_BY_LEVEL[entry.level],
1486
1536
  message,
1487
- metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
1537
+ metadata: { ...sanitiseMetadata(entry.metadata), source },
1488
1538
  agentId: context.agentId
1489
1539
  });
1490
1540
  } catch (err) {
@@ -1495,8 +1545,8 @@ function forwardRunnerActivity(entry, context) {
1495
1545
  }
1496
1546
 
1497
1547
  // src/lib/opencode/session-db-recovery-report.ts
1498
- import { readFileSync as readFileSync2, unlinkSync } from "fs";
1499
- import { join as join2 } from "path";
1548
+ import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
1549
+ import { join as join2 } from "node:path";
1500
1550
  function sessionDbRecoveryReportPath(homeDir, env) {
1501
1551
  const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1502
1552
  return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
@@ -1709,13 +1759,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1709
1759
  }
1710
1760
 
1711
1761
  // src/lib/opencode/session-db-boot.ts
1712
- import { spawn as spawn2 } from "child_process";
1713
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1714
- import { homedir as homedir2 } from "os";
1715
- import { dirname as dirname2, resolve as resolvePath } from "path";
1762
+ import { spawn as spawn2 } from "node:child_process";
1763
+ import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
1764
+ import { homedir as homedir2 } from "node:os";
1765
+ import { dirname as dirname2, resolve as resolvePath } from "node:path";
1716
1766
 
1717
1767
  // src/lib/runner-synchroniser.ts
1718
- import { spawn } from "child_process";
1768
+ import { spawn } from "node:child_process";
1719
1769
  function appendError(stderr, error2) {
1720
1770
  const message = error2 instanceof Error ? error2.message : String(error2);
1721
1771
  return stderr === "" ? message : `${stderr}
@@ -1728,10 +1778,14 @@ function runSynchroniser(args, opts) {
1728
1778
  let stderr = "";
1729
1779
  let settled = false;
1730
1780
  const timer = {};
1781
+ let abortListener;
1782
+ let spawnListener;
1731
1783
  const finish = (result) => {
1732
1784
  if (settled) return;
1733
1785
  settled = true;
1734
1786
  if (timer.handle) clearTimeout(timer.handle);
1787
+ if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
1788
+ if (spawnListener) child.removeListener("spawn", spawnListener);
1735
1789
  resolve4(result);
1736
1790
  };
1737
1791
  try {
@@ -1757,6 +1811,25 @@ function runSynchroniser(args, opts) {
1757
1811
  child.once("close", (code) => {
1758
1812
  finish({ code, stdout, stderr, timedOut: false });
1759
1813
  });
1814
+ if (opts.signal) {
1815
+ const killChild = () => {
1816
+ if (child.pid === void 0) {
1817
+ if (!spawnListener) {
1818
+ spawnListener = killChild;
1819
+ child.once("spawn", spawnListener);
1820
+ }
1821
+ return;
1822
+ }
1823
+ child.kill("SIGKILL");
1824
+ };
1825
+ abortListener = killChild;
1826
+ if (opts.signal.aborted) {
1827
+ abortListener();
1828
+ } else {
1829
+ opts.signal.addEventListener("abort", abortListener, { once: true });
1830
+ if (opts.signal.aborted) abortListener();
1831
+ }
1832
+ }
1760
1833
  timer.handle = setTimeout(
1761
1834
  () => {
1762
1835
  child.kill("SIGKILL");
@@ -2217,9 +2290,9 @@ async function restoreAndVerifySessionDb(options) {
2217
2290
  }
2218
2291
 
2219
2292
  // src/lib/opencode/session-db-provenance.ts
2220
- import { createRequire } from "module";
2221
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2222
- import { dirname as dirname3, join as join3 } from "path";
2293
+ import { createRequire } from "node:module";
2294
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2295
+ import { dirname as dirname3, join as join3 } from "node:path";
2223
2296
  var require2 = createRequire(import.meta.url);
2224
2297
  function readSessionDbMigrationIds(dbPath) {
2225
2298
  let db;
@@ -2413,6 +2486,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2413
2486
 
2414
2487
  // src/lib/opencode/process.ts
2415
2488
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2489
+ var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
2490
+ function resolveOpenCodeLogLevel(env) {
2491
+ const raw = env.OPENCODE_LOG_LEVEL;
2492
+ if (!raw) return "INFO";
2493
+ const upper = raw.toUpperCase();
2494
+ if (VALID_OPENCODE_LOG_LEVELS.has(upper)) return upper;
2495
+ console.warn(
2496
+ `startOpenCode: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected DEBUG|INFO|WARN|ERROR) \u2014 using INFO`
2497
+ );
2498
+ return "INFO";
2499
+ }
2416
2500
  function getProcessCwd(pid) {
2417
2501
  const platform = process.platform;
2418
2502
  try {
@@ -2461,14 +2545,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
2461
2545
  }
2462
2546
  return null;
2463
2547
  }
2464
- function findOpenCodeProcesses() {
2548
+ function findProcessesByPattern(pgrepPattern, psPattern) {
2465
2549
  const instances = [];
2466
2550
  try {
2467
2551
  const platform = process.platform;
2468
2552
  if (platform === "darwin" || platform === "linux") {
2469
2553
  let pids = [];
2470
2554
  try {
2471
- const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
2555
+ const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
2472
2556
  encoding: "utf-8",
2473
2557
  stdio: ["pipe", "pipe", "pipe"]
2474
2558
  }).trim();
@@ -2477,7 +2561,7 @@ function findOpenCodeProcesses() {
2477
2561
  }
2478
2562
  } catch {
2479
2563
  try {
2480
- const psOutput = execSync('ps aux | grep -E "opencode (serve|--port)" | grep -v grep', {
2564
+ const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
2481
2565
  encoding: "utf-8",
2482
2566
  stdio: ["pipe", "pipe", "pipe"]
2483
2567
  }).trim();
@@ -2523,6 +2607,9 @@ function findOpenCodeProcesses() {
2523
2607
  }
2524
2608
  return instances;
2525
2609
  }
2610
+ function findOpenCodeProcesses() {
2611
+ return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
2612
+ }
2526
2613
  async function scanPortsForOpenCode() {
2527
2614
  const instances = [];
2528
2615
  const checks = OPENCODE_PORT_RANGE.map(async (port) => {
@@ -2569,7 +2656,7 @@ async function findHealthyOpenCodeInstances() {
2569
2656
  }
2570
2657
  async function startOpenCode(port, options = {}) {
2571
2658
  let command = "opencode";
2572
- const printLogs = options.inheritStdio ? ["--print-logs"] : [];
2659
+ const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
2573
2660
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
2574
2661
  try {
2575
2662
  execSync("which opencode", { stdio: "ignore" });
@@ -2626,6 +2713,19 @@ function isOpenCodeInstalled() {
2626
2713
  return false;
2627
2714
  }
2628
2715
  }
2716
+ function isOpenCode2Installed() {
2717
+ try {
2718
+ const platform = process.platform;
2719
+ if (platform === "win32") {
2720
+ execSync2("where opencode2", { stdio: "ignore" });
2721
+ } else {
2722
+ execSync2("which opencode2", { stdio: "ignore" });
2723
+ }
2724
+ return true;
2725
+ } catch {
2726
+ return false;
2727
+ }
2728
+ }
2629
2729
  async function promptOpenCodeInstall(interactive) {
2630
2730
  if (!interactive) {
2631
2731
  console.log(
@@ -2635,7 +2735,11 @@ async function promptOpenCodeInstall(interactive) {
2635
2735
  install_url: OPENCODE_INSTALL_URL,
2636
2736
  install_commands: {
2637
2737
  npm: "npm install -g opencode-ai",
2638
- curl: "curl -fsSL https://opencode.ai/install.sh | sh"
2738
+ curl: "curl -fsSL https://opencode.ai/install.sh | sh",
2739
+ v2: {
2740
+ npm: "npm install -g @opencode-ai/cli@beta",
2741
+ curl: "curl -fsSL https://opencode.ai/v2/install | bash"
2742
+ }
2639
2743
  }
2640
2744
  })
2641
2745
  );
@@ -3119,21 +3223,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
3119
3223
  }
3120
3224
  return lastOk ?? last;
3121
3225
  }
3122
- function messageUsage(messages, userMessageId) {
3123
- if (!messages || messages.length === 0) return null;
3124
- const byParentAll = messages.filter(
3125
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3226
+ function collectSubagentSessions(messages, userMessageId) {
3227
+ if (!messages || messages.length === 0) return [];
3228
+ const byParent = messages.filter(
3229
+ (message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
3126
3230
  );
3127
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3128
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3129
- let correlated;
3130
- if (byParent.length > 0) {
3131
- correlated = byParent;
3132
- } else {
3133
- const reply = findAssistantReplyAfter(messages, userMessageId);
3134
- correlated = reply ? [reply] : [];
3231
+ const assistants = byParent.length > 0 ? byParent : [];
3232
+ if (assistants.length === 0) {
3233
+ const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3234
+ if (userIndex === -1) return [];
3235
+ for (let i = userIndex + 1; i < messages.length; i++) {
3236
+ const message = messages[i];
3237
+ if (roleOf(message) === "user") break;
3238
+ if (roleOf(message) === "assistant") assistants.push(message);
3239
+ }
3240
+ }
3241
+ const refs = [];
3242
+ const seen = /* @__PURE__ */ new Set();
3243
+ for (const message of assistants) {
3244
+ const parts = Array.isArray(message.parts) ? message.parts : [];
3245
+ for (const part of parts) {
3246
+ if (!part || typeof part !== "object" || part.type !== "tool" || part.tool !== "task")
3247
+ continue;
3248
+ const state = part.state;
3249
+ if (!state || typeof state !== "object") continue;
3250
+ const metadata = state.metadata;
3251
+ if (!metadata || typeof metadata !== "object") continue;
3252
+ const sessionId = metadata.sessionId;
3253
+ if (typeof sessionId !== "string" || sessionId.length === 0 || seen.has(sessionId)) continue;
3254
+ seen.add(sessionId);
3255
+ const start = state.time?.start;
3256
+ refs.push({
3257
+ sessionId,
3258
+ startedAtMs: typeof start === "number" && Number.isFinite(start) ? start : null
3259
+ });
3260
+ }
3135
3261
  }
3136
- if (correlated.length === 0) return null;
3262
+ return refs;
3263
+ }
3264
+ function finiteNumber(value) {
3265
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3266
+ }
3267
+ function taskCallModel(value) {
3268
+ if (!value || typeof value !== "object") return null;
3269
+ const model = value;
3270
+ const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
3271
+ const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
3272
+ return modelID || providerID ? { modelID, providerID } : null;
3273
+ }
3274
+ function collectTaskCalls(messages, userMessageId) {
3275
+ if (!messages || messages.length === 0) return [];
3276
+ const calls = [];
3277
+ for (const message of messages) {
3278
+ if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
3279
+ for (const part of message.parts ?? []) {
3280
+ if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
3281
+ continue;
3282
+ }
3283
+ const rawName = part.state.input?.subagent_type;
3284
+ const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
3285
+ const metadata = part.state.metadata;
3286
+ calls.push({
3287
+ callID: part.callID,
3288
+ subagentName,
3289
+ childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
3290
+ parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3291
+ model: taskCallModel(metadata?.model),
3292
+ status: part.state.status ?? "unknown",
3293
+ timeStart: finiteNumber(part.state.time?.start),
3294
+ timeEnd: finiteNumber(part.state.time?.end)
3295
+ });
3296
+ }
3297
+ }
3298
+ return calls;
3299
+ }
3300
+ function attributeTaskCallUsage(messages, windows) {
3301
+ const eligibleWindows = windows.filter(
3302
+ (window) => window.timeStart !== null && Number.isFinite(window.timeStart)
3303
+ );
3304
+ const assignments = /* @__PURE__ */ new Map();
3305
+ for (const window of eligibleWindows) assignments.set(window.callID, []);
3306
+ const unattributed = [];
3307
+ for (const message of messages ?? []) {
3308
+ if (roleOf(message) !== "assistant") continue;
3309
+ const created = finiteNumber(createdOf(message));
3310
+ const matching = created === null ? [] : eligibleWindows.filter(
3311
+ (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3312
+ );
3313
+ if (matching.length === 0) {
3314
+ unattributed.push(message);
3315
+ continue;
3316
+ }
3317
+ matching.sort((a, b) => a.timeStart - b.timeStart);
3318
+ assignments.get(matching[0].callID)?.push(message);
3319
+ }
3320
+ return {
3321
+ invocations: eligibleWindows.map((window) => {
3322
+ const assigned = assignments.get(window.callID) ?? [];
3323
+ return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
3324
+ }),
3325
+ unattributed
3326
+ };
3327
+ }
3328
+ function sumAssistantUsage(messages) {
3329
+ if (!messages || messages.length === 0) return null;
3330
+ const nonErrored = messages.filter((message) => errorOf(message) == null);
3331
+ const selected = nonErrored.length > 0 ? nonErrored : messages;
3137
3332
  let sawAnyUsage = false;
3138
3333
  let inputSum = 0;
3139
3334
  let outputSum = 0;
@@ -3144,7 +3339,7 @@ function messageUsage(messages, userMessageId) {
3144
3339
  let sawCost = false;
3145
3340
  let modelId = null;
3146
3341
  let providerId = null;
3147
- for (const m of correlated) {
3342
+ for (const m of selected) {
3148
3343
  const info = m.info;
3149
3344
  if (!info) continue;
3150
3345
  const tokens = info.tokens;
@@ -3179,12 +3374,28 @@ function messageUsage(messages, userMessageId) {
3179
3374
  usage_tokens_reasoning: reasoningSum,
3180
3375
  usage_tokens_cache_read: cacheReadSum,
3181
3376
  usage_tokens_cache_write: cacheWriteSum,
3182
- // NULL means "OpenCode never reported a cost" (never inferred from
3183
- // tokens) distinct from a genuine 0-cost turn, which would set
3184
- // `sawCost` true with `costSum === 0`.
3377
+ // NULL means OpenCode never reported a cost; it is distinct from a genuine
3378
+ // zero-cost message, which sets `sawCost` with `costSum === 0`.
3185
3379
  usage_cost_usd: sawCost ? costSum : null
3186
3380
  };
3187
3381
  }
3382
+ function messageUsage(messages, userMessageId) {
3383
+ if (!messages || messages.length === 0) return null;
3384
+ const byParentAll = messages.filter(
3385
+ (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3386
+ );
3387
+ const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3388
+ const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3389
+ let correlated;
3390
+ if (byParent.length > 0) {
3391
+ correlated = byParent;
3392
+ } else {
3393
+ const reply = findAssistantReplyAfter(messages, userMessageId);
3394
+ correlated = reply ? [reply] : [];
3395
+ }
3396
+ if (correlated.length === 0) return null;
3397
+ return sumAssistantUsage(correlated);
3398
+ }
3188
3399
  function messageRunState(messages, userMessageId) {
3189
3400
  if (!messages || messages.length === 0) return "unknown";
3190
3401
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3247,8 +3458,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
3247
3458
  }
3248
3459
  return false;
3249
3460
  }
3250
- function messageFailure(messages, userMessageId) {
3251
- const reply = findLastAssistantReplyFor(messages, userMessageId);
3461
+ function classifyReplyAuthError(reply) {
3252
3462
  const error2 = errorOf(reply);
3253
3463
  if (error2 == null || typeof error2 !== "object") return null;
3254
3464
  const e = error2;
@@ -3273,6 +3483,32 @@ function messageFailure(messages, userMessageId) {
3273
3483
  }
3274
3484
  return null;
3275
3485
  }
3486
+ function messageFailure(messages, userMessageId) {
3487
+ return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3488
+ }
3489
+ function findLatestSubagentAuthOutcome(messages, sinceMs) {
3490
+ if (!messages || messages.length === 0) return null;
3491
+ for (let i = messages.length - 1; i >= 0; i--) {
3492
+ const message = messages[i];
3493
+ if (roleOf(message) !== "assistant") continue;
3494
+ const created = createdOf(message);
3495
+ if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
3496
+ const failure = classifyReplyAuthError(message);
3497
+ if (failure) {
3498
+ if (!failure.providerId) return null;
3499
+ return { providerId: failure.providerId, outcome: "failed", failure };
3500
+ }
3501
+ const providerId = message.info?.providerID;
3502
+ if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
3503
+ return { providerId, outcome: "succeeded" };
3504
+ }
3505
+ return null;
3506
+ }
3507
+ return null;
3508
+ }
3509
+ function findSubagentAuthOutcome(messages, sinceMs) {
3510
+ return findLatestSubagentAuthOutcome(messages, sinceMs);
3511
+ }
3276
3512
  function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
3277
3513
  if (classified != null) return classified;
3278
3514
  if (hasConfiguredProvider !== false) return null;
@@ -3289,6 +3525,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3289
3525
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3290
3526
  );
3291
3527
  }
3528
+ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
3529
+ if (!messages || messages.length === 0) return false;
3530
+ const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3531
+ if (userIndex === -1) return false;
3532
+ let hasLaterUser = false;
3533
+ let hasStartedLaterUser = false;
3534
+ for (let i = userIndex + 1; i < messages.length; i++) {
3535
+ const message = messages[i];
3536
+ if (roleOf(message) !== "user") continue;
3537
+ hasLaterUser = true;
3538
+ const laterUserMessageId = idOf(message);
3539
+ if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
3540
+ return false;
3541
+ }
3542
+ if (messages.some(
3543
+ (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3544
+ )) {
3545
+ hasStartedLaterUser = true;
3546
+ }
3547
+ }
3548
+ return hasLaterUser && hasStartedLaterUser;
3549
+ }
3292
3550
  async function hasAnyConfiguredProvider(port) {
3293
3551
  try {
3294
3552
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
@@ -3320,6 +3578,94 @@ async function hasAnyConfiguredProvider(port) {
3320
3578
  return null;
3321
3579
  }
3322
3580
  }
3581
+ function sessionErrorReason(error2) {
3582
+ const record = typeof error2 === "object" && error2 !== null ? error2 : null;
3583
+ const data = record?.data;
3584
+ const dataRecord = typeof data === "object" && data !== null ? data : null;
3585
+ const rawReason = typeof dataRecord?.message === "string" && dataRecord.message || typeof record?.message === "string" && record.message || typeof error2 === "string" && error2 || typeof record?.name === "string" && record.name || "OpenCode reported a session error with no details";
3586
+ const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
3587
+ return reason || "OpenCode reported a session error with no details";
3588
+ }
3589
+ function parseSessionErrorFrame(data) {
3590
+ let parsed;
3591
+ try {
3592
+ parsed = JSON.parse(data);
3593
+ } catch (error2) {
3594
+ void error2;
3595
+ return null;
3596
+ }
3597
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3598
+ const parsedRecord = parsed;
3599
+ const payload = parsedRecord.payload;
3600
+ const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
3601
+ if (event.type !== "session.error") return null;
3602
+ const properties = event.properties;
3603
+ if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
3604
+ return null;
3605
+ }
3606
+ const propertiesRecord = properties;
3607
+ const sessionId = propertiesRecord.sessionID;
3608
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
3609
+ return {
3610
+ sessionId,
3611
+ reason: sessionErrorReason(propertiesRecord.error)
3612
+ };
3613
+ }
3614
+ async function readSessionErrorStream(port, options) {
3615
+ let reader = null;
3616
+ try {
3617
+ const response = await fetch(`${opencodeBase(port)}/event`, {
3618
+ headers: { accept: "text/event-stream" },
3619
+ signal: options.signal
3620
+ });
3621
+ if (!response.ok || !response.body) {
3622
+ return { reason: "unavailable", detail: `HTTP ${response.status}` };
3623
+ }
3624
+ reader = response.body.getReader();
3625
+ const decoder = new TextDecoder();
3626
+ let buffer = "";
3627
+ const processLine = (line) => {
3628
+ const trimmed = line.trimEnd();
3629
+ if (!trimmed.startsWith("data:")) return;
3630
+ const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
3631
+ if (event) options.onSessionError(event);
3632
+ };
3633
+ while (true) {
3634
+ const { done, value } = await reader.read();
3635
+ if (done) return { reason: "ended" };
3636
+ buffer += decoder.decode(value, { stream: true });
3637
+ const lines = buffer.split("\n");
3638
+ buffer = lines.pop() ?? "";
3639
+ for (const line of lines) processLine(line);
3640
+ }
3641
+ } catch (err) {
3642
+ if (options.signal.aborted) return { reason: "aborted" };
3643
+ return {
3644
+ reason: "unavailable",
3645
+ detail: err instanceof Error ? err.message : String(err)
3646
+ };
3647
+ } finally {
3648
+ if (reader) void reader.cancel().catch(() => void 0);
3649
+ }
3650
+ }
3651
+ async function reloadProviderCache(port) {
3652
+ try {
3653
+ const res = await timedFetch(`${opencodeBase(port)}/config`, {
3654
+ method: "PATCH",
3655
+ headers: { "Content-Type": "application/json" },
3656
+ body: JSON.stringify({})
3657
+ });
3658
+ if (!res.ok) {
3659
+ console.error(
3660
+ `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
3661
+ );
3662
+ }
3663
+ } catch (err) {
3664
+ console.error(
3665
+ `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3666
+ );
3667
+ }
3668
+ }
3323
3669
 
3324
3670
  // src/lib/opencode/session-cleanup.ts
3325
3671
  var DURATION_UNIT_MS = {
@@ -3426,8 +3772,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
3426
3772
  }
3427
3773
 
3428
3774
  // src/lib/opencode/session-db-size.ts
3429
- import { statSync as statSync3 } from "fs";
3430
- import { join as join4 } from "path";
3775
+ import { statSync as statSync3 } from "node:fs";
3776
+ import { join as join4 } from "node:path";
3431
3777
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
3432
3778
  function statSessionDbBytes(homeDir) {
3433
3779
  const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
@@ -3457,9 +3803,96 @@ function buildSessionStoreSizeWarning(input) {
3457
3803
  return null;
3458
3804
  }
3459
3805
 
3806
+ // src/lib/opencode/log-tail.ts
3807
+ import { statSync as statSync4 } from "node:fs";
3808
+ import { homedir as homedir3 } from "node:os";
3809
+ import { join as join5 } from "node:path";
3810
+ import { open as open2, stat } from "node:fs/promises";
3811
+ var DEFAULT_POLL_INTERVAL_MS = 1e3;
3812
+ function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
3813
+ const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
3814
+ return join5(dataDir, "opencode", "log", "opencode.log");
3815
+ }
3816
+ function isEnoent(error2) {
3817
+ return error2?.code === "ENOENT";
3818
+ }
3819
+ function reportFailure(operation, logPath, error2) {
3820
+ console.error(
3821
+ `[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
3822
+ );
3823
+ }
3824
+ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
3825
+ const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
3826
+ let offset = 0;
3827
+ let inode = null;
3828
+ let baselineReady = true;
3829
+ try {
3830
+ const initial = statSync4(logPath);
3831
+ offset = initial.size;
3832
+ inode = initial.ino;
3833
+ } catch (error2) {
3834
+ if (!isEnoent(error2)) {
3835
+ reportFailure("initial stat", logPath, error2);
3836
+ baselineReady = false;
3837
+ }
3838
+ }
3839
+ let polling = false;
3840
+ let stopped = false;
3841
+ const poll = async () => {
3842
+ if (polling || stopped) return;
3843
+ polling = true;
3844
+ try {
3845
+ let current;
3846
+ try {
3847
+ current = await stat(logPath);
3848
+ } catch (error2) {
3849
+ if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
3850
+ return;
3851
+ }
3852
+ if (!baselineReady) {
3853
+ offset = current.size;
3854
+ inode = current.ino;
3855
+ baselineReady = true;
3856
+ return;
3857
+ }
3858
+ if (inode !== null && current.ino !== inode || current.size < offset) {
3859
+ offset = 0;
3860
+ }
3861
+ inode = current.ino;
3862
+ if (current.size === offset) return;
3863
+ const length = current.size - offset;
3864
+ const fh = await open2(logPath, "r");
3865
+ try {
3866
+ const buf = Buffer.alloc(length);
3867
+ const { bytesRead } = await fh.read(buf, 0, length, offset);
3868
+ offset += bytesRead;
3869
+ if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
3870
+ } finally {
3871
+ await fh.close();
3872
+ }
3873
+ } catch (error2) {
3874
+ if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
3875
+ } finally {
3876
+ polling = false;
3877
+ }
3878
+ };
3879
+ const interval = setInterval(() => void poll(), pollIntervalMs);
3880
+ void poll();
3881
+ return {
3882
+ stop: () => {
3883
+ stopped = true;
3884
+ clearInterval(interval);
3885
+ }
3886
+ };
3887
+ }
3888
+
3460
3889
  // src/lib/opencode/session-db-reclaim.ts
3461
- import { statSync as statSync4, statfsSync } from "fs";
3462
- import { dirname as dirname4 } from "path";
3890
+ import { statSync as statSync5, statfsSync } from "node:fs";
3891
+ import { dirname as dirname4 } from "node:path";
3892
+ function errorMessage(error2) {
3893
+ if (!(error2 instanceof Error)) return String(error2);
3894
+ return error2.cause instanceof Error ? error2.cause.message : error2.message;
3895
+ }
3463
3896
  function insufficientSpaceReason(dbPath, requiredBytes) {
3464
3897
  try {
3465
3898
  const fsStats = statfsSync(dirname4(dbPath));
@@ -3485,17 +3918,17 @@ async function probeReclaimAvailability(input) {
3485
3918
  const { dbPath, requiredBytes } = input;
3486
3919
  let sqlite;
3487
3920
  try {
3488
- sqlite = await import("sqlite");
3921
+ sqlite = await import("node:sqlite");
3489
3922
  } catch (err) {
3490
- console.warn(
3491
- `[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3492
- );
3493
- return "sqlite-unavailable";
3923
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
3924
+ console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
3925
+ return { reason: "sqlite-unavailable", detail };
3494
3926
  }
3495
3927
  let autoVacuum = null;
3496
3928
  try {
3497
3929
  const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
3498
3930
  try {
3931
+ db.exec("PRAGMA busy_timeout=5000");
3499
3932
  autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3500
3933
  } finally {
3501
3934
  db.close();
@@ -3506,23 +3939,25 @@ async function probeReclaimAvailability(input) {
3506
3939
  );
3507
3940
  }
3508
3941
  if (autoVacuum !== 0) return null;
3509
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
3942
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
3510
3943
  }
3511
3944
  async function reclaimSessionDbSpace(input) {
3512
3945
  const { dbPath, maxPages, allowFullVacuum = true } = input;
3513
3946
  let sqlite;
3514
3947
  try {
3515
- sqlite = await import("sqlite");
3948
+ sqlite = await import("node:sqlite");
3516
3949
  } catch (err) {
3950
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
3517
3951
  console.warn(
3518
- `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3952
+ `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
3519
3953
  );
3520
- return { ok: false, skipped: "sqlite-unavailable" };
3954
+ return { ok: false, skipped: "sqlite-unavailable", detail };
3521
3955
  }
3522
3956
  const { DatabaseSync } = sqlite;
3523
3957
  let db;
3524
3958
  try {
3525
3959
  db = new DatabaseSync(dbPath);
3960
+ db.exec("PRAGMA busy_timeout=5000");
3526
3961
  const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3527
3962
  if (autoVacuum === 0) {
3528
3963
  if (!allowFullVacuum) {
@@ -3531,7 +3966,7 @@ async function reclaimSessionDbSpace(input) {
3531
3966
  );
3532
3967
  return { ok: false, skipped: "full-vacuum-blocked" };
3533
3968
  }
3534
- const fileBytesForGuard = statSync4(dbPath).size;
3969
+ const fileBytesForGuard = statSync5(dbPath).size;
3535
3970
  const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
3536
3971
  if (skipReason !== null) {
3537
3972
  console.warn(
@@ -3559,10 +3994,12 @@ async function reclaimSessionDbSpace(input) {
3559
3994
  );
3560
3995
  return { ok: false, skipped: "auto-vacuum-not-applicable" };
3561
3996
  } catch (err) {
3562
- console.error(
3563
- `[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3564
- );
3565
- return { ok: false, skipped: "reclaim-error" };
3997
+ console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
3998
+ return {
3999
+ ok: false,
4000
+ skipped: "reclaim-error",
4001
+ detail: errorMessage(err)
4002
+ };
3566
4003
  } finally {
3567
4004
  db?.close();
3568
4005
  }
@@ -3603,7 +4040,6 @@ var StreamForwarder = class {
3603
4040
  handleFrame(frame) {
3604
4041
  switch (frame.type) {
3605
4042
  case "open":
3606
- this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
3607
4043
  void this.handleOpen(frame);
3608
4044
  break;
3609
4045
  case "req_data":
@@ -3639,12 +4075,21 @@ var StreamForwarder = class {
3639
4075
  const { sid, method, path, headers, has_body } = frame;
3640
4076
  const correlationId = headers?.[CORRELATION_ID_HEADER];
3641
4077
  const startedAt = Date.now();
4078
+ if (path !== TUNNEL_DRAIN_PING_PATH && path !== TUNNEL_USAGE_REARM_PING_PATH) {
4079
+ this.callbacks.onOpen?.(sid, method, path);
4080
+ }
3642
4081
  if (path === TUNNEL_DRAIN_PING_PATH) {
3643
4082
  this.callbacks.onDrainPing?.();
3644
4083
  this.send({ type: "head", sid, status: 204, headers: {} });
3645
4084
  this.send({ type: "res_end", sid });
3646
4085
  return;
3647
4086
  }
4087
+ if (path === TUNNEL_USAGE_REARM_PING_PATH) {
4088
+ this.callbacks.onUsageRearmPing?.();
4089
+ this.send({ type: "head", sid, status: 204, headers: {} });
4090
+ this.send({ type: "res_end", sid });
4091
+ return;
4092
+ }
3648
4093
  if (process.env.DEBUG) {
3649
4094
  log("debug", "agent_request", {
3650
4095
  correlation_id: correlationId,
@@ -3789,7 +4234,8 @@ function connectTunnel(options) {
3789
4234
  onResponse,
3790
4235
  onInfo,
3791
4236
  onWarning,
3792
- onDrainPing
4237
+ onDrainPing,
4238
+ onUsageRearmPing
3793
4239
  } = options;
3794
4240
  const tunnelUrl = getTunnelUrlConfig();
3795
4241
  const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
@@ -3801,7 +4247,8 @@ function connectTunnel(options) {
3801
4247
  });
3802
4248
  const forwarder = new StreamForwarder(ws, port, {
3803
4249
  onHead: () => onResponse?.(),
3804
- onDrainPing: () => onDrainPing?.()
4250
+ onDrainPing: () => onDrainPing?.(),
4251
+ onUsageRearmPing: () => onUsageRearmPing?.()
3805
4252
  });
3806
4253
  const connectionTimeout = setTimeout(() => {
3807
4254
  ws.close();
@@ -3844,8 +4291,8 @@ function connectTunnel(options) {
3844
4291
  try {
3845
4292
  message = JSON.parse(data.toString());
3846
4293
  } catch (error2) {
3847
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
3848
- onError?.(`Failed to handle message: ${errorMessage}`);
4294
+ const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
4295
+ onError?.(`Failed to handle message: ${errorMessage3}`);
3849
4296
  return;
3850
4297
  }
3851
4298
  if (isStreamFrame(message)) {
@@ -3962,6 +4409,7 @@ var RunnerConnection = class {
3962
4409
  onError: (error2) => events.onError?.(error2),
3963
4410
  onResponse: () => events.onResponse?.(),
3964
4411
  onDrainPing: () => events.onDrainPing?.(),
4412
+ onUsageRearmPing: () => events.onUsageRearmPing?.(),
3965
4413
  onInfo: (message) => events.onInfo?.(message),
3966
4414
  onWarning: (message) => events.onWarning?.(message)
3967
4415
  });
@@ -3988,7 +4436,7 @@ var RunnerConnection = class {
3988
4436
  };
3989
4437
 
3990
4438
  // src/lib/tunnel/ready-marker.ts
3991
- import { writeFileSync as writeFileSync3 } from "fs";
4439
+ import { writeFileSync as writeFileSync3 } from "node:fs";
3992
4440
  function writeTunnelReadyMarker(path, agentId) {
3993
4441
  try {
3994
4442
  writeFileSync3(path, `${agentId}
@@ -4000,7 +4448,7 @@ function writeTunnelReadyMarker(path, agentId) {
4000
4448
  }
4001
4449
 
4002
4450
  // src/lib/replication.ts
4003
- import { spawn as spawn4 } from "child_process";
4451
+ import { spawn as spawn4 } from "node:child_process";
4004
4452
  function startSessionDbReplication(configPath) {
4005
4453
  return spawn4("litestream", ["replicate", "-config", configPath], {
4006
4454
  stdio: "inherit"
@@ -4016,7 +4464,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
4016
4464
  }
4017
4465
 
4018
4466
  // src/lib/process-liveness.ts
4019
- import { readFileSync as readFileSync4 } from "fs";
4467
+ import { readFileSync as readFileSync4 } from "node:fs";
4020
4468
  function isProcessAlive(pid) {
4021
4469
  try {
4022
4470
  process.kill(pid, 0);
@@ -4042,9 +4490,9 @@ function isProcessAlive(pid) {
4042
4490
  }
4043
4491
 
4044
4492
  // src/lib/openai-usage.ts
4045
- import { readFileSync as readFileSync5 } from "fs";
4046
- import { homedir as homedir3 } from "os";
4047
- import { join as join5 } from "path";
4493
+ import { readFileSync as readFileSync5 } from "node:fs";
4494
+ import { homedir as homedir4 } from "node:os";
4495
+ import { join as join6 } from "node:path";
4048
4496
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4049
4497
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4050
4498
  var OpenAiUsageError = class extends Error {
@@ -4058,7 +4506,7 @@ function isLocalCredentialProblem2(err) {
4058
4506
  }
4059
4507
  function readOpenCodeChatGptCredentials() {
4060
4508
  try {
4061
- const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4509
+ const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4062
4510
  let parsed;
4063
4511
  try {
4064
4512
  parsed = JSON.parse(raw);
@@ -4080,6 +4528,23 @@ function readOpenCodeChatGptCredentials() {
4080
4528
  return null;
4081
4529
  }
4082
4530
  }
4531
+ function parseChatGptIdentity(accessToken) {
4532
+ const segments = accessToken.split(".");
4533
+ if (segments.length !== 3) return null;
4534
+ let payload;
4535
+ try {
4536
+ const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
4537
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
4538
+ payload = parsed;
4539
+ } catch {
4540
+ return null;
4541
+ }
4542
+ const profile = payload["https://api.openai.com/profile"];
4543
+ const auth = payload["https://api.openai.com/auth"];
4544
+ const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4545
+ const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4546
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
4547
+ }
4083
4548
  function toWindow2(headers, name) {
4084
4549
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
4085
4550
  const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
@@ -4155,6 +4620,7 @@ async function getOpenAiUsage(port) {
4155
4620
  "credentials_expired"
4156
4621
  );
4157
4622
  }
4623
+ const subscription = parseChatGptIdentity(credentials2.accessToken);
4158
4624
  const models = await resolveProbeModels(port);
4159
4625
  if (models.length === 0) {
4160
4626
  throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
@@ -4187,7 +4653,7 @@ async function getOpenAiUsage(port) {
4187
4653
  "no_usable_window"
4188
4654
  );
4189
4655
  }
4190
- return usage;
4656
+ return { ...usage, subscription };
4191
4657
  }
4192
4658
  if (res.status === 401) {
4193
4659
  throw new OpenAiUsageError(
@@ -4302,8 +4768,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
4302
4768
  }
4303
4769
 
4304
4770
  // src/lib/resource-usage.ts
4305
- import { cpus, totalmem, freemem } from "os";
4306
- import { statfsSync as statfsSync2 } from "fs";
4771
+ import { cpus, totalmem, freemem } from "node:os";
4772
+ import { statfsSync as statfsSync2 } from "node:fs";
4307
4773
 
4308
4774
  // src/lib/ecs-task-metadata.ts
4309
4775
  var ECS_METADATA_TIMEOUT_MS = 2e3;
@@ -4388,58 +4854,97 @@ function readDisk(homeDir) {
4388
4854
  };
4389
4855
  }
4390
4856
  }
4391
- function createResourceUsageCollector(homeDir) {
4392
- let previous = readCpuSample();
4393
- return async () => {
4857
+ var CPU_PEAK_WINDOW_MS = 6e4;
4858
+ var CPU_PEAK_WINDOW_SAMPLE_COUNT = 4;
4859
+ var CPU_PEAK_SAMPLE_INTERVAL_MS = CPU_PEAK_WINDOW_MS / CPU_PEAK_WINDOW_SAMPLE_COUNT;
4860
+ function createCpuPeakSampler() {
4861
+ const sampleHistory = new Array(CPU_PEAK_WINDOW_SAMPLE_COUNT);
4862
+ sampleHistory[0] = readCpuSample();
4863
+ let nextSampleIndex = 1;
4864
+ let sampleCount = 1;
4865
+ let peak = null;
4866
+ const timer = setInterval(() => {
4394
4867
  const current = readCpuSample();
4395
- const hostCpuPercent = cpuPercentBetween(previous, current);
4396
- const hostCpuCount = cpus().length;
4397
- previous = current;
4398
- const disk = readDisk(homeDir);
4399
- const opencodeDbBytes = statSessionDbBytes(homeDir);
4400
- const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
4401
- const warnings = [];
4402
- if (disk.warning) warnings.push(disk.warning);
4403
- if (ecsWarning) warnings.push(ecsWarning);
4404
- let cpuPercent = hostCpuPercent;
4405
- let cpuCount = hostCpuCount;
4406
- let memoryTotalBytes = totalmem();
4407
- let memoryAvailableBytes = freemem();
4408
- if (limits !== null) {
4409
- cpuCount = limits.cpuCount;
4410
- memoryTotalBytes = limits.memoryTotalBytes;
4411
- memoryAvailableBytes = clamp(
4412
- limits.memoryTotalBytes - (totalmem() - freemem()),
4413
- 0,
4414
- limits.memoryTotalBytes
4415
- );
4416
- cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
4868
+ const sampleFromWindowAgo = sampleCount === CPU_PEAK_WINDOW_SAMPLE_COUNT ? sampleHistory[nextSampleIndex] : void 0;
4869
+ if (sampleFromWindowAgo !== void 0) {
4870
+ const percentage = cpuPercentBetween(sampleFromWindowAgo, current);
4871
+ if (percentage !== null) {
4872
+ peak = peak === null ? percentage : Math.max(peak, percentage);
4873
+ }
4417
4874
  }
4418
- return {
4419
- usage: {
4420
- cpuPercent,
4421
- cpuCount,
4422
- memoryTotalBytes,
4423
- memoryAvailableBytes,
4424
- diskTotalBytes: disk.totalBytes,
4425
- diskFreeBytes: disk.freeBytes,
4426
- opencodeDbBytes
4427
- },
4428
- warnings
4429
- };
4875
+ sampleHistory[nextSampleIndex] = current;
4876
+ nextSampleIndex = (nextSampleIndex + 1) % CPU_PEAK_WINDOW_SAMPLE_COUNT;
4877
+ sampleCount = Math.min(sampleCount + 1, CPU_PEAK_WINDOW_SAMPLE_COUNT);
4878
+ }, CPU_PEAK_SAMPLE_INTERVAL_MS);
4879
+ return {
4880
+ takeAndReset: () => {
4881
+ const currentPeak = peak;
4882
+ peak = null;
4883
+ return currentPeak;
4884
+ },
4885
+ stop: () => clearInterval(timer)
4886
+ };
4887
+ }
4888
+ function createResourceUsageCollector(homeDir) {
4889
+ let previous = readCpuSample();
4890
+ const cpuPeakSampler = createCpuPeakSampler();
4891
+ return {
4892
+ collect: async () => {
4893
+ const current = readCpuSample();
4894
+ const hostCpuPercent = cpuPercentBetween(previous, current);
4895
+ const hostCpuPeakPercent = cpuPeakSampler.takeAndReset();
4896
+ const hostCpuCount = cpus().length;
4897
+ previous = current;
4898
+ const disk = readDisk(homeDir);
4899
+ const opencodeDbBytes = statSessionDbBytes(homeDir);
4900
+ const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
4901
+ const warnings = [];
4902
+ if (disk.warning) warnings.push(disk.warning);
4903
+ if (ecsWarning) warnings.push(ecsWarning);
4904
+ let cpuPercent = hostCpuPercent;
4905
+ let cpuPeakPercent = hostCpuPeakPercent;
4906
+ let cpuCount = hostCpuCount;
4907
+ let memoryTotalBytes = totalmem();
4908
+ let memoryAvailableBytes = freemem();
4909
+ if (limits !== null) {
4910
+ cpuCount = limits.cpuCount;
4911
+ memoryTotalBytes = limits.memoryTotalBytes;
4912
+ memoryAvailableBytes = clamp(
4913
+ limits.memoryTotalBytes - (totalmem() - freemem()),
4914
+ 0,
4915
+ limits.memoryTotalBytes
4916
+ );
4917
+ cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
4918
+ cpuPeakPercent = hostCpuPeakPercent === null ? null : clamp(round2(hostCpuPeakPercent * hostCpuCount / limits.cpuCount), 0, 100);
4919
+ }
4920
+ return {
4921
+ usage: {
4922
+ cpuPercent,
4923
+ cpuPeakPercent,
4924
+ cpuCount,
4925
+ memoryTotalBytes,
4926
+ memoryAvailableBytes,
4927
+ diskTotalBytes: disk.totalBytes,
4928
+ diskFreeBytes: disk.freeBytes,
4929
+ opencodeDbBytes
4930
+ },
4931
+ warnings
4932
+ };
4933
+ },
4934
+ stop: cpuPeakSampler.stop
4430
4935
  };
4431
4936
  }
4432
4937
 
4433
4938
  // src/lib/channels/driver.ts
4434
- import { homedir as homedir4 } from "os";
4939
+ import { homedir as homedir5 } from "node:os";
4435
4940
 
4436
4941
  // src/lib/runner-file-sync.ts
4437
- import { join as join7 } from "path";
4942
+ import { join as join8 } from "node:path";
4438
4943
 
4439
4944
  // src/lib/file-push.ts
4440
- import { randomUUID } from "crypto";
4441
- import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
4442
- import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
4945
+ import { randomUUID } from "node:crypto";
4946
+ import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
4947
+ import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
4443
4948
  var FILE_MODE = 384;
4444
4949
  var DIRECTORY_MODE = 448;
4445
4950
  async function writePushedFile(request) {
@@ -4472,7 +4977,7 @@ async function writePushedFile(request) {
4472
4977
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
4473
4978
  dirname5(candidate)
4474
4979
  );
4475
- const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
4980
+ const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
4476
4981
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
4477
4982
  if (allowedDirectory === null) {
4478
4983
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -4508,7 +5013,7 @@ function expandAndValidate(requestedPath, homeDir) {
4508
5013
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
4509
5014
  return null;
4510
5015
  }
4511
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
5016
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4512
5017
  if (expanded.split(/[/\\]/).includes("..")) {
4513
5018
  return null;
4514
5019
  }
@@ -4581,16 +5086,16 @@ function contains(realDirectory, realTarget) {
4581
5086
  async function createMissingDirectories(existingAncestor, missingSegments) {
4582
5087
  let current = existingAncestor;
4583
5088
  for (const segment of missingSegments) {
4584
- current = join6(current, segment);
5089
+ current = join7(current, segment);
4585
5090
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
4586
5091
  await chmod(current, DIRECTORY_MODE);
4587
5092
  }
4588
5093
  }
4589
5094
  async function writeAtomically(realTarget, content) {
4590
- const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
5095
+ const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
4591
5096
  let handle;
4592
5097
  try {
4593
- handle = await open2(temporaryPath, "wx", FILE_MODE);
5098
+ handle = await open3(temporaryPath, "wx", FILE_MODE);
4594
5099
  await handle.writeFile(content);
4595
5100
  await handle.chmod(FILE_MODE);
4596
5101
  await handle.close();
@@ -4717,12 +5222,12 @@ var NOT_APPLIED = {
4717
5222
  opencodeAuthApplied: false
4718
5223
  };
4719
5224
  function isClaudeCredentialPath(requestedPath, homeDir) {
4720
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4721
- return expanded === join7(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
5225
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5226
+ return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4722
5227
  }
4723
5228
  function isOpenCodeAuthPath(requestedPath, homeDir) {
4724
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4725
- return expanded === join7(homeDir, ...OPENCODE_AUTH_SEGMENTS);
5229
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5230
+ return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
4726
5231
  }
4727
5232
  async function applyOne(options, file) {
4728
5233
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -4882,6 +5387,10 @@ var DEFAULT_RETRY_POLICY = {
4882
5387
  baseDelayMs: 500,
4883
5388
  maxDelayMs: 3e4
4884
5389
  };
5390
+ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
5391
+ var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
5392
+ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
5393
+ var MAX_BUFFERED_SESSION_ERRORS = 256;
4885
5394
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
4886
5395
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
4887
5396
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5022,6 +5531,17 @@ var ChannelDriver = class _ChannelDriver {
5022
5531
  * message; it is removed once its in-flight set empties.
5023
5532
  */
5024
5533
  watchers = /* @__PURE__ */ new Map();
5534
+ sessionErrorStream = null;
5535
+ /**
5536
+ * Session-error failures currently being reported; entries are empty at rest
5537
+ * because each handoff deletes its id in `finally`.
5538
+ */
5539
+ sessionErrorHandled = /* @__PURE__ */ new Set();
5540
+ /**
5541
+ * Session errors that arrived before their dispatch was registered. Bounded FIFO
5542
+ * with a short TTL so an unmatched session cannot retain an event indefinitely.
5543
+ */
5544
+ bufferedSessionErrors = /* @__PURE__ */ new Map();
5025
5545
  /**
5026
5546
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5027
5547
  * dispatched and are still in-flight. A message in this set is never
@@ -5205,6 +5725,13 @@ var ChannelDriver = class _ChannelDriver {
5205
5725
  * no watcher) can resolve the title.
5206
5726
  */
5207
5727
  sessionTitles = /* @__PURE__ */ new Map();
5728
+ /** One best-effort terminal subagent collection per Evident message id. */
5729
+ subagentInvocationCollections = /* @__PURE__ */ new Map();
5730
+ /**
5731
+ * Early snapshots are only liveness hints; they must not become the terminal
5732
+ * collection when the task parts or child transcript have advanced.
5733
+ */
5734
+ subagentInvocationPrefetches = /* @__PURE__ */ new Map();
5208
5735
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5209
5736
  draining = false;
5210
5737
  /**
@@ -5249,6 +5776,7 @@ var ChannelDriver = class _ChannelDriver {
5249
5776
  * and stops opencode.
5250
5777
  */
5251
5778
  stopped = false;
5779
+ recycleRequestedFlag = false;
5252
5780
  constructor(config) {
5253
5781
  this.agentId = config.agentId;
5254
5782
  this.port = config.port;
@@ -5268,7 +5796,7 @@ var ChannelDriver = class _ChannelDriver {
5268
5796
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
5269
5797
  this.now = config.now ?? (() => Date.now());
5270
5798
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
5271
- this.homeDir = config.homeDir ?? homedir4();
5799
+ this.homeDir = config.homeDir ?? homedir5();
5272
5800
  this.maxActiveSessions = config.maxActiveSessions;
5273
5801
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5274
5802
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -5354,6 +5882,9 @@ var ChannelDriver = class _ChannelDriver {
5354
5882
  let dispatched = 0;
5355
5883
  try {
5356
5884
  const conversations = await this.getPendingConversations();
5885
+ if (this.recycleRequestedFlag) {
5886
+ this.stop();
5887
+ }
5357
5888
  if (conversations.length > 0) {
5358
5889
  const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
5359
5890
  this.log({
@@ -5415,6 +5946,21 @@ var ChannelDriver = class _ChannelDriver {
5415
5946
  }
5416
5947
  return ids;
5417
5948
  }
5949
+ /**
5950
+ * OpenCode user-message ids tracked for other Evident messages in a session.
5951
+ * Excluding this message makes an unattributed later row fail safe; a missing
5952
+ * watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
5953
+ */
5954
+ siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
5955
+ const ids = /* @__PURE__ */ new Set();
5956
+ if (!watcher) return ids;
5957
+ for (const inFlight of watcher.inFlight.values()) {
5958
+ if (inFlight.evidentMessageId !== ownEvidentMessageId) {
5959
+ ids.add(inFlight.opencodeMessageId);
5960
+ }
5961
+ }
5962
+ return ids;
5963
+ }
5418
5964
  /**
5419
5965
  * File-pull work, for `run.ts`'s idle accounting (#559).
5420
5966
  *
@@ -5481,6 +6027,16 @@ var ChannelDriver = class _ChannelDriver {
5481
6027
  */
5482
6028
  stop() {
5483
6029
  this.stopped = true;
6030
+ this.sessionErrorStream?.abort.abort();
6031
+ this.sessionErrorStream = null;
6032
+ }
6033
+ /**
6034
+ * The server clears this request when a new MicroVM identity is recorded, so a
6035
+ * same-VM tunnel reconnect does not consume it. This is a plain read rather
6036
+ * than a consume; `run.ts` guards the action once-only.
6037
+ */
6038
+ get recycleRequested() {
6039
+ return this.recycleRequestedFlag;
5484
6040
  }
5485
6041
  /**
5486
6042
  * Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
@@ -5547,6 +6103,7 @@ var ChannelDriver = class _ChannelDriver {
5547
6103
  */
5548
6104
  async processConversation(conv) {
5549
6105
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6106
+ this.ensureSessionErrorStream();
5550
6107
  const messages = await this.getPendingMessages(conv.id);
5551
6108
  let dispatched = 0;
5552
6109
  let skippedAlreadyDispatched = 0;
@@ -5619,7 +6176,7 @@ var ChannelDriver = class _ChannelDriver {
5619
6176
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
5620
6177
  break;
5621
6178
  }
5622
- const errorMessage = err instanceof Error ? err.message : String(err);
6179
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
5623
6180
  this.sessions.delete(conv.id);
5624
6181
  this.supersede(conv.id, sessionId);
5625
6182
  this.log({
@@ -5628,7 +6185,7 @@ var ChannelDriver = class _ChannelDriver {
5628
6185
  conversation_id: conv.id,
5629
6186
  message_id: message.id
5630
6187
  });
5631
- await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
6188
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5632
6189
  this.log({
5633
6190
  level: "warn",
5634
6191
  message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -5639,7 +6196,7 @@ var ChannelDriver = class _ChannelDriver {
5639
6196
  });
5640
6197
  this.log({
5641
6198
  level: "error",
5642
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage}`,
6199
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
5643
6200
  conversation_id: conv.id,
5644
6201
  message_id: message.id
5645
6202
  });
@@ -5660,14 +6217,14 @@ var ChannelDriver = class _ChannelDriver {
5660
6217
  this.unconfirmedDispatchFailures.delete(message.id);
5661
6218
  this.sessions.delete(conv.id);
5662
6219
  this.supersede(conv.id, sessionId);
5663
- const errorMessage = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
6220
+ const errorMessage3 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
5664
6221
  this.log({
5665
6222
  level: "error",
5666
- message: errorMessage,
6223
+ message: errorMessage3,
5667
6224
  conversation_id: conv.id,
5668
6225
  message_id: message.id
5669
6226
  });
5670
- await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
6227
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5671
6228
  this.log({
5672
6229
  level: "warn",
5673
6230
  message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -5893,6 +6450,23 @@ var ChannelDriver = class _ChannelDriver {
5893
6450
  if (state === "running" || state === "queued") {
5894
6451
  const ongoing = await isSessionOngoing(this.port, sessionId);
5895
6452
  if (ongoing === true) {
6453
+ if (state === "queued") {
6454
+ const siblingOcIds = this.siblingOpencodeMessageIds(
6455
+ this.watchers.get(sessionId),
6456
+ message.id
6457
+ );
6458
+ if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
6459
+ this.log({
6460
+ level: "warn",
6461
+ message: `Re-drive: OpenCode already served a later, different Evident message's turn in session ${sessionId.slice(0, 8)} while message ${message.id.slice(0, 8)} produced no reply \u2014 re-dispatching instead of reattaching to someone else's turn`,
6462
+ conversation_id: conv.id,
6463
+ message_id: message.id
6464
+ });
6465
+ this.clearRedriveUnresolved(message.id);
6466
+ void this.postSignal(conv.id, message.id, "redrive_redispatched");
6467
+ return "dispatch";
6468
+ }
6469
+ }
5896
6470
  return this.reattachRedrive(conv, sessionId, message, ocId);
5897
6471
  }
5898
6472
  if (ongoing === false) {
@@ -5982,16 +6556,37 @@ var ChannelDriver = class _ChannelDriver {
5982
6556
  if (state === "done") {
5983
6557
  const title = await this.resolveSessionTitle(sessionId, conv.id);
5984
6558
  const usage = messageUsage(messages, ocId ?? "");
6559
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
6560
+ const subagentInvocations = await this.resolveSubagentInvocations(
6561
+ messages,
6562
+ ocId ?? "",
6563
+ message.id
6564
+ );
5985
6565
  this.log({
5986
6566
  level: "info",
5987
6567
  message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending \u2014 marking done instead of re-dispatching`,
5988
6568
  conversation_id: conv.id,
5989
6569
  message_id: message.id
5990
6570
  });
5991
- await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);
6571
+ await this.markDone(
6572
+ conv.id,
6573
+ message.id,
6574
+ sessionId,
6575
+ ocId,
6576
+ title,
6577
+ usage,
6578
+ usageAgentName,
6579
+ subagentInvocations
6580
+ );
5992
6581
  } else {
5993
6582
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
5994
6583
  const usage = messageUsage(messages, ocId ?? "");
6584
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
6585
+ const subagentInvocations = await this.resolveSubagentInvocations(
6586
+ messages,
6587
+ ocId ?? "",
6588
+ message.id
6589
+ );
5995
6590
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
5996
6591
  this.log({
5997
6592
  level: "error",
@@ -5999,7 +6594,19 @@ var ChannelDriver = class _ChannelDriver {
5999
6594
  conversation_id: conv.id,
6000
6595
  message_id: message.id
6001
6596
  });
6002
- await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
6597
+ await this.markFailed(
6598
+ conv.id,
6599
+ message.id,
6600
+ sessionId,
6601
+ error2,
6602
+ usage,
6603
+ failure,
6604
+ usageAgentName,
6605
+ subagentInvocations
6606
+ );
6607
+ }
6608
+ if (ocId !== null) {
6609
+ await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
6003
6610
  }
6004
6611
  } catch (err) {
6005
6612
  if (err instanceof ChannelAuthError) throw err;
@@ -6540,9 +7147,27 @@ var ChannelDriver = class _ChannelDriver {
6540
7147
  ambiguousPinnedSinceMs: 0,
6541
7148
  ambiguousResolved: false
6542
7149
  });
7150
+ const buffered = this.bufferedSessionErrors.get(sessionId);
7151
+ if (!buffered) return;
7152
+ this.bufferedSessionErrors.delete(sessionId);
7153
+ if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
7154
+ this.handleSessionError(buffered.event);
7155
+ }
6543
7156
  }
6544
- /**
6545
- * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
7157
+ bufferSessionError(event) {
7158
+ this.bufferedSessionErrors.delete(event.sessionId);
7159
+ this.bufferedSessionErrors.set(event.sessionId, {
7160
+ event,
7161
+ receivedAt: this.now()
7162
+ });
7163
+ while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
7164
+ const oldest = this.bufferedSessionErrors.keys().next().value;
7165
+ if (typeof oldest !== "string") break;
7166
+ this.bufferedSessionErrors.delete(oldest);
7167
+ }
7168
+ }
7169
+ /**
7170
+ * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
6546
7171
  * EQUAL (#1618) so a watcher whose loop has not started ticking yet is never
6547
7172
  * misread as stalled by the very first reconciliation that sees it.
6548
7173
  */
@@ -6744,6 +7369,7 @@ var ChannelDriver = class _ChannelDriver {
6744
7369
  ensureWatcherRunning(sessionId) {
6745
7370
  const watcher = this.watchers.get(sessionId);
6746
7371
  if (!watcher) return;
7372
+ this.ensureSessionErrorStream();
6747
7373
  if (watcher.loop) return;
6748
7374
  if (watcher.inFlight.size === 0) {
6749
7375
  this.watchers.delete(sessionId);
@@ -6759,6 +7385,154 @@ var ChannelDriver = class _ChannelDriver {
6759
7385
  });
6760
7386
  watcher.loop = loop;
6761
7387
  }
7388
+ ensureSessionErrorStream() {
7389
+ if (this.sessionErrorStream || this.stopped) return;
7390
+ const abort = new AbortController();
7391
+ const loop = this.runSessionErrorStream(abort.signal);
7392
+ this.sessionErrorStream = { abort, loop };
7393
+ }
7394
+ async runSessionErrorStream(signal) {
7395
+ let attempt = 0;
7396
+ let warned = false;
7397
+ while (!this.stopped && !signal.aborted) {
7398
+ const openedAt = this.now();
7399
+ try {
7400
+ const outcome = await readSessionErrorStream(this.port, {
7401
+ signal,
7402
+ onSessionError: (event) => this.handleSessionError(event)
7403
+ });
7404
+ if (outcome.reason === "aborted" || signal.aborted) return;
7405
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
7406
+ if (outcome.reason === "unavailable" || outcome.reason === "ended") {
7407
+ if (!healthy) {
7408
+ const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
7409
+ this.log({
7410
+ level: warned ? "debug" : "warn",
7411
+ message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
7412
+ });
7413
+ warned = true;
7414
+ }
7415
+ }
7416
+ if (healthy) {
7417
+ if (warned) {
7418
+ this.log({
7419
+ level: "info",
7420
+ message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
7421
+ });
7422
+ warned = false;
7423
+ }
7424
+ attempt = 0;
7425
+ } else {
7426
+ attempt += 1;
7427
+ }
7428
+ if (this.stopped || signal.aborted) return;
7429
+ await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
7430
+ } catch (err) {
7431
+ if (this.stopped || signal.aborted) return;
7432
+ this.log({
7433
+ level: "error",
7434
+ message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
7435
+ });
7436
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
7437
+ const delayAttempt = healthy ? 0 : attempt;
7438
+ attempt = healthy ? 0 : attempt + 1;
7439
+ try {
7440
+ await this.sleep(backoffDelay(delayAttempt, this.retry));
7441
+ } catch (sleepErr) {
7442
+ this.log({
7443
+ level: "error",
7444
+ message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
7445
+ });
7446
+ }
7447
+ }
7448
+ }
7449
+ }
7450
+ handleSessionError(event) {
7451
+ try {
7452
+ const watcher = this.watchers.get(event.sessionId);
7453
+ if (!watcher) {
7454
+ this.bufferSessionError(event);
7455
+ this.log({
7456
+ level: "debug",
7457
+ message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
7458
+ });
7459
+ return;
7460
+ }
7461
+ if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
7462
+ this.log({
7463
+ level: "debug",
7464
+ message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
7465
+ conversation_id: watcher.conv.id
7466
+ });
7467
+ return;
7468
+ }
7469
+ const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
7470
+ if (!inFlight) {
7471
+ this.bufferSessionError(event);
7472
+ this.log({
7473
+ level: "debug",
7474
+ message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
7475
+ conversation_id: watcher.conv.id
7476
+ });
7477
+ return;
7478
+ }
7479
+ if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
7480
+ this.sessionErrorHandled.add(inFlight.evidentMessageId);
7481
+ void this.failFromSessionError(watcher, event, inFlight);
7482
+ } catch (err) {
7483
+ this.log({
7484
+ level: "error",
7485
+ message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
7486
+ });
7487
+ }
7488
+ }
7489
+ async failFromSessionError(watcher, event, inFlight) {
7490
+ try {
7491
+ const messages = await getSessionMessages(this.port, event.sessionId);
7492
+ const state = messageRunState(messages, inFlight.opencodeMessageId);
7493
+ if (state !== "queued") {
7494
+ this.log({
7495
+ level: "debug",
7496
+ message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
7497
+ conversation_id: watcher.conv.id,
7498
+ message_id: inFlight.evidentMessageId
7499
+ });
7500
+ return;
7501
+ }
7502
+ this.log({
7503
+ level: "error",
7504
+ message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
7505
+ conversation_id: watcher.conv.id,
7506
+ message_id: inFlight.evidentMessageId
7507
+ });
7508
+ await this.markFailed(
7509
+ watcher.conv.id,
7510
+ inFlight.evidentMessageId,
7511
+ event.sessionId,
7512
+ `OpenCode could not run this turn: ${event.reason}`
7513
+ );
7514
+ inFlight.done = true;
7515
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
7516
+ } catch (err) {
7517
+ if (err instanceof ChannelAuthError) {
7518
+ this.log({
7519
+ level: "warn",
7520
+ message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed because authentication failed: ${err.message}; leaving it to transcript polling / the existing give-up path`,
7521
+ conversation_id: watcher.conv.id,
7522
+ message_id: inFlight.evidentMessageId
7523
+ });
7524
+ } else {
7525
+ this.log({
7526
+ level: "warn",
7527
+ message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed: ${err instanceof Error ? err.message : String(err)}; leaving it to transcript polling / the existing give-up path`,
7528
+ conversation_id: watcher.conv.id,
7529
+ message_id: inFlight.evidentMessageId
7530
+ });
7531
+ }
7532
+ } finally {
7533
+ this.sessionErrorHandled.delete(inFlight.evidentMessageId);
7534
+ }
7535
+ }
6762
7536
  /**
6763
7537
  * The per-session polling loop (WI-3). Once per tick it:
6764
7538
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -6871,6 +7645,21 @@ var ChannelDriver = class _ChannelDriver {
6871
7645
  const conv = watcher.conv;
6872
7646
  const state = messageRunState(messages, inFlight.opencodeMessageId);
6873
7647
  const id = inFlight.evidentMessageId;
7648
+ if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
7649
+ void this.resolveSubagentInvocations(
7650
+ messages,
7651
+ inFlight.opencodeMessageId,
7652
+ id,
7653
+ "prefetch"
7654
+ ).catch((err) => {
7655
+ this.log({
7656
+ level: "warn",
7657
+ message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
7658
+ conversation_id: conv.id,
7659
+ message_id: id
7660
+ });
7661
+ });
7662
+ }
6874
7663
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
6875
7664
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
6876
7665
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -6924,6 +7713,12 @@ var ChannelDriver = class _ChannelDriver {
6924
7713
  message_id: inFlight.evidentMessageId
6925
7714
  });
6926
7715
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
7716
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
7717
+ const subagentInvocations = await this.resolveSubagentInvocations(
7718
+ messages,
7719
+ inFlight.opencodeMessageId,
7720
+ inFlight.evidentMessageId
7721
+ );
6927
7722
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
6928
7723
  try {
6929
7724
  await this.markFailed(
@@ -6932,7 +7727,9 @@ var ChannelDriver = class _ChannelDriver {
6932
7727
  sessionId,
6933
7728
  error2,
6934
7729
  usage,
6935
- failure
7730
+ failure,
7731
+ usageAgentName,
7732
+ subagentInvocations
6936
7733
  );
6937
7734
  } catch (err) {
6938
7735
  if (err instanceof ChannelAuthError) throw err;
@@ -6965,13 +7762,21 @@ var ChannelDriver = class _ChannelDriver {
6965
7762
  return;
6966
7763
  }
6967
7764
  inFlight.done = true;
7765
+ await this.reportSubagentAuthFailures(
7766
+ watcher.conv.id,
7767
+ inFlight.opencodeMessageId,
7768
+ inFlight.evidentMessageId,
7769
+ messages
7770
+ );
6968
7771
  }
6969
7772
  this.removeInFlight(watcher, inFlight.evidentMessageId);
6970
7773
  return;
6971
7774
  }
7775
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
7776
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
6972
7777
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
6973
7778
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
6974
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
7779
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
6975
7780
  inFlight.stuckReported = true;
6976
7781
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
6977
7782
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7132,7 +7937,7 @@ var ChannelDriver = class _ChannelDriver {
7132
7937
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7133
7938
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7134
7939
  );
7135
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
7940
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7136
7941
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7137
7942
  this.log({
7138
7943
  level: "debug",
@@ -7167,6 +7972,12 @@ var ChannelDriver = class _ChannelDriver {
7167
7972
  });
7168
7973
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7169
7974
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
7975
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
7976
+ const subagentInvocations = await this.resolveSubagentInvocations(
7977
+ messages,
7978
+ inFlight.opencodeMessageId,
7979
+ inFlight.evidentMessageId
7980
+ );
7170
7981
  try {
7171
7982
  await this.markDone(
7172
7983
  conv.id,
@@ -7174,7 +7985,9 @@ var ChannelDriver = class _ChannelDriver {
7174
7985
  sessionId,
7175
7986
  inFlight.opencodeMessageId,
7176
7987
  title,
7177
- usage
7988
+ usage,
7989
+ usageAgentName,
7990
+ subagentInvocations
7178
7991
  );
7179
7992
  } catch (err) {
7180
7993
  if (err instanceof ChannelAuthError) throw err;
@@ -7207,6 +8020,12 @@ var ChannelDriver = class _ChannelDriver {
7207
8020
  return;
7208
8021
  }
7209
8022
  inFlight.done = true;
8023
+ await this.reportSubagentAuthFailures(
8024
+ watcher.conv.id,
8025
+ inFlight.opencodeMessageId,
8026
+ inFlight.evidentMessageId,
8027
+ messages
8028
+ );
7210
8029
  }
7211
8030
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7212
8031
  }
@@ -7347,6 +8166,12 @@ var ChannelDriver = class _ChannelDriver {
7347
8166
  if (state === "failed" && !restartAborted) {
7348
8167
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7349
8168
  const usage = messageUsage(messages, ocId ?? "");
8169
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
8170
+ const subagentInvocations = await this.resolveSubagentInvocations(
8171
+ messages,
8172
+ ocId ?? "",
8173
+ row.id
8174
+ );
7350
8175
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7351
8176
  this.log({
7352
8177
  level: "error",
@@ -7355,7 +8180,16 @@ var ChannelDriver = class _ChannelDriver {
7355
8180
  message_id: row.id
7356
8181
  });
7357
8182
  try {
7358
- await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage, failure);
8183
+ await this.markFailed(
8184
+ row.conversation_id,
8185
+ row.id,
8186
+ sessionId,
8187
+ error2,
8188
+ usage,
8189
+ failure,
8190
+ usageAgentName,
8191
+ subagentInvocations
8192
+ );
7359
8193
  } catch (err) {
7360
8194
  if (err instanceof ChannelAuthError) throw err;
7361
8195
  if (err instanceof ChannelTerminalError) {
@@ -7377,6 +8211,7 @@ var ChannelDriver = class _ChannelDriver {
7377
8211
  });
7378
8212
  return;
7379
8213
  }
8214
+ await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
7380
8215
  this.dontRedispatch.delete(row.id);
7381
8216
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
7382
8217
  return;
@@ -7516,7 +8351,22 @@ var ChannelDriver = class _ChannelDriver {
7516
8351
  try {
7517
8352
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7518
8353
  const usage = messageUsage(messages, ocId ?? "");
7519
- await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
8354
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
8355
+ const subagentInvocations = await this.resolveSubagentInvocations(
8356
+ messages,
8357
+ ocId ?? "",
8358
+ row.id
8359
+ );
8360
+ await this.markDone(
8361
+ row.conversation_id,
8362
+ row.id,
8363
+ sessionId,
8364
+ ocId,
8365
+ title,
8366
+ usage,
8367
+ usageAgentName,
8368
+ subagentInvocations
8369
+ );
7520
8370
  } catch (err) {
7521
8371
  if (err instanceof ChannelAuthError) throw err;
7522
8372
  if (err instanceof ChannelTerminalError) {
@@ -7538,6 +8388,9 @@ var ChannelDriver = class _ChannelDriver {
7538
8388
  });
7539
8389
  return;
7540
8390
  }
8391
+ if (ocId !== null) {
8392
+ await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
8393
+ }
7541
8394
  this.dontRedispatch.delete(row.id);
7542
8395
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
7543
8396
  }
@@ -7631,14 +8484,14 @@ var ChannelDriver = class _ChannelDriver {
7631
8484
  this.unconfirmedDispatchFailures.delete(row.id);
7632
8485
  this.sessions.delete(readoptConv.id);
7633
8486
  this.supersede(readoptConv.id, sessionId);
7634
- const errorMessage = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
8487
+ const errorMessage3 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
7635
8488
  this.log({
7636
8489
  level: "error",
7637
- message: errorMessage,
8490
+ message: errorMessage3,
7638
8491
  conversation_id: row.conversation_id,
7639
8492
  message_id: row.id
7640
8493
  });
7641
- await this.markFailed(row.conversation_id, row.id, null, errorMessage).catch((markErr) => {
8494
+ await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
7642
8495
  this.log({
7643
8496
  level: "warn",
7644
8497
  message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -7917,6 +8770,166 @@ var ChannelDriver = class _ChannelDriver {
7917
8770
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
7918
8771
  return parent;
7919
8772
  }
8773
+ usageAgentName(messages, userMessageId) {
8774
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
8775
+ const mode = reply?.info?.mode;
8776
+ if (typeof mode === "string" && mode.length > 0) return mode;
8777
+ const agent = reply?.info?.agent;
8778
+ return typeof agent === "string" && agent.length > 0 ? agent : null;
8779
+ }
8780
+ async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
8781
+ if (!messages) return void 0;
8782
+ const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
8783
+ const cached = cache.get(messageId);
8784
+ if (cached) return cached;
8785
+ const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
8786
+ (err) => {
8787
+ this.log({
8788
+ level: "warn",
8789
+ message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
8790
+ message_id: messageId
8791
+ });
8792
+ return void 0;
8793
+ }
8794
+ );
8795
+ cache.set(messageId, collection);
8796
+ const result = await collection;
8797
+ if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
8798
+ return result;
8799
+ }
8800
+ clearSubagentInvocationCaches(messageId) {
8801
+ this.subagentInvocationCollections.delete(messageId);
8802
+ this.subagentInvocationPrefetches.delete(messageId);
8803
+ }
8804
+ async buildSubagentInvocations(messages, userMessageId, messageId) {
8805
+ const rootCalls = collectTaskCalls(messages, userMessageId);
8806
+ if (rootCalls.length === 0) return void 0;
8807
+ const childMessages = /* @__PURE__ */ new Map();
8808
+ const seenCallIds = new Set(rootCalls.map((call) => call.callID));
8809
+ const work = rootCalls.map((call) => ({
8810
+ call,
8811
+ depth: 1
8812
+ }));
8813
+ const payload = [];
8814
+ const fetchChildMessages = (sessionId) => {
8815
+ const cached = childMessages.get(sessionId);
8816
+ if (cached) return cached;
8817
+ const pending = (async () => {
8818
+ try {
8819
+ const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8820
+ if (!res.ok) {
8821
+ this.log({
8822
+ level: "warn",
8823
+ message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 omitting invocation telemetry`,
8824
+ message_id: messageId
8825
+ });
8826
+ return null;
8827
+ }
8828
+ const body = await res.json();
8829
+ if (!Array.isArray(body)) throw new Error("response body was not a message array");
8830
+ return body;
8831
+ } catch (err) {
8832
+ this.log({
8833
+ level: "warn",
8834
+ message: `Best-effort subagent session fetch failed for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
8835
+ message_id: messageId
8836
+ });
8837
+ return null;
8838
+ }
8839
+ })();
8840
+ childMessages.set(sessionId, pending);
8841
+ return pending;
8842
+ };
8843
+ const fetchChildWithoutBlocking = async (sessionId) => {
8844
+ const pending = fetchChildMessages(sessionId);
8845
+ let timer;
8846
+ const timeout = new Promise((resolve4) => {
8847
+ timer = setTimeout(() => {
8848
+ this.log({
8849
+ level: "warn",
8850
+ message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was slow \u2014 omitting invocation telemetry without delaying completion`,
8851
+ message_id: messageId
8852
+ });
8853
+ resolve4(null);
8854
+ }, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
8855
+ });
8856
+ try {
8857
+ return await Promise.race([pending, timeout]);
8858
+ } finally {
8859
+ if (timer !== void 0) clearTimeout(timer);
8860
+ }
8861
+ };
8862
+ while (work.length > 0) {
8863
+ const groups = /* @__PURE__ */ new Map();
8864
+ for (const item of work.splice(0)) {
8865
+ const group = groups.get(item.call.childSessionId) ?? [];
8866
+ group.push(item);
8867
+ groups.set(item.call.childSessionId, group);
8868
+ }
8869
+ const groupResults = await Promise.all(
8870
+ [...groups].map(async ([sessionId, items]) => ({
8871
+ sessionId,
8872
+ items,
8873
+ messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
8874
+ }))
8875
+ );
8876
+ for (const { sessionId, items, messages: child } of groupResults) {
8877
+ if (sessionId !== null && child === null) continue;
8878
+ const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
8879
+ child,
8880
+ items.map(({ call }) => ({
8881
+ callID: call.callID,
8882
+ timeStart: call.timeStart,
8883
+ timeEnd: call.timeEnd
8884
+ }))
8885
+ );
8886
+ if (sessionId !== null && attribution.unattributed.length > 0) {
8887
+ this.log({
8888
+ level: "warn",
8889
+ message: `Omitted ${attribution.unattributed.length} unattributable assistant message(s) from subagent usage for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 assigned to no invocation window`,
8890
+ message_id: messageId
8891
+ });
8892
+ }
8893
+ const usageByCall = new Map(
8894
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
8895
+ );
8896
+ const messagesByCall = new Map(
8897
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
8898
+ );
8899
+ for (const { call, depth } of items) {
8900
+ const usage = usageByCall.get(call.callID) ?? null;
8901
+ payload.push({
8902
+ tool_call_id: call.callID,
8903
+ agent_name: call.subagentName,
8904
+ opencode_session_id: call.childSessionId,
8905
+ parent_opencode_session_id: call.parentSessionId,
8906
+ depth,
8907
+ status: call.status,
8908
+ started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
8909
+ ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
8910
+ usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
8911
+ usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
8912
+ usage_tokens_input: usage?.usage_tokens_input ?? null,
8913
+ usage_tokens_output: usage?.usage_tokens_output ?? null,
8914
+ usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
8915
+ usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
8916
+ usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
8917
+ usage_cost_usd: usage?.usage_cost_usd ?? null
8918
+ });
8919
+ for (const assigned of messagesByCall.get(call.callID) ?? []) {
8920
+ const parentId = assigned.info?.parentID ?? assigned.parentID;
8921
+ if (!parentId) continue;
8922
+ for (const nested of collectTaskCalls([assigned], parentId)) {
8923
+ if (seenCallIds.has(nested.callID)) continue;
8924
+ seenCallIds.add(nested.callID);
8925
+ work.push({ call: nested, depth: depth + 1 });
8926
+ }
8927
+ }
8928
+ }
8929
+ }
8930
+ }
8931
+ return payload.length > 0 ? payload : void 0;
8932
+ }
7920
8933
  /**
7921
8934
  * OpenCode's synchronous default session title (e.g.
7922
8935
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8253,6 +9266,7 @@ var ChannelDriver = class _ChannelDriver {
8253
9266
  throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
8254
9267
  }
8255
9268
  const data = await res.json();
9269
+ this.recycleRequestedFlag = data.recycle_requested === true;
8256
9270
  let conversations = data.conversations;
8257
9271
  if (this.conversationFilter) {
8258
9272
  conversations = conversations.filter((c) => c.id === this.conversationFilter);
@@ -8399,7 +9413,7 @@ var ChannelDriver = class _ChannelDriver {
8399
9413
  * watcher retries next tick within the
8400
9414
  * deadline, Finding 4).
8401
9415
  */
8402
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
9416
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8403
9417
  const res = await this.fetchImpl(
8404
9418
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8405
9419
  {
@@ -8415,15 +9429,21 @@ var ChannelDriver = class _ChannelDriver {
8415
9429
  opencode_session_id: sessionId,
8416
9430
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8417
9431
  ...title ? { title } : {},
8418
- ...usage ? usage : {}
9432
+ ...usage ? usage : {},
9433
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
9434
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8419
9435
  })
8420
9436
  }
8421
9437
  );
8422
9438
  this.assertAuth(res, "marking message as done");
8423
- if (res.ok) return;
9439
+ if (res.ok) {
9440
+ this.clearSubagentInvocationCaches(messageId);
9441
+ return;
9442
+ }
8424
9443
  if (isRetryableStatus(res.status)) {
8425
9444
  throw new Error(`marking message as done: HTTP ${res.status}`);
8426
9445
  }
9446
+ this.clearSubagentInvocationCaches(messageId);
8427
9447
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8428
9448
  }
8429
9449
  /**
@@ -8438,7 +9458,7 @@ var ChannelDriver = class _ChannelDriver {
8438
9458
  * exists but is wedged, so the next attempt must get a fresh one
8439
9459
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8440
9460
  */
8441
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
9461
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8442
9462
  const body = { status: "failed" };
8443
9463
  if (sessionId === null) {
8444
9464
  body.opencode_session_id = null;
@@ -8447,23 +9467,33 @@ var ChannelDriver = class _ChannelDriver {
8447
9467
  }
8448
9468
  if (error2 !== void 0) body.error = error2;
8449
9469
  if (usage) Object.assign(body, usage);
9470
+ if (usageAgentName) body.usage_agent_name = usageAgentName;
9471
+ if (subagentInvocations && subagentInvocations.length > 0) {
9472
+ body.subagent_invocations = subagentInvocations;
9473
+ }
8450
9474
  if (failure) {
8451
9475
  body.failure_kind = failure.kind;
8452
9476
  body.failure_provider_id = failure.providerId;
8453
9477
  body.failure_model_id = failure.modelId;
8454
9478
  body.failure_reason = failure.reason;
8455
9479
  }
8456
- await this.callWithRetry(
8457
- "marking message as failed",
8458
- () => this.fetchImpl(
8459
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8460
- {
8461
- method: "PATCH",
8462
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8463
- body: JSON.stringify(body)
8464
- }
8465
- )
8466
- );
9480
+ try {
9481
+ await this.callWithRetry(
9482
+ "marking message as failed",
9483
+ () => this.fetchImpl(
9484
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
9485
+ {
9486
+ method: "PATCH",
9487
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
9488
+ body: JSON.stringify(body)
9489
+ }
9490
+ )
9491
+ );
9492
+ } catch (err) {
9493
+ if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
9494
+ throw err;
9495
+ }
9496
+ this.clearSubagentInvocationCaches(messageId);
8467
9497
  }
8468
9498
  /**
8469
9499
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -8488,6 +9518,111 @@ var ChannelDriver = class _ChannelDriver {
8488
9518
  reply?.info?.modelID ?? null
8489
9519
  );
8490
9520
  }
9521
+ async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
9522
+ const providerId = failure.providerId ?? "(unknown)";
9523
+ try {
9524
+ const res = await this.fetchImpl(
9525
+ `${this.apiUrl}/runners/${this.agentId}/model-auth-failures`,
9526
+ {
9527
+ method: "POST",
9528
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
9529
+ body: JSON.stringify({
9530
+ provider_id: failure.providerId,
9531
+ model_id: failure.modelId,
9532
+ reason: failure.reason
9533
+ })
9534
+ }
9535
+ );
9536
+ if (!res.ok) {
9537
+ this.log({
9538
+ level: "warn",
9539
+ message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
9540
+ conversation_id: conversationId,
9541
+ message_id: messageId
9542
+ });
9543
+ }
9544
+ } catch (err) {
9545
+ this.log({
9546
+ level: "warn",
9547
+ message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
9548
+ conversation_id: conversationId,
9549
+ message_id: messageId
9550
+ });
9551
+ }
9552
+ }
9553
+ async clearSubagentModelAuthFailure(providerId, conversationId, messageId) {
9554
+ try {
9555
+ const res = await this.fetchImpl(
9556
+ `${this.apiUrl}/runners/${this.agentId}/model-auth-failures/${encodeURIComponent(providerId)}`,
9557
+ {
9558
+ method: "DELETE",
9559
+ headers: { Authorization: this.getAuthHeader() }
9560
+ }
9561
+ );
9562
+ if (!res.ok) {
9563
+ this.log({
9564
+ level: "warn",
9565
+ message: `Sub-agent model-auth clear for provider ${providerId} returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
9566
+ conversation_id: conversationId,
9567
+ message_id: messageId
9568
+ });
9569
+ }
9570
+ } catch (err) {
9571
+ this.log({
9572
+ level: "warn",
9573
+ message: `Sub-agent model-auth clear for provider ${providerId} failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
9574
+ conversation_id: conversationId,
9575
+ message_id: messageId
9576
+ });
9577
+ }
9578
+ }
9579
+ async reportSubagentAuthFailures(conversationId, opencodeMessageId, evidentMessageId, messages) {
9580
+ const refs = collectSubagentSessions(messages, opencodeMessageId);
9581
+ if (refs.length === 0) return;
9582
+ const failedProviders = /* @__PURE__ */ new Map();
9583
+ const succeededProviders = /* @__PURE__ */ new Set();
9584
+ for (const ref of refs) {
9585
+ try {
9586
+ const childMessages = await getSessionMessages(this.port, ref.sessionId);
9587
+ if (childMessages === null) {
9588
+ this.log({
9589
+ level: "debug",
9590
+ message: `Could not read sub-agent session ${ref.sessionId} while checking credential failures \u2014 skipping it`,
9591
+ conversation_id: conversationId,
9592
+ message_id: evidentMessageId
9593
+ });
9594
+ continue;
9595
+ }
9596
+ const outcome = findSubagentAuthOutcome(childMessages, ref.startedAtMs);
9597
+ if (!outcome) continue;
9598
+ if (outcome.outcome === "failed") {
9599
+ failedProviders.set(outcome.providerId, outcome.failure);
9600
+ } else {
9601
+ succeededProviders.add(outcome.providerId);
9602
+ }
9603
+ } catch (err) {
9604
+ this.log({
9605
+ level: "warn",
9606
+ message: `Failed to inspect sub-agent session ${ref.sessionId} for credential failures (conversation ${conversationId.slice(0, 8)}, message ${evidentMessageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
9607
+ conversation_id: conversationId,
9608
+ message_id: evidentMessageId
9609
+ });
9610
+ }
9611
+ }
9612
+ for (const [providerId, failure] of failedProviders) {
9613
+ this.log({
9614
+ level: "warn",
9615
+ message: `Sub-agent turn failed on provider ${providerId} (${failure.reason}) \u2014 recording credential evidence`,
9616
+ conversation_id: conversationId,
9617
+ message_id: evidentMessageId
9618
+ });
9619
+ await this.recordSubagentModelAuthFailure(failure, conversationId, evidentMessageId);
9620
+ }
9621
+ for (const providerId of succeededProviders) {
9622
+ if (failedProviders.has(providerId)) continue;
9623
+ await this.clearSubagentModelAuthFailure(providerId, conversationId, evidentMessageId);
9624
+ }
9625
+ }
8491
9626
  /**
8492
9627
  * Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
8493
9628
  * (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
@@ -8641,6 +9776,13 @@ import chalk5 from "chalk";
8641
9776
  import ora2 from "ora";
8642
9777
  import { select as select2 } from "@inquirer/prompts";
8643
9778
  var INTERACTIVE_START_TIMEOUT_MS = 3e4;
9779
+ function checkNonInteractivePortConflict(port, isPortInUseFn) {
9780
+ if (isPortInUseFn(port)) {
9781
+ throw new Error(
9782
+ `Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
9783
+ );
9784
+ }
9785
+ }
8644
9786
  async function ensureOpenCodeRunning(ctx) {
8645
9787
  const healthCheck = await checkOpenCodeHealth(ctx.port);
8646
9788
  if (healthCheck.healthy) {
@@ -8688,6 +9830,7 @@ async function ensureOpenCodeRunning(ctx) {
8688
9830
  }
8689
9831
  }
8690
9832
  if (!ctx.interactive) {
9833
+ checkNonInteractivePortConflict(ctx.port, isPortInUse);
8691
9834
  ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
8692
9835
  const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
8693
9836
  const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
@@ -8769,9 +9912,119 @@ Port ${port} is already in use.`));
8769
9912
  return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
8770
9913
  }
8771
9914
 
9915
+ // src/commands/ensure-opencode-v2.ts
9916
+ import chalk6 from "chalk";
9917
+ import { select as select3 } from "@inquirer/prompts";
9918
+ async function probeOpenCode2WithoutPassword(port) {
9919
+ try {
9920
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
9921
+ signal: AbortSignal.timeout(2e3)
9922
+ });
9923
+ if (response.status === 401) {
9924
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
9925
+ }
9926
+ if (!response.ok) {
9927
+ return { healthy: false, error: `HTTP ${response.status}` };
9928
+ }
9929
+ return { healthy: true };
9930
+ } catch (error2) {
9931
+ return {
9932
+ healthy: false,
9933
+ error: error2 instanceof Error ? error2.message : "Unknown error"
9934
+ };
9935
+ }
9936
+ }
9937
+ function unknownPasswordError(port) {
9938
+ return new Error(
9939
+ `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
9940
+ );
9941
+ }
9942
+ function v2SessionSupportIncompleteError() {
9943
+ return new Error(
9944
+ "OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
9945
+ );
9946
+ }
9947
+ async function ensureOpenCode2Running(ctx) {
9948
+ const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
9949
+ if (initialHealth.authFailed) {
9950
+ throw unknownPasswordError(ctx.port);
9951
+ }
9952
+ if (initialHealth.healthy) {
9953
+ return {
9954
+ port: ctx.port,
9955
+ process: null,
9956
+ version: null,
9957
+ notReadyReason: null,
9958
+ password: null
9959
+ };
9960
+ }
9961
+ if (!isOpenCode2Installed()) {
9962
+ throw new Error(
9963
+ "OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
9964
+ );
9965
+ }
9966
+ let port = ctx.port;
9967
+ if (!ctx.interactive) {
9968
+ checkNonInteractivePortConflict(port, isPortInUse);
9969
+ } else if (isPortInUse(port)) {
9970
+ console.log(chalk6.yellow(`
9971
+ Port ${port} is already in use.`));
9972
+ const alternativePort = findAvailablePort(port + 1);
9973
+ if (alternativePort) {
9974
+ const useAlternative = await select3({
9975
+ message: `Use port ${alternativePort} instead?`,
9976
+ choices: [
9977
+ { name: `Yes, use port ${alternativePort}`, value: "yes" },
9978
+ { name: "No, I will free the port manually", value: "no" }
9979
+ ]
9980
+ });
9981
+ if (useAlternative === "yes") {
9982
+ port = alternativePort;
9983
+ } else {
9984
+ throw new Error(`Port ${ctx.port} is in use`);
9985
+ }
9986
+ }
9987
+ }
9988
+ if (!ctx.interactive) {
9989
+ throw v2SessionSupportIncompleteError();
9990
+ }
9991
+ console.log(chalk6.yellow(`
9992
+ ${v2SessionSupportIncompleteError().message}`));
9993
+ const action = await select3({
9994
+ message: "OpenCode V2 is not running. What would you like to do?",
9995
+ choices: [
9996
+ {
9997
+ name: "Show me the command",
9998
+ value: "manual",
9999
+ description: "Display the command to run manually"
10000
+ },
10001
+ {
10002
+ name: "Continue without OpenCode V2",
10003
+ value: "continue",
10004
+ description: "Requests will fail until OpenCode V2 starts"
10005
+ }
10006
+ ]
10007
+ });
10008
+ if (action === "manual") {
10009
+ blank();
10010
+ console.log(chalk6.bold("Run this command in another terminal:"));
10011
+ blank();
10012
+ console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
10013
+ blank();
10014
+ throw new Error("Please start OpenCode V2 manually");
10015
+ }
10016
+ return {
10017
+ port,
10018
+ process: null,
10019
+ version: null,
10020
+ notReadyReason: "you chose to continue without OpenCode V2",
10021
+ password: null
10022
+ };
10023
+ }
10024
+
8772
10025
  // src/lib/runner-credentials.ts
8773
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
8774
- import { spawn as spawn5 } from "child_process";
10026
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
10027
+ import { spawn as spawn5 } from "node:child_process";
8775
10028
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
8776
10029
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
8777
10030
  var GITHUB_PROBE_TIMEOUT_MS = 1e4;
@@ -9043,11 +10296,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
9043
10296
  }
9044
10297
 
9045
10298
  // src/lib/opencode/config-overlay.ts
9046
- import { execFileSync as execFileSync2 } from "child_process";
9047
- import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
9048
- import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
10299
+ import { execFileSync as execFileSync2 } from "node:child_process";
10300
+ import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
10301
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
9049
10302
  function isFile(filePath) {
9050
- return existsSync2(filePath) && statSync5(filePath).isFile();
10303
+ return existsSync2(filePath) && statSync6(filePath).isFile();
9051
10304
  }
9052
10305
  function applyRunnerOpenCodeConfig({
9053
10306
  overlayPath,
@@ -9059,7 +10312,7 @@ function applyRunnerOpenCodeConfig({
9059
10312
  return;
9060
10313
  }
9061
10314
  const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
9062
- const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
10315
+ const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
9063
10316
  if (!isFile(source)) {
9064
10317
  log3(
9065
10318
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -9067,7 +10320,7 @@ function applyRunnerOpenCodeConfig({
9067
10320
  );
9068
10321
  return;
9069
10322
  }
9070
- copyFileSync(source, join8(cwd, target));
10323
+ copyFileSync(source, join9(cwd, target));
9071
10324
  try {
9072
10325
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
9073
10326
  stdio: "ignore"
@@ -9076,7 +10329,242 @@ function applyRunnerOpenCodeConfig({
9076
10329
  const detail = error2 instanceof Error ? error2.message : String(error2);
9077
10330
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
9078
10331
  }
9079
- log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
10332
+ log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
10333
+ }
10334
+
10335
+ // src/lib/credential-sync.ts
10336
+ import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
10337
+ var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
10338
+ var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
10339
+ var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
10340
+ var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
10341
+ var STORES = ["claude", "opencode"];
10342
+ var MAX_FLUSH_PASSES = 2;
10343
+ function outcomesWith(outcome) {
10344
+ return { claude: outcome, opencode: outcome };
10345
+ }
10346
+ function errorMessage2(error2) {
10347
+ return error2 instanceof Error ? error2.message : String(error2);
10348
+ }
10349
+ function waitForSettlement(promise, timeoutMs) {
10350
+ return new Promise((resolve4) => {
10351
+ let settled = false;
10352
+ const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
10353
+ const finish = (value) => {
10354
+ if (settled) return;
10355
+ settled = true;
10356
+ clearTimeout(timer);
10357
+ resolve4(value);
10358
+ };
10359
+ promise.then(
10360
+ () => finish(true),
10361
+ () => finish(true)
10362
+ );
10363
+ });
10364
+ }
10365
+ function writeMarker(markerPath, outcomes, log3) {
10366
+ const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
10367
+ `;
10368
+ const temporaryPath = `${markerPath}.tmp`;
10369
+ try {
10370
+ writeFileSync5(temporaryPath, body, { mode: 384 });
10371
+ renameSync(temporaryPath, markerPath);
10372
+ } catch (error2) {
10373
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
10374
+ }
10375
+ }
10376
+ function intervalSeconds(env, log3) {
10377
+ const raw = env.CREDS_SYNC_INTERVAL;
10378
+ if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
10379
+ return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
10380
+ }
10381
+ log3(
10382
+ `CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
10383
+ "warn"
10384
+ );
10385
+ return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
10386
+ }
10387
+ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
10388
+ const remainingMs = deadlineAt - Date.now();
10389
+ if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
10390
+ const controller = new AbortController();
10391
+ let result;
10392
+ let failed = false;
10393
+ const completion = Promise.resolve().then(
10394
+ () => synchroniserRunner(["sync-once", store], {
10395
+ timeoutMs: remainingMs,
10396
+ env,
10397
+ signal: controller.signal
10398
+ })
10399
+ ).then(
10400
+ (value) => {
10401
+ result = value;
10402
+ },
10403
+ (error2) => {
10404
+ failed = true;
10405
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
10406
+ }
10407
+ );
10408
+ const abortTimer = setTimeout(() => controller.abort(), remainingMs);
10409
+ const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
10410
+ clearTimeout(abortTimer);
10411
+ if (!settledBeforeDeadline) {
10412
+ controller.abort();
10413
+ const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
10414
+ if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
10415
+ return { outcome: "timeout", orphaned: false };
10416
+ }
10417
+ if (failed || !result) return { outcome: "failed", orphaned: false };
10418
+ if (result.timedOut || Date.now() >= deadlineAt) {
10419
+ return { outcome: "timeout", orphaned: false };
10420
+ }
10421
+ return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
10422
+ }
10423
+ function createCredentialSync({
10424
+ markerPath,
10425
+ env,
10426
+ log: log3,
10427
+ synchroniserRunner = runSynchroniser
10428
+ }) {
10429
+ const persistenceDisabled = !env.PERSISTENCE_BUCKET;
10430
+ let disabled = persistenceDisabled;
10431
+ let armed = false;
10432
+ let stopped = false;
10433
+ let timer;
10434
+ let inFlight;
10435
+ let activeTickAbort;
10436
+ let lastTickFailed;
10437
+ let flushPromise;
10438
+ const scheduleTick = (intervalMs, startTick2) => {
10439
+ if (stopped) return;
10440
+ timer = setTimeout(() => {
10441
+ timer = void 0;
10442
+ startTick2();
10443
+ }, intervalMs);
10444
+ };
10445
+ const startTick = (intervalMs) => {
10446
+ if (stopped) return;
10447
+ const controller = new AbortController();
10448
+ activeTickAbort = controller;
10449
+ const tick = (async () => {
10450
+ const outcomes = {
10451
+ claude: "failed",
10452
+ opencode: "failed"
10453
+ };
10454
+ for (const store of STORES) {
10455
+ if (controller.signal.aborted) break;
10456
+ try {
10457
+ const result = await synchroniserRunner(["sync-once", store], {
10458
+ timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
10459
+ env,
10460
+ signal: controller.signal
10461
+ });
10462
+ outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
10463
+ } catch (error2) {
10464
+ outcomes[store] = "failed";
10465
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
10466
+ }
10467
+ }
10468
+ const failed = STORES.some((store) => outcomes[store] === "failed");
10469
+ log3(
10470
+ `CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
10471
+ "debug"
10472
+ );
10473
+ if (failed && lastTickFailed !== true) {
10474
+ log3(
10475
+ "CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
10476
+ "warn"
10477
+ );
10478
+ } else if (!failed && lastTickFailed === true) {
10479
+ log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
10480
+ }
10481
+ lastTickFailed = failed;
10482
+ })().finally(() => {
10483
+ if (activeTickAbort === controller) activeTickAbort = void 0;
10484
+ if (inFlight === tick) inFlight = void 0;
10485
+ scheduleTick(intervalMs, () => startTick(intervalMs));
10486
+ });
10487
+ inFlight = tick;
10488
+ };
10489
+ const performFlush = async () => {
10490
+ stopped = true;
10491
+ if (timer) {
10492
+ clearTimeout(timer);
10493
+ timer = void 0;
10494
+ }
10495
+ const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
10496
+ if (inFlight) {
10497
+ const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
10498
+ if (!settled) {
10499
+ activeTickAbort?.abort();
10500
+ const settledAfterAbort = await waitForSettlement(
10501
+ inFlight,
10502
+ CREDENTIAL_FLUSH_ABORT_GRACE_MS
10503
+ );
10504
+ if (!settledAfterAbort) {
10505
+ log3(
10506
+ "CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
10507
+ "warn"
10508
+ );
10509
+ return { outcomes: outcomesWith("timeout"), orphaned: true };
10510
+ }
10511
+ }
10512
+ }
10513
+ if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
10514
+ const outcomes = outcomesWith("timeout");
10515
+ for (const store of STORES) {
10516
+ const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
10517
+ if (result.orphaned) {
10518
+ log3(
10519
+ "CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
10520
+ "warn"
10521
+ );
10522
+ return { outcomes: outcomesWith("timeout"), orphaned: true };
10523
+ }
10524
+ outcomes[store] = result.outcome;
10525
+ }
10526
+ return { outcomes, orphaned: false };
10527
+ };
10528
+ let flushPasses = 0;
10529
+ let lastFlush;
10530
+ return {
10531
+ arm() {
10532
+ if (stopped || armed) return;
10533
+ armed = true;
10534
+ if (persistenceDisabled) {
10535
+ disabled = true;
10536
+ log3(
10537
+ "CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
10538
+ "warn"
10539
+ );
10540
+ return;
10541
+ }
10542
+ disabled = false;
10543
+ const intervalMs = intervalSeconds(env, log3) * 1e3;
10544
+ scheduleTick(intervalMs, () => startTick(intervalMs));
10545
+ },
10546
+ async stopAndFlush(publish) {
10547
+ let result;
10548
+ const runningFlush = flushPromise;
10549
+ if (runningFlush) {
10550
+ result = await runningFlush;
10551
+ } else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
10552
+ result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
10553
+ } else {
10554
+ flushPasses++;
10555
+ const currentFlush = performFlush();
10556
+ flushPromise = currentFlush;
10557
+ try {
10558
+ result = await currentFlush;
10559
+ lastFlush = result;
10560
+ } finally {
10561
+ if (flushPromise === currentFlush) flushPromise = void 0;
10562
+ }
10563
+ }
10564
+ if (publish) writeMarker(markerPath, result.outcomes, log3);
10565
+ return result.outcomes;
10566
+ }
10567
+ };
9080
10568
  }
9081
10569
 
9082
10570
  // src/commands/run.ts
@@ -9116,7 +10604,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
9116
10604
  if (trimmed === "") {
9117
10605
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
9118
10606
  }
9119
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join9(homeDir, trimmed.slice(2)) : trimmed;
10607
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
9120
10608
  if (!isAbsolute3(expanded)) {
9121
10609
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
9122
10610
  }
@@ -9140,6 +10628,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
9140
10628
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
9141
10629
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
9142
10630
  var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
10631
+ var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
10632
+ function resolveOpenCodeVersion(options, env = process.env) {
10633
+ let raw;
10634
+ let source;
10635
+ if (options.opencodeVersion !== void 0) {
10636
+ raw = options.opencodeVersion;
10637
+ source = "--opencode-version";
10638
+ } else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
10639
+ raw = env[OPENCODE_VERSION_ENV];
10640
+ source = OPENCODE_VERSION_ENV;
10641
+ } else {
10642
+ return { version: "v1", warnings: [] };
10643
+ }
10644
+ const normalized = raw.trim().toLowerCase();
10645
+ if (normalized !== "v1" && normalized !== "v2") {
10646
+ return {
10647
+ version: "v1",
10648
+ warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
10649
+ };
10650
+ }
10651
+ return { version: normalized, warnings: [] };
10652
+ }
9143
10653
  function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
9144
10654
  const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
9145
10655
  let raw;
@@ -9206,7 +10716,7 @@ function log2(state, message, level = "info") {
9206
10716
  })
9207
10717
  );
9208
10718
  } else if (!state.interactive) {
9209
- const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
10719
+ const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
9210
10720
  console.log(`${prefix} ${message}`);
9211
10721
  }
9212
10722
  }
@@ -9236,7 +10746,7 @@ function logActivity(state, entry) {
9236
10746
  }
9237
10747
  function reportSessionDbRecovery(state) {
9238
10748
  try {
9239
- const report = drainSessionDbRecoveryReport({ homeDir: homedir5(), env: process.env });
10749
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
9240
10750
  for (const record of report.records) {
9241
10751
  const activity = buildSessionDbRecoveryActivity(record);
9242
10752
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -9267,18 +10777,18 @@ function reportSessionDbRecoveryRecord(state, record) {
9267
10777
  function displayStatus(state) {
9268
10778
  if (!state.interactive) return;
9269
10779
  const attempt = state.connection?.reconnectAttempt ?? 0;
9270
- const tunnel = state.connected ? chalk6.green("tunnel: connected") : attempt > 0 ? chalk6.yellow(`tunnel: reconnecting (#${attempt})`) : chalk6.yellow("tunnel: connecting");
9271
- const opencode = state.opencodeConnected ? chalk6.green(`opencode: :${state.port}`) : chalk6.red(`opencode: :${state.port} (down)`);
9272
- const messages = state.messageCount > 0 ? chalk6.dim(` \xB7 ${state.messageCount} processed`) : "";
10780
+ const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
10781
+ const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
10782
+ const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
9273
10783
  const last = state.activityLog[state.activityLog.length - 1];
9274
- const detail = last ? chalk6.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
10784
+ const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
9275
10785
  const agent = state.agentName ?? state.agentId;
9276
10786
  console.log(
9277
- `${chalk6.bold("Evident")} ${chalk6.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
10787
+ `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
9278
10788
  );
9279
10789
  }
9280
10790
  async function promptForLogin(promptMessage, successMessage) {
9281
- const action = await select3({
10791
+ const action = await select4({
9282
10792
  message: promptMessage,
9283
10793
  choices: [
9284
10794
  {
@@ -9294,7 +10804,7 @@ async function promptForLogin(promptMessage, successMessage) {
9294
10804
  ]
9295
10805
  });
9296
10806
  if (action === "exit") {
9297
- console.log(chalk6.dim(`
10807
+ console.log(chalk7.dim(`
9298
10808
  You can log in later by running: ${getCliName()} login`));
9299
10809
  process.exit(0);
9300
10810
  }
@@ -9305,7 +10815,7 @@ You can log in later by running: ${getCliName()} login`));
9305
10815
  process.exit(1);
9306
10816
  }
9307
10817
  blank();
9308
- console.log(chalk6.green(successMessage));
10818
+ console.log(chalk7.green(successMessage));
9309
10819
  blank();
9310
10820
  return { token: credentials2.token, authType: "bearer", user: credentials2.user };
9311
10821
  }
@@ -9318,12 +10828,12 @@ async function handleAuthError(state, error2) {
9318
10828
  if (state.interactive) displayStatus(state);
9319
10829
  if (!state.interactive) {
9320
10830
  blank();
9321
- console.log(chalk6.red("Authentication expired"));
9322
- console.log(chalk6.dim("Your authentication token is no longer valid."));
10831
+ console.log(chalk7.red("Authentication expired"));
10832
+ console.log(chalk7.dim("Your authentication token is no longer valid."));
9323
10833
  blank();
9324
- console.log(chalk6.dim("To fix this:"));
9325
- console.log(chalk6.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
9326
- console.log(chalk6.dim(" 2. Restart this command"));
10834
+ console.log(chalk7.dim("To fix this:"));
10835
+ console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
10836
+ console.log(chalk7.dim(" 2. Restart this command"));
9327
10837
  blank();
9328
10838
  await cleanup(state);
9329
10839
  await shutdownTelemetry();
@@ -9331,7 +10841,7 @@ async function handleAuthError(state, error2) {
9331
10841
  return { success: false };
9332
10842
  }
9333
10843
  blank();
9334
- console.log(chalk6.yellow("Your authentication has expired."));
10844
+ console.log(chalk7.yellow("Your authentication has expired."));
9335
10845
  blank();
9336
10846
  try {
9337
10847
  const credentials2 = await promptForLogin(
@@ -9376,6 +10886,10 @@ async function driveChannels(state, driver) {
9376
10886
  consecutiveDrainFailures = 0;
9377
10887
  unreachableMs = 0;
9378
10888
  state.messageCount += processed;
10889
+ if (driver.recycleRequested) {
10890
+ await beginGracefulShutdown(state, "recycle");
10891
+ return;
10892
+ }
9379
10893
  const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
9380
10894
  lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
9381
10895
  const fileActivitySnapshot = driver.fileSyncActivity();
@@ -9391,6 +10905,14 @@ async function driveChannels(state, driver) {
9391
10905
  const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
9392
10906
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
9393
10907
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
10908
+ if (claudeCredentialApplied || opencodeAuthApplied) {
10909
+ void reloadProviderCache(state.port).catch(
10910
+ (error2) => logActivity(state, {
10911
+ type: "error",
10912
+ error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
10913
+ })
10914
+ );
10915
+ }
9394
10916
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
9395
10917
  idlePolls = 0;
9396
10918
  idleMs = 0;
@@ -9418,8 +10940,8 @@ async function driveChannels(state, driver) {
9418
10940
  state.running = false;
9419
10941
  break;
9420
10942
  }
9421
- const errorMessage = error2 instanceof Error ? error2.message : String(error2);
9422
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage}` });
10943
+ const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
10944
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
9423
10945
  if (state.interactive) displayStatus(state);
9424
10946
  if (driver.hasInFlightWatchers()) {
9425
10947
  consecutiveDrainFailures = 0;
@@ -9457,9 +10979,18 @@ async function driveChannels(state, driver) {
9457
10979
  }
9458
10980
  }
9459
10981
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
9460
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
10982
+ var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
10983
+ function shouldWarnForReclaimSkip(reason) {
10984
+ if (reason !== "sqlite-unavailable") return false;
10985
+ const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
10986
+ if (!version2) return false;
10987
+ const major = Number(version2[1]);
10988
+ const minor = Number(version2[2]);
10989
+ const patch = Number(version2[3]);
10990
+ return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
10991
+ }
9461
10992
  function sessionDbPath() {
9462
- return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
10993
+ return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
9463
10994
  }
9464
10995
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
9465
10996
  const record = {
@@ -9555,7 +11086,7 @@ async function runSweep(state, driver, config) {
9555
11086
  } else {
9556
11087
  logActivity(state, {
9557
11088
  type: "info",
9558
- message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
11089
+ message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
9559
11090
  });
9560
11091
  }
9561
11092
  } catch (error2) {
@@ -9578,13 +11109,20 @@ function scheduleSessionCleanup(state, driver, options) {
9578
11109
  for (const warning2 of config.warnings) {
9579
11110
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
9580
11111
  }
9581
- const dbBytes = statSessionDbBytes(homedir5());
11112
+ const dbBytes = statSessionDbBytes(homedir6());
9582
11113
  void (async () => {
9583
- const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
11114
+ const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
11115
+ if (reclaimAvailability !== null) {
11116
+ logActivity(state, {
11117
+ type: "info",
11118
+ level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
11119
+ message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
11120
+ });
11121
+ }
9584
11122
  const sizeWarning = buildSessionStoreSizeWarning({
9585
11123
  dbBytes,
9586
11124
  cleanupEnabled: config.enabled,
9587
- reclaimSkipReason
11125
+ reclaimSkipReason: reclaimAvailability?.reason ?? null
9588
11126
  });
9589
11127
  if (sizeWarning !== null) {
9590
11128
  logActivity(state, { type: "info", level: "warn", message: sizeWarning });
@@ -9779,7 +11317,8 @@ function scheduleResourceUsageReporting(state, options) {
9779
11317
  });
9780
11318
  return;
9781
11319
  }
9782
- const collect = createResourceUsageCollector(homedir5());
11320
+ const { collect, stop } = createResourceUsageCollector(homedir6());
11321
+ state.stopResourceUsageSampling = stop;
9783
11322
  let consecutiveFailures = 0;
9784
11323
  const tick = async () => {
9785
11324
  try {
@@ -9875,6 +11414,8 @@ async function cleanup(state, opts = {}) {
9875
11414
  clearTimeout(timer);
9876
11415
  }
9877
11416
  state.sessionCleanupTimers = [];
11417
+ state.stopOpenCodeLogTail?.();
11418
+ state.stopOpenCodeLogTail = null;
9878
11419
  if (state.claudeUsageTimer) {
9879
11420
  clearTimeout(state.claudeUsageTimer);
9880
11421
  state.claudeUsageTimer = null;
@@ -9889,21 +11430,41 @@ async function cleanup(state, opts = {}) {
9889
11430
  clearTimeout(state.resourceUsageTimer);
9890
11431
  state.resourceUsageTimer = null;
9891
11432
  }
11433
+ state.stopResourceUsageSampling?.();
11434
+ state.stopResourceUsageSampling = null;
11435
+ const credentialSync = state.credentialSync;
11436
+ const flushCredentials = credentialSync ? async (phase, publish) => {
11437
+ await timeShutdownPhase(state, durations, phase, async () => {
11438
+ const outcomes = await credentialSync.stopAndFlush(publish);
11439
+ const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
11440
+ log2(
11441
+ state,
11442
+ `Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
11443
+ level
11444
+ );
11445
+ });
11446
+ } : void 0;
11447
+ let drainSettled = true;
9892
11448
  if (opts.graceful && state.channelDriver) {
9893
11449
  state.channelDriver.stop();
11450
+ }
11451
+ if (flushCredentials) {
11452
+ await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
11453
+ }
11454
+ if (opts.graceful && state.channelDriver) {
9894
11455
  log2(state, "Draining in-flight channel work before shutdown...");
9895
11456
  if (state.interactive) {
9896
11457
  logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
9897
11458
  displayStatus(state);
9898
11459
  }
9899
11460
  const driver = state.channelDriver;
9900
- const settled = await timeShutdownPhase(
11461
+ drainSettled = await timeShutdownPhase(
9901
11462
  state,
9902
11463
  durations,
9903
11464
  "drain",
9904
11465
  () => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
9905
11466
  );
9906
- if (!settled) {
11467
+ if (!drainSettled) {
9907
11468
  logActivity(state, {
9908
11469
  type: "info",
9909
11470
  message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
@@ -9911,6 +11472,9 @@ async function cleanup(state, opts = {}) {
9911
11472
  if (state.interactive) displayStatus(state);
9912
11473
  }
9913
11474
  }
11475
+ if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
11476
+ await flushCredentials("credential_flush_final", true);
11477
+ }
9914
11478
  await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
9915
11479
  if (state.connection) {
9916
11480
  const connection = state.connection;
@@ -9946,13 +11510,51 @@ async function cleanup(state, opts = {}) {
9946
11510
  }
9947
11511
  return durations;
9948
11512
  }
11513
+ async function beginGracefulShutdown(state, trigger) {
11514
+ if (state.shuttingDown) return;
11515
+ state.shuttingDown = true;
11516
+ const shutdownStartedAt = Date.now();
11517
+ const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
11518
+ if (state.interactive) {
11519
+ logActivity(state, { type: "info", message: shutdownMessage });
11520
+ displayStatus(state);
11521
+ } else {
11522
+ log2(state, shutdownMessage);
11523
+ }
11524
+ const durations = await cleanup(state, { graceful: true });
11525
+ const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
11526
+ await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
11527
+ let timer;
11528
+ const flushed = shutdownTelemetry().then(
11529
+ () => true,
11530
+ (error2) => {
11531
+ log2(
11532
+ state,
11533
+ `Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
11534
+ "warn"
11535
+ );
11536
+ return true;
11537
+ }
11538
+ );
11539
+ const timedOut = new Promise((resolve4) => {
11540
+ timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
11541
+ });
11542
+ if (!await Promise.race([flushed, timedOut])) {
11543
+ log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
11544
+ }
11545
+ clearTimeout(timer);
11546
+ });
11547
+ const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
11548
+ log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
11549
+ process.exit(0);
11550
+ }
9949
11551
  async function run(options) {
9950
11552
  const interactive = isInteractive(options.json);
9951
11553
  let logLevel;
9952
11554
  let fileSyncDirectories;
9953
11555
  try {
9954
11556
  logLevel = resolveLogLevel(options);
9955
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir5());
11557
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
9956
11558
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
9957
11559
  throw new Error(
9958
11560
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -9983,6 +11585,7 @@ async function run(options) {
9983
11585
  opencodeVersion: null,
9984
11586
  sessionDbProvenanceAnomaly: false,
9985
11587
  opencodeProcess: null,
11588
+ stopOpenCodeLogTail: null,
9986
11589
  litestreamProcess: null,
9987
11590
  connection: null,
9988
11591
  channelDriver: null,
@@ -9997,9 +11600,24 @@ async function run(options) {
9997
11600
  openaiUsageTimer: null,
9998
11601
  openaiUsageRearm: null,
9999
11602
  resourceUsageTimer: null,
11603
+ stopResourceUsageSampling: null,
11604
+ credentialSync: null,
10000
11605
  authHeader: ""
10001
11606
  };
10002
11607
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
11608
+ if (options.credentialSyncMarker) {
11609
+ state.credentialSync = createCredentialSync({
11610
+ markerPath: options.credentialSyncMarker,
11611
+ env: process.env,
11612
+ log: (message, level = "info") => {
11613
+ if (level === "error") {
11614
+ logActivity(state, { type: "error", error: message });
11615
+ } else {
11616
+ logActivity(state, { type: "info", level, message });
11617
+ }
11618
+ }
11619
+ });
11620
+ }
10003
11621
  if (fileSyncDirectories.length > 0) {
10004
11622
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
10005
11623
  } else {
@@ -10025,43 +11643,7 @@ async function run(options) {
10025
11643
  "warn"
10026
11644
  );
10027
11645
  }
10028
- const handleSignal = async () => {
10029
- if (state.shuttingDown) return;
10030
- state.shuttingDown = true;
10031
- const shutdownStartedAt = Date.now();
10032
- if (state.interactive) {
10033
- logActivity(state, { type: "info", message: "Shutting down..." });
10034
- displayStatus(state);
10035
- } else {
10036
- log2(state, "Shutting down...");
10037
- }
10038
- const durations = await cleanup(state, { graceful: true });
10039
- const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
10040
- await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
10041
- let timer;
10042
- const flushed = shutdownTelemetry().then(
10043
- () => true,
10044
- (error2) => {
10045
- log2(
10046
- state,
10047
- `Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
10048
- "warn"
10049
- );
10050
- return true;
10051
- }
10052
- );
10053
- const timedOut = new Promise((resolve4) => {
10054
- timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
10055
- });
10056
- if (!await Promise.race([flushed, timedOut])) {
10057
- log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
10058
- }
10059
- clearTimeout(timer);
10060
- });
10061
- const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
10062
- log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
10063
- process.exit(0);
10064
- };
11646
+ const handleSignal = () => beginGracefulShutdown(state, "signal");
10065
11647
  process.on("SIGINT", handleSignal);
10066
11648
  process.on("SIGTERM", handleSignal);
10067
11649
  try {
@@ -10071,15 +11653,15 @@ async function run(options) {
10071
11653
  printError("Authentication required");
10072
11654
  blank();
10073
11655
  console.log(
10074
- chalk6.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
11656
+ chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
10075
11657
  );
10076
- console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
11658
+ console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
10077
11659
  blank();
10078
11660
  process.exit(1);
10079
11661
  return;
10080
11662
  }
10081
11663
  blank();
10082
- console.log(chalk6.yellow("You are not logged in to Evident."));
11664
+ console.log(chalk7.yellow("You are not logged in to Evident."));
10083
11665
  blank();
10084
11666
  credentials2 = await promptForLogin(
10085
11667
  "Would you like to log in now?",
@@ -10129,7 +11711,7 @@ async function run(options) {
10129
11711
  );
10130
11712
  blank();
10131
11713
  console.log(
10132
- chalk6.dim(
11714
+ chalk7.dim(
10133
11715
  "Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
10134
11716
  )
10135
11717
  );
@@ -10152,15 +11734,15 @@ async function run(options) {
10152
11734
  );
10153
11735
  if (interactive && !state.json) {
10154
11736
  blank();
10155
- console.log(chalk6.bold("Evident Run"));
10156
- console.log(chalk6.dim("-".repeat(40)));
11737
+ console.log(chalk7.bold("Evident Run"));
11738
+ console.log(chalk7.dim("-".repeat(40)));
10157
11739
  }
10158
11740
  const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
10159
11741
  let validation = await getAgentInfo(state.agentId, state.authHeader);
10160
11742
  if (!validation.valid && validation.authFailed && interactive) {
10161
11743
  spinner?.fail("Authentication failed");
10162
11744
  blank();
10163
- console.log(chalk6.yellow("Your authentication token is invalid or expired."));
11745
+ console.log(chalk7.yellow("Your authentication token is invalid or expired."));
10164
11746
  blank();
10165
11747
  credentials2 = await promptForLogin(
10166
11748
  "Would you like to log in again?",
@@ -10207,6 +11789,14 @@ async function run(options) {
10207
11789
  await restoreCredentialStores(credentialContext);
10208
11790
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
10209
11791
  }
11792
+ state.credentialSync?.arm();
11793
+ state.stopOpenCodeLogTail = tailOpenCodeLogFile(
11794
+ resolveOpenCodeLogPath(homedir6(), process.env),
11795
+ createOpenCodeActivityForwarder(() => ({
11796
+ agentId: state.agentId,
11797
+ authHeader: state.authHeader
11798
+ }))
11799
+ ).stop;
10210
11800
  let sessionDbVerifyFatal = false;
10211
11801
  if (!options.restoreSessionDb) {
10212
11802
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -10256,6 +11846,13 @@ async function run(options) {
10256
11846
  for (const warning2 of opencodeStartTimeoutWarnings) {
10257
11847
  logActivity(state, { type: "info", level: "warn", message: warning2 });
10258
11848
  }
11849
+ const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
11850
+ options,
11851
+ process.env
11852
+ );
11853
+ for (const warning2 of opencodeVersionWarnings) {
11854
+ logActivity(state, { type: "info", level: "warn", message: warning2 });
11855
+ }
10259
11856
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
10260
11857
  for (const warning2 of maxActiveSessionsWarnings) {
10261
11858
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -10263,7 +11860,14 @@ async function run(options) {
10263
11860
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
10264
11861
  const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
10265
11862
  try {
10266
- const oc = await ensureOpenCodeRunning({
11863
+ const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
11864
+ port: state.port,
11865
+ interactive: state.interactive,
11866
+ agentId: state.agentId,
11867
+ log: (message) => log2(state, message),
11868
+ startTimeoutMs: opencodeStartTimeoutMs,
11869
+ inheritStdio: Boolean(options.opencodePidFile)
11870
+ }) : await ensureOpenCodeRunning({
10267
11871
  port: state.port,
10268
11872
  interactive: state.interactive,
10269
11873
  agentId: state.agentId,
@@ -10276,7 +11880,7 @@ async function run(options) {
10276
11880
  state.opencodeVersion = oc.version;
10277
11881
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
10278
11882
  try {
10279
- writeFileSync5(options.opencodePidFile, `${oc.process.pid}
11883
+ writeFileSync6(options.opencodePidFile, `${oc.process.pid}
10280
11884
  `, { mode: 384 });
10281
11885
  chmodSync3(options.opencodePidFile, 384);
10282
11886
  } catch (error2) {
@@ -10290,7 +11894,7 @@ async function run(options) {
10290
11894
  const provenance = checkSessionDbProvenance({
10291
11895
  dbPath: sessionDbPath(),
10292
11896
  currentVersion: state.opencodeVersion,
10293
- homeDir: homedir5(),
11897
+ homeDir: homedir6(),
10294
11898
  env: process.env
10295
11899
  });
10296
11900
  if (provenance.anomaly) {
@@ -10317,6 +11921,7 @@ async function run(options) {
10317
11921
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
10318
11922
  }
10319
11923
  }
11924
+ await reloadProviderCache(state.port);
10320
11925
  const noProviderWarning = buildNoProviderWarning(
10321
11926
  await hasAnyConfiguredProvider(state.port)
10322
11927
  );
@@ -10325,10 +11930,10 @@ async function run(options) {
10325
11930
  if (state.interactive && !state.json) {
10326
11931
  logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
10327
11932
  blank();
10328
- console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
11933
+ console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
10329
11934
  console.log(
10330
- chalk6.dim(
10331
- `Run ${chalk6.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
11935
+ chalk7.dim(
11936
+ `Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
10332
11937
  )
10333
11938
  );
10334
11939
  blank();
@@ -10392,7 +11997,7 @@ async function run(options) {
10392
11997
  });
10393
11998
  try {
10394
11999
  if (litestreamProcess.pid !== void 0) {
10395
- writeFileSync5(options.litestreamPidFile, `${litestreamProcess.pid}
12000
+ writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
10396
12001
  `, {
10397
12002
  mode: 384
10398
12003
  });
@@ -10452,7 +12057,7 @@ async function run(options) {
10452
12057
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
10453
12058
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
10454
12059
  fileSyncDirectories,
10455
- homeDir: homedir5(),
12060
+ homeDir: homedir6(),
10456
12061
  maxActiveSessions,
10457
12062
  log: (entry) => (
10458
12063
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -10578,6 +12183,18 @@ async function run(options) {
10578
12183
  if (state.interactive) displayStatus(state);
10579
12184
  });
10580
12185
  },
12186
+ // Both loops are rearmed because `rearm()` is idempotent for the
12187
+ // provider that did not just connect, and is a no-op when reporting is off.
12188
+ onUsageRearmPing: () => {
12189
+ if (!state.running) return;
12190
+ logActivity(state, {
12191
+ type: "info",
12192
+ level: "debug",
12193
+ message: "Usage rearm ping received"
12194
+ });
12195
+ state.claudeUsageRearm?.();
12196
+ state.openaiUsageRearm?.();
12197
+ },
10581
12198
  onInfo: (message) => logActivity(state, { type: "info", message })
10582
12199
  }
10583
12200
  });
@@ -10598,7 +12215,17 @@ async function run(options) {
10598
12215
  setTimer: (timer) => {
10599
12216
  state.openaiUsageTimer = timer;
10600
12217
  },
10601
- fetchUsage: () => getOpenAiUsage(state.port),
12218
+ fetchUsage: async () => {
12219
+ const usage = await getOpenAiUsage(state.port);
12220
+ if (usage.subscription === null) {
12221
+ logActivity(state, {
12222
+ type: "info",
12223
+ level: "debug",
12224
+ message: "OpenAI usage subscription could not be identified from the local credential"
12225
+ });
12226
+ }
12227
+ return usage;
12228
+ },
10602
12229
  report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
10603
12230
  isLocalCredentialProblem: isLocalCredentialProblem2,
10604
12231
  forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
@@ -10672,6 +12299,9 @@ program.command("run").description("Connect to Evident and process messages").op
10672
12299
  ).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option(
10673
12300
  "--opencode-start-timeout <seconds>",
10674
12301
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
12302
+ ).option(
12303
+ "--opencode-version <v1|v2>",
12304
+ "Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
10675
12305
  ).option("--json", "Output in JSON format").option(
10676
12306
  "--session-cleanup-max-age <duration>",
10677
12307
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -10722,6 +12352,9 @@ program.command("run").description("Connect to Evident and process messages").op
10722
12352
  ).option(
10723
12353
  "--opencode-config-overlay <path>",
10724
12354
  "Apply this runner-provided OpenCode config before starting OpenCode."
12355
+ ).option(
12356
+ "--credential-sync-marker <path>",
12357
+ "Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
10725
12358
  ).action(
10726
12359
  (options) => {
10727
12360
  run({
@@ -10737,6 +12370,7 @@ program.command("run").description("Connect to Evident and process messages").op
10737
12370
  // Raw string — validation/precedence is single-sourced in run.ts's
10738
12371
  // resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
10739
12372
  opencodeStartTimeout: options.opencodeStartTimeout,
12373
+ opencodeVersion: options.opencodeVersion,
10740
12374
  json: options.json,
10741
12375
  // Raw strings — the resolver in run.ts single-sources parsing (M1).
10742
12376
  sessionCleanupMaxAge: options.sessionCleanupMaxAge,
@@ -10760,7 +12394,8 @@ program.command("run").description("Connect to Evident and process messages").op
10760
12394
  sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
10761
12395
  restoreSessionDb: options.restoreSessionDb,
10762
12396
  restoreRunnerCredentials: options.restoreRunnerCredentials,
10763
- opencodeConfigOverlay: options.opencodeConfigOverlay
12397
+ opencodeConfigOverlay: options.opencodeConfigOverlay,
12398
+ credentialSyncMarker: options.credentialSyncMarker
10764
12399
  });
10765
12400
  }
10766
12401
  );