@evident-ai/cli 3.4.1-dev.c138c09 → 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
  });
@@ -763,13 +768,6 @@ function toReportedOpenAiWindow(window) {
763
768
  resets_at: window.resetsAt
764
769
  };
765
770
  }
766
- function toReportedOpenAiSubscription(snapshot) {
767
- if (!snapshot.subscription) return null;
768
- return {
769
- owner_email: snapshot.subscription.ownerEmail,
770
- plan_type: snapshot.subscription.planType
771
- };
772
- }
773
771
  async function reportOpenAiUsage(agentId, authHeader, snapshot) {
774
772
  try {
775
773
  const apiUrl = getApiUrlConfig();
@@ -781,7 +779,7 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
781
779
  secondary: toReportedOpenAiWindow(snapshot.secondary),
782
780
  has_credits: snapshot.hasCredits,
783
781
  credits_unlimited: snapshot.creditsUnlimited,
784
- subscription: toReportedOpenAiSubscription(snapshot)
782
+ subscription: toReportedSubscription(snapshot.subscription)
785
783
  }),
786
784
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
787
785
  });
@@ -1009,10 +1007,10 @@ async function status(options = {}) {
1009
1007
  }
1010
1008
 
1011
1009
  // src/lib/claude-usage.ts
1012
- import { execFileSync } from "child_process";
1013
- import { readFileSync } from "fs";
1014
- import { homedir } from "os";
1015
- 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";
1016
1014
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1017
1015
  var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
1018
1016
  var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
@@ -1097,7 +1095,7 @@ function ownerLookupFailure(error2) {
1097
1095
  }
1098
1096
  async function getClaudeUsageOwner(accessToken) {
1099
1097
  if (cachedOwner?.accessToken === accessToken) {
1100
- return { owner: cachedOwner.owner, ownerLookupError: null };
1098
+ return { subscription: cachedOwner.owner, ownerLookupError: null };
1101
1099
  }
1102
1100
  try {
1103
1101
  const response = await fetch(CLAUDE_PROFILE_URL, {
@@ -1109,27 +1107,27 @@ async function getClaudeUsageOwner(accessToken) {
1109
1107
  signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1110
1108
  });
1111
1109
  if (!response.ok) {
1112
- return { owner: null, ownerLookupError: `HTTP ${response.status}` };
1110
+ return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
1113
1111
  }
1114
1112
  let body;
1115
1113
  try {
1116
1114
  body = await response.json();
1117
1115
  } catch (error2) {
1118
- return { owner: null, ownerLookupError: "malformed response" };
1116
+ return { subscription: null, ownerLookupError: "malformed response" };
1119
1117
  }
1120
1118
  const profile = body;
1121
1119
  if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
1122
- return { owner: null, ownerLookupError: "malformed response" };
1120
+ return { subscription: null, ownerLookupError: "malformed response" };
1123
1121
  }
1124
- const owner = {
1125
- email: profile.account.email,
1122
+ const subscription = {
1123
+ ownerEmail: profile.account.email,
1126
1124
  organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
1127
- 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
1128
1126
  };
1129
- cachedOwner = { accessToken, owner };
1130
- return { owner, ownerLookupError: null };
1127
+ cachedOwner = { accessToken, owner: subscription };
1128
+ return { subscription, ownerLookupError: null };
1131
1129
  } catch (error2) {
1132
- return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
1130
+ return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
1133
1131
  }
1134
1132
  }
1135
1133
  async function getClaudeUsage() {
@@ -1158,11 +1156,11 @@ async function getClaudeUsage() {
1158
1156
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1159
1157
  }
1160
1158
  const body = await res.json();
1161
- const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1159
+ const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1162
1160
  return {
1163
1161
  fiveHour: toWindow(body.five_hour),
1164
1162
  sevenDay: toWindow(body.seven_day),
1165
- owner,
1163
+ subscription,
1166
1164
  ownerLookupError
1167
1165
  };
1168
1166
  }
@@ -1192,10 +1190,10 @@ async function claudeUsage() {
1192
1190
  }
1193
1191
 
1194
1192
  // src/commands/run.ts
1195
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
1196
- import { homedir as homedir5 } from "os";
1197
- import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
1198
- 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";
1199
1197
 
1200
1198
  // ../../packages/types/src/agents/index.ts
1201
1199
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
@@ -1254,7 +1252,7 @@ function stripQuery(url) {
1254
1252
 
1255
1253
  // src/commands/run.ts
1256
1254
  import ora3 from "ora";
1257
- import { select as select3 } from "@inquirer/prompts";
1255
+ import { select as select4 } from "@inquirer/prompts";
1258
1256
 
1259
1257
  // src/lib/telemetry.ts
1260
1258
  var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
@@ -1427,12 +1425,50 @@ var SEVERITY_BY_LEVEL = {
1427
1425
  warn: "warning",
1428
1426
  error: "error"
1429
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
+ }
1430
1466
  var MAX_MESSAGE_LENGTH = 500;
1431
1467
  var MAX_METADATA_VALUE_LENGTH = 200;
1432
1468
  var MAX_METADATA_ENTRIES = 20;
1433
1469
  var TRUNCATION_MARKER = "\u2026";
1434
1470
  function redact(message) {
1435
- 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>");
1436
1472
  }
1437
1473
  function truncate(message) {
1438
1474
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
@@ -1458,43 +1494,47 @@ function sanitiseMetadata(metadata) {
1458
1494
  }
1459
1495
  var RATE_LIMIT_WINDOW_MS = 6e4;
1460
1496
  var RATE_LIMIT_MAX_EVENTS = 30;
1461
- var windowStartedAt = 0;
1462
- var windowCount = 0;
1463
- var windowDroppedCount = 0;
1464
- function admitUnderRateLimit(now) {
1465
- if (now - windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1466
- 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) {
1467
1506
  console.error(
1468
- `[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}"`
1469
1508
  );
1470
1509
  }
1471
- windowStartedAt = now;
1472
- windowCount = 0;
1473
- windowDroppedCount = 0;
1510
+ window.windowStartedAt = now;
1511
+ window.windowCount = 0;
1512
+ window.windowDroppedCount = 0;
1474
1513
  }
1475
- if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
1476
- windowDroppedCount++;
1477
- if (windowDroppedCount === 1) {
1514
+ if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1515
+ window.windowDroppedCount++;
1516
+ if (window.windowDroppedCount === 1) {
1478
1517
  console.error(
1479
- `[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}"`
1480
1519
  );
1481
1520
  }
1482
1521
  return false;
1483
1522
  }
1484
- windowCount++;
1523
+ window.windowCount++;
1485
1524
  return true;
1486
1525
  }
1487
1526
  function forwardRunnerActivity(entry, context) {
1488
1527
  try {
1489
1528
  if (!FORWARDED_LEVELS.has(entry.level)) return;
1490
1529
  if (!context.agentId || !context.authHeader) return;
1491
- if (!admitUnderRateLimit(Date.now())) return;
1530
+ const source = entry.source ?? "cli.run";
1531
+ if (!admitUnderRateLimit(source, Date.now())) return;
1492
1532
  const rawMessage = entry.error ?? entry.message ?? "";
1493
1533
  const message = truncate(redact(rawMessage));
1494
1534
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1495
1535
  severity: SEVERITY_BY_LEVEL[entry.level],
1496
1536
  message,
1497
- metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
1537
+ metadata: { ...sanitiseMetadata(entry.metadata), source },
1498
1538
  agentId: context.agentId
1499
1539
  });
1500
1540
  } catch (err) {
@@ -1505,8 +1545,8 @@ function forwardRunnerActivity(entry, context) {
1505
1545
  }
1506
1546
 
1507
1547
  // src/lib/opencode/session-db-recovery-report.ts
1508
- import { readFileSync as readFileSync2, unlinkSync } from "fs";
1509
- import { join as join2 } from "path";
1548
+ import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
1549
+ import { join as join2 } from "node:path";
1510
1550
  function sessionDbRecoveryReportPath(homeDir, env) {
1511
1551
  const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1512
1552
  return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
@@ -1719,13 +1759,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1719
1759
  }
1720
1760
 
1721
1761
  // src/lib/opencode/session-db-boot.ts
1722
- import { spawn as spawn2 } from "child_process";
1723
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1724
- import { homedir as homedir2 } from "os";
1725
- 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";
1726
1766
 
1727
1767
  // src/lib/runner-synchroniser.ts
1728
- import { spawn } from "child_process";
1768
+ import { spawn } from "node:child_process";
1729
1769
  function appendError(stderr, error2) {
1730
1770
  const message = error2 instanceof Error ? error2.message : String(error2);
1731
1771
  return stderr === "" ? message : `${stderr}
@@ -2250,9 +2290,9 @@ async function restoreAndVerifySessionDb(options) {
2250
2290
  }
2251
2291
 
2252
2292
  // src/lib/opencode/session-db-provenance.ts
2253
- import { createRequire } from "module";
2254
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2255
- 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";
2256
2296
  var require2 = createRequire(import.meta.url);
2257
2297
  function readSessionDbMigrationIds(dbPath) {
2258
2298
  let db;
@@ -2446,6 +2486,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2446
2486
 
2447
2487
  // src/lib/opencode/process.ts
2448
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
+ }
2449
2500
  function getProcessCwd(pid) {
2450
2501
  const platform = process.platform;
2451
2502
  try {
@@ -2494,14 +2545,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
2494
2545
  }
2495
2546
  return null;
2496
2547
  }
2497
- function findOpenCodeProcesses() {
2548
+ function findProcessesByPattern(pgrepPattern, psPattern) {
2498
2549
  const instances = [];
2499
2550
  try {
2500
2551
  const platform = process.platform;
2501
2552
  if (platform === "darwin" || platform === "linux") {
2502
2553
  let pids = [];
2503
2554
  try {
2504
- const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
2555
+ const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
2505
2556
  encoding: "utf-8",
2506
2557
  stdio: ["pipe", "pipe", "pipe"]
2507
2558
  }).trim();
@@ -2510,7 +2561,7 @@ function findOpenCodeProcesses() {
2510
2561
  }
2511
2562
  } catch {
2512
2563
  try {
2513
- 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`, {
2514
2565
  encoding: "utf-8",
2515
2566
  stdio: ["pipe", "pipe", "pipe"]
2516
2567
  }).trim();
@@ -2556,6 +2607,9 @@ function findOpenCodeProcesses() {
2556
2607
  }
2557
2608
  return instances;
2558
2609
  }
2610
+ function findOpenCodeProcesses() {
2611
+ return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
2612
+ }
2559
2613
  async function scanPortsForOpenCode() {
2560
2614
  const instances = [];
2561
2615
  const checks = OPENCODE_PORT_RANGE.map(async (port) => {
@@ -2602,7 +2656,7 @@ async function findHealthyOpenCodeInstances() {
2602
2656
  }
2603
2657
  async function startOpenCode(port, options = {}) {
2604
2658
  let command = "opencode";
2605
- const printLogs = options.inheritStdio ? ["--print-logs"] : [];
2659
+ const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
2606
2660
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
2607
2661
  try {
2608
2662
  execSync("which opencode", { stdio: "ignore" });
@@ -2659,6 +2713,19 @@ function isOpenCodeInstalled() {
2659
2713
  return false;
2660
2714
  }
2661
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
+ }
2662
2729
  async function promptOpenCodeInstall(interactive) {
2663
2730
  if (!interactive) {
2664
2731
  console.log(
@@ -2668,7 +2735,11 @@ async function promptOpenCodeInstall(interactive) {
2668
2735
  install_url: OPENCODE_INSTALL_URL,
2669
2736
  install_commands: {
2670
2737
  npm: "npm install -g opencode-ai",
2671
- 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
+ }
2672
2743
  }
2673
2744
  })
2674
2745
  );
@@ -3190,21 +3261,74 @@ function collectSubagentSessions(messages, userMessageId) {
3190
3261
  }
3191
3262
  return refs;
3192
3263
  }
3193
- function messageUsage(messages, userMessageId) {
3194
- if (!messages || messages.length === 0) return null;
3195
- const byParentAll = messages.filter(
3196
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
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)
3197
3303
  );
3198
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3199
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3200
- let correlated;
3201
- if (byParent.length > 0) {
3202
- correlated = byParent;
3203
- } else {
3204
- const reply = findAssistantReplyAfter(messages, userMessageId);
3205
- correlated = reply ? [reply] : [];
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);
3206
3319
  }
3207
- if (correlated.length === 0) return null;
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;
3208
3332
  let sawAnyUsage = false;
3209
3333
  let inputSum = 0;
3210
3334
  let outputSum = 0;
@@ -3215,7 +3339,7 @@ function messageUsage(messages, userMessageId) {
3215
3339
  let sawCost = false;
3216
3340
  let modelId = null;
3217
3341
  let providerId = null;
3218
- for (const m of correlated) {
3342
+ for (const m of selected) {
3219
3343
  const info = m.info;
3220
3344
  if (!info) continue;
3221
3345
  const tokens = info.tokens;
@@ -3250,12 +3374,28 @@ function messageUsage(messages, userMessageId) {
3250
3374
  usage_tokens_reasoning: reasoningSum,
3251
3375
  usage_tokens_cache_read: cacheReadSum,
3252
3376
  usage_tokens_cache_write: cacheWriteSum,
3253
- // NULL means "OpenCode never reported a cost" (never inferred from
3254
- // tokens) distinct from a genuine 0-cost turn, which would set
3255
- // `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`.
3256
3379
  usage_cost_usd: sawCost ? costSum : null
3257
3380
  };
3258
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
+ }
3259
3399
  function messageRunState(messages, userMessageId) {
3260
3400
  if (!messages || messages.length === 0) return "unknown";
3261
3401
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3385,6 +3525,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3385
3525
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3386
3526
  );
3387
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
+ }
3388
3550
  async function hasAnyConfiguredProvider(port) {
3389
3551
  try {
3390
3552
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
@@ -3416,6 +3578,94 @@ async function hasAnyConfiguredProvider(port) {
3416
3578
  return null;
3417
3579
  }
3418
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
+ }
3419
3669
 
3420
3670
  // src/lib/opencode/session-cleanup.ts
3421
3671
  var DURATION_UNIT_MS = {
@@ -3522,8 +3772,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
3522
3772
  }
3523
3773
 
3524
3774
  // src/lib/opencode/session-db-size.ts
3525
- import { statSync as statSync3 } from "fs";
3526
- import { join as join4 } from "path";
3775
+ import { statSync as statSync3 } from "node:fs";
3776
+ import { join as join4 } from "node:path";
3527
3777
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
3528
3778
  function statSessionDbBytes(homeDir) {
3529
3779
  const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
@@ -3553,9 +3803,96 @@ function buildSessionStoreSizeWarning(input) {
3553
3803
  return null;
3554
3804
  }
3555
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
+
3556
3889
  // src/lib/opencode/session-db-reclaim.ts
3557
- import { statSync as statSync4, statfsSync } from "fs";
3558
- 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
+ }
3559
3896
  function insufficientSpaceReason(dbPath, requiredBytes) {
3560
3897
  try {
3561
3898
  const fsStats = statfsSync(dirname4(dbPath));
@@ -3581,17 +3918,17 @@ async function probeReclaimAvailability(input) {
3581
3918
  const { dbPath, requiredBytes } = input;
3582
3919
  let sqlite;
3583
3920
  try {
3584
- sqlite = await import("sqlite");
3921
+ sqlite = await import("node:sqlite");
3585
3922
  } catch (err) {
3586
- console.warn(
3587
- `[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3588
- );
3589
- 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 };
3590
3926
  }
3591
3927
  let autoVacuum = null;
3592
3928
  try {
3593
3929
  const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
3594
3930
  try {
3931
+ db.exec("PRAGMA busy_timeout=5000");
3595
3932
  autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3596
3933
  } finally {
3597
3934
  db.close();
@@ -3602,23 +3939,25 @@ async function probeReclaimAvailability(input) {
3602
3939
  );
3603
3940
  }
3604
3941
  if (autoVacuum !== 0) return null;
3605
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
3942
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
3606
3943
  }
3607
3944
  async function reclaimSessionDbSpace(input) {
3608
3945
  const { dbPath, maxPages, allowFullVacuum = true } = input;
3609
3946
  let sqlite;
3610
3947
  try {
3611
- sqlite = await import("sqlite");
3948
+ sqlite = await import("node:sqlite");
3612
3949
  } catch (err) {
3950
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
3613
3951
  console.warn(
3614
- `[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}`
3615
3953
  );
3616
- return { ok: false, skipped: "sqlite-unavailable" };
3954
+ return { ok: false, skipped: "sqlite-unavailable", detail };
3617
3955
  }
3618
3956
  const { DatabaseSync } = sqlite;
3619
3957
  let db;
3620
3958
  try {
3621
3959
  db = new DatabaseSync(dbPath);
3960
+ db.exec("PRAGMA busy_timeout=5000");
3622
3961
  const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3623
3962
  if (autoVacuum === 0) {
3624
3963
  if (!allowFullVacuum) {
@@ -3627,7 +3966,7 @@ async function reclaimSessionDbSpace(input) {
3627
3966
  );
3628
3967
  return { ok: false, skipped: "full-vacuum-blocked" };
3629
3968
  }
3630
- const fileBytesForGuard = statSync4(dbPath).size;
3969
+ const fileBytesForGuard = statSync5(dbPath).size;
3631
3970
  const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
3632
3971
  if (skipReason !== null) {
3633
3972
  console.warn(
@@ -3655,10 +3994,12 @@ async function reclaimSessionDbSpace(input) {
3655
3994
  );
3656
3995
  return { ok: false, skipped: "auto-vacuum-not-applicable" };
3657
3996
  } catch (err) {
3658
- console.error(
3659
- `[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3660
- );
3661
- 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
+ };
3662
4003
  } finally {
3663
4004
  db?.close();
3664
4005
  }
@@ -3950,8 +4291,8 @@ function connectTunnel(options) {
3950
4291
  try {
3951
4292
  message = JSON.parse(data.toString());
3952
4293
  } catch (error2) {
3953
- const errorMessage2 = error2 instanceof Error ? error2.message : "Unknown error";
3954
- onError?.(`Failed to handle message: ${errorMessage2}`);
4294
+ const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
4295
+ onError?.(`Failed to handle message: ${errorMessage3}`);
3955
4296
  return;
3956
4297
  }
3957
4298
  if (isStreamFrame(message)) {
@@ -4095,7 +4436,7 @@ var RunnerConnection = class {
4095
4436
  };
4096
4437
 
4097
4438
  // src/lib/tunnel/ready-marker.ts
4098
- import { writeFileSync as writeFileSync3 } from "fs";
4439
+ import { writeFileSync as writeFileSync3 } from "node:fs";
4099
4440
  function writeTunnelReadyMarker(path, agentId) {
4100
4441
  try {
4101
4442
  writeFileSync3(path, `${agentId}
@@ -4107,7 +4448,7 @@ function writeTunnelReadyMarker(path, agentId) {
4107
4448
  }
4108
4449
 
4109
4450
  // src/lib/replication.ts
4110
- import { spawn as spawn4 } from "child_process";
4451
+ import { spawn as spawn4 } from "node:child_process";
4111
4452
  function startSessionDbReplication(configPath) {
4112
4453
  return spawn4("litestream", ["replicate", "-config", configPath], {
4113
4454
  stdio: "inherit"
@@ -4123,7 +4464,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
4123
4464
  }
4124
4465
 
4125
4466
  // src/lib/process-liveness.ts
4126
- import { readFileSync as readFileSync4 } from "fs";
4467
+ import { readFileSync as readFileSync4 } from "node:fs";
4127
4468
  function isProcessAlive(pid) {
4128
4469
  try {
4129
4470
  process.kill(pid, 0);
@@ -4149,9 +4490,9 @@ function isProcessAlive(pid) {
4149
4490
  }
4150
4491
 
4151
4492
  // src/lib/openai-usage.ts
4152
- import { readFileSync as readFileSync5 } from "fs";
4153
- import { homedir as homedir3 } from "os";
4154
- 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";
4155
4496
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4156
4497
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4157
4498
  var OpenAiUsageError = class extends Error {
@@ -4165,7 +4506,7 @@ function isLocalCredentialProblem2(err) {
4165
4506
  }
4166
4507
  function readOpenCodeChatGptCredentials() {
4167
4508
  try {
4168
- const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4509
+ const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4169
4510
  let parsed;
4170
4511
  try {
4171
4512
  parsed = JSON.parse(raw);
@@ -4202,7 +4543,7 @@ function parseChatGptIdentity(accessToken) {
4202
4543
  const auth = payload["https://api.openai.com/auth"];
4203
4544
  const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4204
4545
  const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4205
- return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
4546
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
4206
4547
  }
4207
4548
  function toWindow2(headers, name) {
4208
4549
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
@@ -4427,8 +4768,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
4427
4768
  }
4428
4769
 
4429
4770
  // src/lib/resource-usage.ts
4430
- import { cpus, totalmem, freemem } from "os";
4431
- import { statfsSync as statfsSync2 } from "fs";
4771
+ import { cpus, totalmem, freemem } from "node:os";
4772
+ import { statfsSync as statfsSync2 } from "node:fs";
4432
4773
 
4433
4774
  // src/lib/ecs-task-metadata.ts
4434
4775
  var ECS_METADATA_TIMEOUT_MS = 2e3;
@@ -4595,15 +4936,15 @@ function createResourceUsageCollector(homeDir) {
4595
4936
  }
4596
4937
 
4597
4938
  // src/lib/channels/driver.ts
4598
- import { homedir as homedir4 } from "os";
4939
+ import { homedir as homedir5 } from "node:os";
4599
4940
 
4600
4941
  // src/lib/runner-file-sync.ts
4601
- import { join as join7 } from "path";
4942
+ import { join as join8 } from "node:path";
4602
4943
 
4603
4944
  // src/lib/file-push.ts
4604
- import { randomUUID } from "crypto";
4605
- import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
4606
- 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";
4607
4948
  var FILE_MODE = 384;
4608
4949
  var DIRECTORY_MODE = 448;
4609
4950
  async function writePushedFile(request) {
@@ -4636,7 +4977,7 @@ async function writePushedFile(request) {
4636
4977
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
4637
4978
  dirname5(candidate)
4638
4979
  );
4639
- const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
4980
+ const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
4640
4981
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
4641
4982
  if (allowedDirectory === null) {
4642
4983
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -4672,7 +5013,7 @@ function expandAndValidate(requestedPath, homeDir) {
4672
5013
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
4673
5014
  return null;
4674
5015
  }
4675
- 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;
4676
5017
  if (expanded.split(/[/\\]/).includes("..")) {
4677
5018
  return null;
4678
5019
  }
@@ -4745,16 +5086,16 @@ function contains(realDirectory, realTarget) {
4745
5086
  async function createMissingDirectories(existingAncestor, missingSegments) {
4746
5087
  let current = existingAncestor;
4747
5088
  for (const segment of missingSegments) {
4748
- current = join6(current, segment);
5089
+ current = join7(current, segment);
4749
5090
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
4750
5091
  await chmod(current, DIRECTORY_MODE);
4751
5092
  }
4752
5093
  }
4753
5094
  async function writeAtomically(realTarget, content) {
4754
- const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
5095
+ const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
4755
5096
  let handle;
4756
5097
  try {
4757
- handle = await open2(temporaryPath, "wx", FILE_MODE);
5098
+ handle = await open3(temporaryPath, "wx", FILE_MODE);
4758
5099
  await handle.writeFile(content);
4759
5100
  await handle.chmod(FILE_MODE);
4760
5101
  await handle.close();
@@ -4881,12 +5222,12 @@ var NOT_APPLIED = {
4881
5222
  opencodeAuthApplied: false
4882
5223
  };
4883
5224
  function isClaudeCredentialPath(requestedPath, homeDir) {
4884
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4885
- 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);
4886
5227
  }
4887
5228
  function isOpenCodeAuthPath(requestedPath, homeDir) {
4888
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4889
- 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);
4890
5231
  }
4891
5232
  async function applyOne(options, file) {
4892
5233
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -5046,6 +5387,10 @@ var DEFAULT_RETRY_POLICY = {
5046
5387
  baseDelayMs: 500,
5047
5388
  maxDelayMs: 3e4
5048
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;
5049
5394
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
5050
5395
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
5051
5396
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5186,6 +5531,17 @@ var ChannelDriver = class _ChannelDriver {
5186
5531
  * message; it is removed once its in-flight set empties.
5187
5532
  */
5188
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();
5189
5545
  /**
5190
5546
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5191
5547
  * dispatched and are still in-flight. A message in this set is never
@@ -5369,6 +5725,13 @@ var ChannelDriver = class _ChannelDriver {
5369
5725
  * no watcher) can resolve the title.
5370
5726
  */
5371
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();
5372
5735
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5373
5736
  draining = false;
5374
5737
  /**
@@ -5433,7 +5796,7 @@ var ChannelDriver = class _ChannelDriver {
5433
5796
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
5434
5797
  this.now = config.now ?? (() => Date.now());
5435
5798
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
5436
- this.homeDir = config.homeDir ?? homedir4();
5799
+ this.homeDir = config.homeDir ?? homedir5();
5437
5800
  this.maxActiveSessions = config.maxActiveSessions;
5438
5801
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5439
5802
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -5583,6 +5946,21 @@ var ChannelDriver = class _ChannelDriver {
5583
5946
  }
5584
5947
  return ids;
5585
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
+ }
5586
5964
  /**
5587
5965
  * File-pull work, for `run.ts`'s idle accounting (#559).
5588
5966
  *
@@ -5649,6 +6027,8 @@ var ChannelDriver = class _ChannelDriver {
5649
6027
  */
5650
6028
  stop() {
5651
6029
  this.stopped = true;
6030
+ this.sessionErrorStream?.abort.abort();
6031
+ this.sessionErrorStream = null;
5652
6032
  }
5653
6033
  /**
5654
6034
  * The server clears this request when a new MicroVM identity is recorded, so a
@@ -5723,6 +6103,7 @@ var ChannelDriver = class _ChannelDriver {
5723
6103
  */
5724
6104
  async processConversation(conv) {
5725
6105
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6106
+ this.ensureSessionErrorStream();
5726
6107
  const messages = await this.getPendingMessages(conv.id);
5727
6108
  let dispatched = 0;
5728
6109
  let skippedAlreadyDispatched = 0;
@@ -5795,7 +6176,7 @@ var ChannelDriver = class _ChannelDriver {
5795
6176
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
5796
6177
  break;
5797
6178
  }
5798
- const errorMessage2 = err instanceof Error ? err.message : String(err);
6179
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
5799
6180
  this.sessions.delete(conv.id);
5800
6181
  this.supersede(conv.id, sessionId);
5801
6182
  this.log({
@@ -5804,7 +6185,7 @@ var ChannelDriver = class _ChannelDriver {
5804
6185
  conversation_id: conv.id,
5805
6186
  message_id: message.id
5806
6187
  });
5807
- await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
6188
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5808
6189
  this.log({
5809
6190
  level: "warn",
5810
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)}`,
@@ -5815,7 +6196,7 @@ var ChannelDriver = class _ChannelDriver {
5815
6196
  });
5816
6197
  this.log({
5817
6198
  level: "error",
5818
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage2}`,
6199
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
5819
6200
  conversation_id: conv.id,
5820
6201
  message_id: message.id
5821
6202
  });
@@ -5836,14 +6217,14 @@ var ChannelDriver = class _ChannelDriver {
5836
6217
  this.unconfirmedDispatchFailures.delete(message.id);
5837
6218
  this.sessions.delete(conv.id);
5838
6219
  this.supersede(conv.id, sessionId);
5839
- const errorMessage2 = `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.`;
5840
6221
  this.log({
5841
6222
  level: "error",
5842
- message: errorMessage2,
6223
+ message: errorMessage3,
5843
6224
  conversation_id: conv.id,
5844
6225
  message_id: message.id
5845
6226
  });
5846
- await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
6227
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5847
6228
  this.log({
5848
6229
  level: "warn",
5849
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)}`,
@@ -6069,6 +6450,23 @@ var ChannelDriver = class _ChannelDriver {
6069
6450
  if (state === "running" || state === "queued") {
6070
6451
  const ongoing = await isSessionOngoing(this.port, sessionId);
6071
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
+ }
6072
6470
  return this.reattachRedrive(conv, sessionId, message, ocId);
6073
6471
  }
6074
6472
  if (ongoing === false) {
@@ -6158,16 +6556,37 @@ var ChannelDriver = class _ChannelDriver {
6158
6556
  if (state === "done") {
6159
6557
  const title = await this.resolveSessionTitle(sessionId, conv.id);
6160
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
+ );
6161
6565
  this.log({
6162
6566
  level: "info",
6163
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`,
6164
6568
  conversation_id: conv.id,
6165
6569
  message_id: message.id
6166
6570
  });
6167
- 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
+ );
6168
6581
  } else {
6169
6582
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
6170
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
+ );
6171
6590
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
6172
6591
  this.log({
6173
6592
  level: "error",
@@ -6175,7 +6594,16 @@ var ChannelDriver = class _ChannelDriver {
6175
6594
  conversation_id: conv.id,
6176
6595
  message_id: message.id
6177
6596
  });
6178
- 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
+ );
6179
6607
  }
6180
6608
  if (ocId !== null) {
6181
6609
  await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
@@ -6719,6 +7147,24 @@ var ChannelDriver = class _ChannelDriver {
6719
7147
  ambiguousPinnedSinceMs: 0,
6720
7148
  ambiguousResolved: false
6721
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
+ }
7156
+ }
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
+ }
6722
7168
  }
6723
7169
  /**
6724
7170
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
@@ -6923,6 +7369,7 @@ var ChannelDriver = class _ChannelDriver {
6923
7369
  ensureWatcherRunning(sessionId) {
6924
7370
  const watcher = this.watchers.get(sessionId);
6925
7371
  if (!watcher) return;
7372
+ this.ensureSessionErrorStream();
6926
7373
  if (watcher.loop) return;
6927
7374
  if (watcher.inFlight.size === 0) {
6928
7375
  this.watchers.delete(sessionId);
@@ -6938,6 +7385,154 @@ var ChannelDriver = class _ChannelDriver {
6938
7385
  });
6939
7386
  watcher.loop = loop;
6940
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
+ }
6941
7536
  /**
6942
7537
  * The per-session polling loop (WI-3). Once per tick it:
6943
7538
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -7050,6 +7645,21 @@ var ChannelDriver = class _ChannelDriver {
7050
7645
  const conv = watcher.conv;
7051
7646
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7052
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
+ }
7053
7663
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
7054
7664
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
7055
7665
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -7103,6 +7713,12 @@ var ChannelDriver = class _ChannelDriver {
7103
7713
  message_id: inFlight.evidentMessageId
7104
7714
  });
7105
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
+ );
7106
7722
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
7107
7723
  try {
7108
7724
  await this.markFailed(
@@ -7111,7 +7727,9 @@ var ChannelDriver = class _ChannelDriver {
7111
7727
  sessionId,
7112
7728
  error2,
7113
7729
  usage,
7114
- failure
7730
+ failure,
7731
+ usageAgentName,
7732
+ subagentInvocations
7115
7733
  );
7116
7734
  } catch (err) {
7117
7735
  if (err instanceof ChannelAuthError) throw err;
@@ -7154,9 +7772,11 @@ var ChannelDriver = class _ChannelDriver {
7154
7772
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7155
7773
  return;
7156
7774
  }
7775
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
7776
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
7157
7777
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
7158
7778
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
7159
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
7779
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
7160
7780
  inFlight.stuckReported = true;
7161
7781
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
7162
7782
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7317,7 +7937,7 @@ var ChannelDriver = class _ChannelDriver {
7317
7937
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7318
7938
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7319
7939
  );
7320
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
7940
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7321
7941
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7322
7942
  this.log({
7323
7943
  level: "debug",
@@ -7352,6 +7972,12 @@ var ChannelDriver = class _ChannelDriver {
7352
7972
  });
7353
7973
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7354
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
+ );
7355
7981
  try {
7356
7982
  await this.markDone(
7357
7983
  conv.id,
@@ -7359,7 +7985,9 @@ var ChannelDriver = class _ChannelDriver {
7359
7985
  sessionId,
7360
7986
  inFlight.opencodeMessageId,
7361
7987
  title,
7362
- usage
7988
+ usage,
7989
+ usageAgentName,
7990
+ subagentInvocations
7363
7991
  );
7364
7992
  } catch (err) {
7365
7993
  if (err instanceof ChannelAuthError) throw err;
@@ -7538,6 +8166,12 @@ var ChannelDriver = class _ChannelDriver {
7538
8166
  if (state === "failed" && !restartAborted) {
7539
8167
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7540
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
+ );
7541
8175
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7542
8176
  this.log({
7543
8177
  level: "error",
@@ -7546,7 +8180,16 @@ var ChannelDriver = class _ChannelDriver {
7546
8180
  message_id: row.id
7547
8181
  });
7548
8182
  try {
7549
- 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
+ );
7550
8193
  } catch (err) {
7551
8194
  if (err instanceof ChannelAuthError) throw err;
7552
8195
  if (err instanceof ChannelTerminalError) {
@@ -7708,7 +8351,22 @@ var ChannelDriver = class _ChannelDriver {
7708
8351
  try {
7709
8352
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7710
8353
  const usage = messageUsage(messages, ocId ?? "");
7711
- 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
+ );
7712
8370
  } catch (err) {
7713
8371
  if (err instanceof ChannelAuthError) throw err;
7714
8372
  if (err instanceof ChannelTerminalError) {
@@ -7826,14 +8484,14 @@ var ChannelDriver = class _ChannelDriver {
7826
8484
  this.unconfirmedDispatchFailures.delete(row.id);
7827
8485
  this.sessions.delete(readoptConv.id);
7828
8486
  this.supersede(readoptConv.id, sessionId);
7829
- const errorMessage2 = `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.`;
7830
8488
  this.log({
7831
8489
  level: "error",
7832
- message: errorMessage2,
8490
+ message: errorMessage3,
7833
8491
  conversation_id: row.conversation_id,
7834
8492
  message_id: row.id
7835
8493
  });
7836
- await this.markFailed(row.conversation_id, row.id, null, errorMessage2).catch((markErr) => {
8494
+ await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
7837
8495
  this.log({
7838
8496
  level: "warn",
7839
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)}`,
@@ -8112,6 +8770,166 @@ var ChannelDriver = class _ChannelDriver {
8112
8770
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
8113
8771
  return parent;
8114
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
+ }
8115
8933
  /**
8116
8934
  * OpenCode's synchronous default session title (e.g.
8117
8935
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8595,7 +9413,7 @@ var ChannelDriver = class _ChannelDriver {
8595
9413
  * watcher retries next tick within the
8596
9414
  * deadline, Finding 4).
8597
9415
  */
8598
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
9416
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8599
9417
  const res = await this.fetchImpl(
8600
9418
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8601
9419
  {
@@ -8611,15 +9429,21 @@ var ChannelDriver = class _ChannelDriver {
8611
9429
  opencode_session_id: sessionId,
8612
9430
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8613
9431
  ...title ? { title } : {},
8614
- ...usage ? usage : {}
9432
+ ...usage ? usage : {},
9433
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
9434
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8615
9435
  })
8616
9436
  }
8617
9437
  );
8618
9438
  this.assertAuth(res, "marking message as done");
8619
- if (res.ok) return;
9439
+ if (res.ok) {
9440
+ this.clearSubagentInvocationCaches(messageId);
9441
+ return;
9442
+ }
8620
9443
  if (isRetryableStatus(res.status)) {
8621
9444
  throw new Error(`marking message as done: HTTP ${res.status}`);
8622
9445
  }
9446
+ this.clearSubagentInvocationCaches(messageId);
8623
9447
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8624
9448
  }
8625
9449
  /**
@@ -8634,7 +9458,7 @@ var ChannelDriver = class _ChannelDriver {
8634
9458
  * exists but is wedged, so the next attempt must get a fresh one
8635
9459
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8636
9460
  */
8637
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
9461
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8638
9462
  const body = { status: "failed" };
8639
9463
  if (sessionId === null) {
8640
9464
  body.opencode_session_id = null;
@@ -8643,23 +9467,33 @@ var ChannelDriver = class _ChannelDriver {
8643
9467
  }
8644
9468
  if (error2 !== void 0) body.error = error2;
8645
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
+ }
8646
9474
  if (failure) {
8647
9475
  body.failure_kind = failure.kind;
8648
9476
  body.failure_provider_id = failure.providerId;
8649
9477
  body.failure_model_id = failure.modelId;
8650
9478
  body.failure_reason = failure.reason;
8651
9479
  }
8652
- await this.callWithRetry(
8653
- "marking message as failed",
8654
- () => this.fetchImpl(
8655
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8656
- {
8657
- method: "PATCH",
8658
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8659
- body: JSON.stringify(body)
8660
- }
8661
- )
8662
- );
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);
8663
9497
  }
8664
9498
  /**
8665
9499
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -8942,6 +9776,13 @@ import chalk5 from "chalk";
8942
9776
  import ora2 from "ora";
8943
9777
  import { select as select2 } from "@inquirer/prompts";
8944
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
+ }
8945
9786
  async function ensureOpenCodeRunning(ctx) {
8946
9787
  const healthCheck = await checkOpenCodeHealth(ctx.port);
8947
9788
  if (healthCheck.healthy) {
@@ -8989,6 +9830,7 @@ async function ensureOpenCodeRunning(ctx) {
8989
9830
  }
8990
9831
  }
8991
9832
  if (!ctx.interactive) {
9833
+ checkNonInteractivePortConflict(ctx.port, isPortInUse);
8992
9834
  ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
8993
9835
  const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
8994
9836
  const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
@@ -9070,9 +9912,119 @@ Port ${port} is already in use.`));
9070
9912
  return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
9071
9913
  }
9072
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
+
9073
10025
  // src/lib/runner-credentials.ts
9074
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
9075
- 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";
9076
10028
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
9077
10029
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
9078
10030
  var GITHUB_PROBE_TIMEOUT_MS = 1e4;
@@ -9344,11 +10296,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
9344
10296
  }
9345
10297
 
9346
10298
  // src/lib/opencode/config-overlay.ts
9347
- import { execFileSync as execFileSync2 } from "child_process";
9348
- import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
9349
- 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";
9350
10302
  function isFile(filePath) {
9351
- return existsSync2(filePath) && statSync5(filePath).isFile();
10303
+ return existsSync2(filePath) && statSync6(filePath).isFile();
9352
10304
  }
9353
10305
  function applyRunnerOpenCodeConfig({
9354
10306
  overlayPath,
@@ -9360,7 +10312,7 @@ function applyRunnerOpenCodeConfig({
9360
10312
  return;
9361
10313
  }
9362
10314
  const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
9363
- const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
10315
+ const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
9364
10316
  if (!isFile(source)) {
9365
10317
  log3(
9366
10318
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -9368,7 +10320,7 @@ function applyRunnerOpenCodeConfig({
9368
10320
  );
9369
10321
  return;
9370
10322
  }
9371
- copyFileSync(source, join8(cwd, target));
10323
+ copyFileSync(source, join9(cwd, target));
9372
10324
  try {
9373
10325
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
9374
10326
  stdio: "ignore"
@@ -9377,11 +10329,11 @@ function applyRunnerOpenCodeConfig({
9377
10329
  const detail = error2 instanceof Error ? error2.message : String(error2);
9378
10330
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
9379
10331
  }
9380
- log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
10332
+ log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
9381
10333
  }
9382
10334
 
9383
10335
  // src/lib/credential-sync.ts
9384
- import { renameSync, writeFileSync as writeFileSync5 } from "fs";
10336
+ import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
9385
10337
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
9386
10338
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
9387
10339
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -9391,7 +10343,7 @@ var MAX_FLUSH_PASSES = 2;
9391
10343
  function outcomesWith(outcome) {
9392
10344
  return { claude: outcome, opencode: outcome };
9393
10345
  }
9394
- function errorMessage(error2) {
10346
+ function errorMessage2(error2) {
9395
10347
  return error2 instanceof Error ? error2.message : String(error2);
9396
10348
  }
9397
10349
  function waitForSettlement(promise, timeoutMs) {
@@ -9418,7 +10370,7 @@ function writeMarker(markerPath, outcomes, log3) {
9418
10370
  writeFileSync5(temporaryPath, body, { mode: 384 });
9419
10371
  renameSync(temporaryPath, markerPath);
9420
10372
  } catch (error2) {
9421
- log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage(error2)}`, "warn");
10373
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
9422
10374
  }
9423
10375
  }
9424
10376
  function intervalSeconds(env, log3) {
@@ -9450,7 +10402,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
9450
10402
  },
9451
10403
  (error2) => {
9452
10404
  failed = true;
9453
- log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage(error2)}`, "warn");
10405
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
9454
10406
  }
9455
10407
  );
9456
10408
  const abortTimer = setTimeout(() => controller.abort(), remainingMs);
@@ -9510,7 +10462,7 @@ function createCredentialSync({
9510
10462
  outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
9511
10463
  } catch (error2) {
9512
10464
  outcomes[store] = "failed";
9513
- log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage(error2)}`, "debug");
10465
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
9514
10466
  }
9515
10467
  }
9516
10468
  const failed = STORES.some((store) => outcomes[store] === "failed");
@@ -9652,7 +10604,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
9652
10604
  if (trimmed === "") {
9653
10605
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
9654
10606
  }
9655
- 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;
9656
10608
  if (!isAbsolute3(expanded)) {
9657
10609
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
9658
10610
  }
@@ -9676,6 +10628,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
9676
10628
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
9677
10629
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
9678
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
+ }
9679
10653
  function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
9680
10654
  const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
9681
10655
  let raw;
@@ -9742,7 +10716,7 @@ function log2(state, message, level = "info") {
9742
10716
  })
9743
10717
  );
9744
10718
  } else if (!state.interactive) {
9745
- 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");
9746
10720
  console.log(`${prefix} ${message}`);
9747
10721
  }
9748
10722
  }
@@ -9772,7 +10746,7 @@ function logActivity(state, entry) {
9772
10746
  }
9773
10747
  function reportSessionDbRecovery(state) {
9774
10748
  try {
9775
- const report = drainSessionDbRecoveryReport({ homeDir: homedir5(), env: process.env });
10749
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
9776
10750
  for (const record of report.records) {
9777
10751
  const activity = buildSessionDbRecoveryActivity(record);
9778
10752
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -9803,18 +10777,18 @@ function reportSessionDbRecoveryRecord(state, record) {
9803
10777
  function displayStatus(state) {
9804
10778
  if (!state.interactive) return;
9805
10779
  const attempt = state.connection?.reconnectAttempt ?? 0;
9806
- const tunnel = state.connected ? chalk6.green("tunnel: connected") : attempt > 0 ? chalk6.yellow(`tunnel: reconnecting (#${attempt})`) : chalk6.yellow("tunnel: connecting");
9807
- const opencode = state.opencodeConnected ? chalk6.green(`opencode: :${state.port}`) : chalk6.red(`opencode: :${state.port} (down)`);
9808
- 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`) : "";
9809
10783
  const last = state.activityLog[state.activityLog.length - 1];
9810
- 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 ?? ""}`) : "";
9811
10785
  const agent = state.agentName ?? state.agentId;
9812
10786
  console.log(
9813
- `${chalk6.bold("Evident")} ${chalk6.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
10787
+ `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
9814
10788
  );
9815
10789
  }
9816
10790
  async function promptForLogin(promptMessage, successMessage) {
9817
- const action = await select3({
10791
+ const action = await select4({
9818
10792
  message: promptMessage,
9819
10793
  choices: [
9820
10794
  {
@@ -9830,7 +10804,7 @@ async function promptForLogin(promptMessage, successMessage) {
9830
10804
  ]
9831
10805
  });
9832
10806
  if (action === "exit") {
9833
- console.log(chalk6.dim(`
10807
+ console.log(chalk7.dim(`
9834
10808
  You can log in later by running: ${getCliName()} login`));
9835
10809
  process.exit(0);
9836
10810
  }
@@ -9841,7 +10815,7 @@ You can log in later by running: ${getCliName()} login`));
9841
10815
  process.exit(1);
9842
10816
  }
9843
10817
  blank();
9844
- console.log(chalk6.green(successMessage));
10818
+ console.log(chalk7.green(successMessage));
9845
10819
  blank();
9846
10820
  return { token: credentials2.token, authType: "bearer", user: credentials2.user };
9847
10821
  }
@@ -9854,12 +10828,12 @@ async function handleAuthError(state, error2) {
9854
10828
  if (state.interactive) displayStatus(state);
9855
10829
  if (!state.interactive) {
9856
10830
  blank();
9857
- console.log(chalk6.red("Authentication expired"));
9858
- 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."));
9859
10833
  blank();
9860
- console.log(chalk6.dim("To fix this:"));
9861
- console.log(chalk6.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
9862
- 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"));
9863
10837
  blank();
9864
10838
  await cleanup(state);
9865
10839
  await shutdownTelemetry();
@@ -9867,7 +10841,7 @@ async function handleAuthError(state, error2) {
9867
10841
  return { success: false };
9868
10842
  }
9869
10843
  blank();
9870
- console.log(chalk6.yellow("Your authentication has expired."));
10844
+ console.log(chalk7.yellow("Your authentication has expired."));
9871
10845
  blank();
9872
10846
  try {
9873
10847
  const credentials2 = await promptForLogin(
@@ -9931,6 +10905,14 @@ async function driveChannels(state, driver) {
9931
10905
  const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
9932
10906
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
9933
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
+ }
9934
10916
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
9935
10917
  idlePolls = 0;
9936
10918
  idleMs = 0;
@@ -9958,8 +10940,8 @@ async function driveChannels(state, driver) {
9958
10940
  state.running = false;
9959
10941
  break;
9960
10942
  }
9961
- const errorMessage2 = error2 instanceof Error ? error2.message : String(error2);
9962
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage2}` });
10943
+ const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
10944
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
9963
10945
  if (state.interactive) displayStatus(state);
9964
10946
  if (driver.hasInFlightWatchers()) {
9965
10947
  consecutiveDrainFailures = 0;
@@ -9997,9 +10979,18 @@ async function driveChannels(state, driver) {
9997
10979
  }
9998
10980
  }
9999
10981
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
10000
- 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
+ }
10001
10992
  function sessionDbPath() {
10002
- return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
10993
+ return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
10003
10994
  }
10004
10995
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
10005
10996
  const record = {
@@ -10095,7 +11086,7 @@ async function runSweep(state, driver, config) {
10095
11086
  } else {
10096
11087
  logActivity(state, {
10097
11088
  type: "info",
10098
- 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}` : "")
10099
11090
  });
10100
11091
  }
10101
11092
  } catch (error2) {
@@ -10118,13 +11109,20 @@ function scheduleSessionCleanup(state, driver, options) {
10118
11109
  for (const warning2 of config.warnings) {
10119
11110
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
10120
11111
  }
10121
- const dbBytes = statSessionDbBytes(homedir5());
11112
+ const dbBytes = statSessionDbBytes(homedir6());
10122
11113
  void (async () => {
10123
- 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
+ }
10124
11122
  const sizeWarning = buildSessionStoreSizeWarning({
10125
11123
  dbBytes,
10126
11124
  cleanupEnabled: config.enabled,
10127
- reclaimSkipReason
11125
+ reclaimSkipReason: reclaimAvailability?.reason ?? null
10128
11126
  });
10129
11127
  if (sizeWarning !== null) {
10130
11128
  logActivity(state, { type: "info", level: "warn", message: sizeWarning });
@@ -10319,7 +11317,7 @@ function scheduleResourceUsageReporting(state, options) {
10319
11317
  });
10320
11318
  return;
10321
11319
  }
10322
- const { collect, stop } = createResourceUsageCollector(homedir5());
11320
+ const { collect, stop } = createResourceUsageCollector(homedir6());
10323
11321
  state.stopResourceUsageSampling = stop;
10324
11322
  let consecutiveFailures = 0;
10325
11323
  const tick = async () => {
@@ -10416,6 +11414,8 @@ async function cleanup(state, opts = {}) {
10416
11414
  clearTimeout(timer);
10417
11415
  }
10418
11416
  state.sessionCleanupTimers = [];
11417
+ state.stopOpenCodeLogTail?.();
11418
+ state.stopOpenCodeLogTail = null;
10419
11419
  if (state.claudeUsageTimer) {
10420
11420
  clearTimeout(state.claudeUsageTimer);
10421
11421
  state.claudeUsageTimer = null;
@@ -10554,7 +11554,7 @@ async function run(options) {
10554
11554
  let fileSyncDirectories;
10555
11555
  try {
10556
11556
  logLevel = resolveLogLevel(options);
10557
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir5());
11557
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
10558
11558
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
10559
11559
  throw new Error(
10560
11560
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -10585,6 +11585,7 @@ async function run(options) {
10585
11585
  opencodeVersion: null,
10586
11586
  sessionDbProvenanceAnomaly: false,
10587
11587
  opencodeProcess: null,
11588
+ stopOpenCodeLogTail: null,
10588
11589
  litestreamProcess: null,
10589
11590
  connection: null,
10590
11591
  channelDriver: null,
@@ -10652,15 +11653,15 @@ async function run(options) {
10652
11653
  printError("Authentication required");
10653
11654
  blank();
10654
11655
  console.log(
10655
- 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")
10656
11657
  );
10657
- console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
11658
+ console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
10658
11659
  blank();
10659
11660
  process.exit(1);
10660
11661
  return;
10661
11662
  }
10662
11663
  blank();
10663
- console.log(chalk6.yellow("You are not logged in to Evident."));
11664
+ console.log(chalk7.yellow("You are not logged in to Evident."));
10664
11665
  blank();
10665
11666
  credentials2 = await promptForLogin(
10666
11667
  "Would you like to log in now?",
@@ -10710,7 +11711,7 @@ async function run(options) {
10710
11711
  );
10711
11712
  blank();
10712
11713
  console.log(
10713
- chalk6.dim(
11714
+ chalk7.dim(
10714
11715
  "Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
10715
11716
  )
10716
11717
  );
@@ -10733,15 +11734,15 @@ async function run(options) {
10733
11734
  );
10734
11735
  if (interactive && !state.json) {
10735
11736
  blank();
10736
- console.log(chalk6.bold("Evident Run"));
10737
- console.log(chalk6.dim("-".repeat(40)));
11737
+ console.log(chalk7.bold("Evident Run"));
11738
+ console.log(chalk7.dim("-".repeat(40)));
10738
11739
  }
10739
11740
  const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
10740
11741
  let validation = await getAgentInfo(state.agentId, state.authHeader);
10741
11742
  if (!validation.valid && validation.authFailed && interactive) {
10742
11743
  spinner?.fail("Authentication failed");
10743
11744
  blank();
10744
- console.log(chalk6.yellow("Your authentication token is invalid or expired."));
11745
+ console.log(chalk7.yellow("Your authentication token is invalid or expired."));
10745
11746
  blank();
10746
11747
  credentials2 = await promptForLogin(
10747
11748
  "Would you like to log in again?",
@@ -10789,6 +11790,13 @@ async function run(options) {
10789
11790
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
10790
11791
  }
10791
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;
10792
11800
  let sessionDbVerifyFatal = false;
10793
11801
  if (!options.restoreSessionDb) {
10794
11802
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -10838,6 +11846,13 @@ async function run(options) {
10838
11846
  for (const warning2 of opencodeStartTimeoutWarnings) {
10839
11847
  logActivity(state, { type: "info", level: "warn", message: warning2 });
10840
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
+ }
10841
11856
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
10842
11857
  for (const warning2 of maxActiveSessionsWarnings) {
10843
11858
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -10845,7 +11860,14 @@ async function run(options) {
10845
11860
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
10846
11861
  const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
10847
11862
  try {
10848
- 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({
10849
11871
  port: state.port,
10850
11872
  interactive: state.interactive,
10851
11873
  agentId: state.agentId,
@@ -10872,7 +11894,7 @@ async function run(options) {
10872
11894
  const provenance = checkSessionDbProvenance({
10873
11895
  dbPath: sessionDbPath(),
10874
11896
  currentVersion: state.opencodeVersion,
10875
- homeDir: homedir5(),
11897
+ homeDir: homedir6(),
10876
11898
  env: process.env
10877
11899
  });
10878
11900
  if (provenance.anomaly) {
@@ -10899,6 +11921,7 @@ async function run(options) {
10899
11921
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
10900
11922
  }
10901
11923
  }
11924
+ await reloadProviderCache(state.port);
10902
11925
  const noProviderWarning = buildNoProviderWarning(
10903
11926
  await hasAnyConfiguredProvider(state.port)
10904
11927
  );
@@ -10907,10 +11930,10 @@ async function run(options) {
10907
11930
  if (state.interactive && !state.json) {
10908
11931
  logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
10909
11932
  blank();
10910
- console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
11933
+ console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
10911
11934
  console.log(
10912
- chalk6.dim(
10913
- `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.`
10914
11937
  )
10915
11938
  );
10916
11939
  blank();
@@ -11034,7 +12057,7 @@ async function run(options) {
11034
12057
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
11035
12058
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
11036
12059
  fileSyncDirectories,
11037
- homeDir: homedir5(),
12060
+ homeDir: homedir6(),
11038
12061
  maxActiveSessions,
11039
12062
  log: (entry) => (
11040
12063
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -11276,6 +12299,9 @@ program.command("run").description("Connect to Evident and process messages").op
11276
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(
11277
12300
  "--opencode-start-timeout <seconds>",
11278
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"
11279
12305
  ).option("--json", "Output in JSON format").option(
11280
12306
  "--session-cleanup-max-age <duration>",
11281
12307
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -11344,6 +12370,7 @@ program.command("run").description("Connect to Evident and process messages").op
11344
12370
  // Raw string — validation/precedence is single-sourced in run.ts's
11345
12371
  // resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
11346
12372
  opencodeStartTimeout: options.opencodeStartTimeout,
12373
+ opencodeVersion: options.opencodeVersion,
11347
12374
  json: options.json,
11348
12375
  // Raw strings — the resolver in run.ts single-sources parsing (M1).
11349
12376
  sessionCleanupMaxAge: options.sessionCleanupMaxAge,