@evident-ai/cli 3.4.1-dev.661a835 → 3.4.1-dev.7393414

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(() => "");
@@ -670,12 +683,15 @@ async function resolveAgentIdFromKey(authHeader) {
670
683
  }
671
684
  }
672
685
  var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
673
- async function notifyAgentDisconnected(agentId, authHeader) {
674
- const apiUrl = getApiUrlConfig();
686
+ async function postBestEffort(path, authHeader, body) {
675
687
  try {
676
- const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
688
+ const apiUrl = getApiUrlConfig();
689
+ const headers = { Authorization: authHeader };
690
+ if (body !== void 0) headers["Content-Type"] = "application/json";
691
+ const response = await fetch(`${apiUrl}${path}`, {
677
692
  method: "POST",
678
- headers: { Authorization: authHeader },
693
+ headers,
694
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
679
695
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
680
696
  });
681
697
  if (!response.ok) {
@@ -687,73 +703,32 @@ async function notifyAgentDisconnected(agentId, authHeader) {
687
703
  }
688
704
  return { ok: true };
689
705
  } catch (error2) {
690
- return { ok: false, error: describeBestEffortError(error2) };
706
+ return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
691
707
  }
692
708
  }
693
- function describeBestEffortError(error2) {
709
+ function describeTimeoutError(error2, timeoutMs) {
694
710
  const name = error2?.name;
695
711
  if (name === "TimeoutError" || name === "AbortError") {
696
- return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;
712
+ return `timed out after ${timeoutMs}ms`;
697
713
  }
698
714
  return error2 instanceof Error ? error2.message : String(error2);
699
715
  }
716
+ async function notifyAgentDisconnected(agentId, authHeader) {
717
+ return postBestEffort(`/runners/${agentId}/disconnect`, authHeader);
718
+ }
700
719
  async function reportMicrovmId(agentId, authHeader, microvmId) {
701
- try {
702
- const apiUrl = getApiUrlConfig();
703
- const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
704
- method: "POST",
705
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
706
- body: JSON.stringify({ microvm_id: microvmId }),
707
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
708
- });
709
- if (!response.ok) {
710
- const serverMessage = await readErrorMessage(response);
711
- return {
712
- ok: false,
713
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
714
- };
715
- }
716
- return { ok: true };
717
- } catch (error2) {
718
- return { ok: false, error: describeBestEffortError(error2) };
719
- }
720
+ return postBestEffort(`/runners/${agentId}/microvm`, authHeader, { microvm_id: microvmId });
720
721
  }
721
722
  function toReportedWindow(window) {
722
723
  if (!window) return null;
723
724
  return { utilization: window.utilization, resets_at: window.resetsAt };
724
725
  }
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
726
  async function reportClaudeUsage(agentId, authHeader, snapshot) {
734
- try {
735
- const apiUrl = getApiUrlConfig();
736
- const response = await fetch(`${apiUrl}/runners/${agentId}/claude-usage`, {
737
- method: "POST",
738
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
739
- body: JSON.stringify({
740
- five_hour: toReportedWindow(snapshot.fiveHour),
741
- seven_day: toReportedWindow(snapshot.sevenDay),
742
- owner: toReportedOwner(snapshot)
743
- }),
744
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
745
- });
746
- if (!response.ok) {
747
- const serverMessage = await readErrorMessage(response);
748
- return {
749
- ok: false,
750
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
751
- };
752
- }
753
- return { ok: true };
754
- } catch (error2) {
755
- return { ok: false, error: describeBestEffortError(error2) };
756
- }
727
+ return postBestEffort(`/runners/${agentId}/claude-usage`, authHeader, {
728
+ five_hour: toReportedWindow(snapshot.fiveHour),
729
+ seven_day: toReportedWindow(snapshot.sevenDay),
730
+ subscription: toReportedSubscription(snapshot.subscription)
731
+ });
757
732
  }
758
733
  function toReportedOpenAiWindow(window) {
759
734
  if (!window) return null;
@@ -763,68 +738,26 @@ function toReportedOpenAiWindow(window) {
763
738
  resets_at: window.resetsAt
764
739
  };
765
740
  }
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
741
  async function reportOpenAiUsage(agentId, authHeader, snapshot) {
774
- try {
775
- const apiUrl = getApiUrlConfig();
776
- const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
777
- method: "POST",
778
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
779
- body: JSON.stringify({
780
- primary: toReportedOpenAiWindow(snapshot.primary),
781
- secondary: toReportedOpenAiWindow(snapshot.secondary),
782
- has_credits: snapshot.hasCredits,
783
- credits_unlimited: snapshot.creditsUnlimited,
784
- subscription: toReportedOpenAiSubscription(snapshot)
785
- }),
786
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
787
- });
788
- if (!response.ok) {
789
- const serverMessage = await readErrorMessage(response);
790
- return {
791
- ok: false,
792
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
793
- };
794
- }
795
- return { ok: true };
796
- } catch (error2) {
797
- return { ok: false, error: describeBestEffortError(error2) };
798
- }
742
+ return postBestEffort(`/runners/${agentId}/openai-usage`, authHeader, {
743
+ primary: toReportedOpenAiWindow(snapshot.primary),
744
+ secondary: toReportedOpenAiWindow(snapshot.secondary),
745
+ has_credits: snapshot.hasCredits,
746
+ credits_unlimited: snapshot.creditsUnlimited,
747
+ subscription: toReportedSubscription(snapshot.subscription)
748
+ });
799
749
  }
800
750
  async function reportResourceUsage(agentId, authHeader, usage) {
801
- try {
802
- const apiUrl = getApiUrlConfig();
803
- const response = await fetch(`${apiUrl}/runners/${agentId}/resource-usage`, {
804
- method: "POST",
805
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
806
- body: JSON.stringify({
807
- cpu_percent: usage.cpuPercent,
808
- cpu_count: usage.cpuCount,
809
- memory_total_bytes: usage.memoryTotalBytes,
810
- memory_available_bytes: usage.memoryAvailableBytes,
811
- disk_total_bytes: usage.diskTotalBytes,
812
- disk_free_bytes: usage.diskFreeBytes,
813
- opencode_db_bytes: usage.opencodeDbBytes
814
- }),
815
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
816
- });
817
- if (!response.ok) {
818
- const serverMessage = await readErrorMessage(response);
819
- return {
820
- ok: false,
821
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
822
- };
823
- }
824
- return { ok: true };
825
- } catch (error2) {
826
- return { ok: false, error: describeBestEffortError(error2) };
827
- }
751
+ return postBestEffort(`/runners/${agentId}/resource-usage`, authHeader, {
752
+ cpu_percent: usage.cpuPercent,
753
+ cpu_peak_percent: usage.cpuPeakPercent,
754
+ cpu_count: usage.cpuCount,
755
+ memory_total_bytes: usage.memoryTotalBytes,
756
+ memory_available_bytes: usage.memoryAvailableBytes,
757
+ disk_total_bytes: usage.diskTotalBytes,
758
+ disk_free_bytes: usage.diskFreeBytes,
759
+ opencode_db_bytes: usage.opencodeDbBytes
760
+ });
828
761
  }
829
762
  async function getAgentInfo(agentId, authHeader) {
830
763
  const apiUrl = getApiUrlConfig();
@@ -876,13 +809,6 @@ function authLabelFor(credentials2) {
876
809
  }
877
810
  return "user token";
878
811
  }
879
- function describeFetchError(error2) {
880
- const name = error2?.name;
881
- if (name === "TimeoutError" || name === "AbortError") {
882
- return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
883
- }
884
- return error2 instanceof Error ? error2.message : String(error2);
885
- }
886
812
  async function checkStatus(jsonMode) {
887
813
  const apiUrl = getApiUrlConfig();
888
814
  const credentials2 = await getAuthCredentials();
@@ -910,7 +836,7 @@ async function checkStatus(jsonMode) {
910
836
  endpoint: apiUrl,
911
837
  authLabel: authLabelFor(credentials2),
912
838
  reason: "unreachable",
913
- error: `Could not reach ${apiUrl}: ${describeFetchError(error2)}. The credentials were NOT validated.`,
839
+ error: `Could not reach ${apiUrl}: ${describeTimeoutError(error2, STATUS_TIMEOUT_MS)}. The credentials were NOT validated.`,
914
840
  exitCode: 75
915
841
  };
916
842
  }
@@ -1008,10 +934,10 @@ async function status(options = {}) {
1008
934
  }
1009
935
 
1010
936
  // src/lib/claude-usage.ts
1011
- import { execFileSync } from "child_process";
1012
- import { readFileSync } from "fs";
1013
- import { homedir } from "os";
1014
- import { join } from "path";
937
+ import { execFileSync } from "node:child_process";
938
+ import { readFileSync } from "node:fs";
939
+ import { homedir } from "node:os";
940
+ import { join } from "node:path";
1015
941
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1016
942
  var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
1017
943
  var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
@@ -1096,7 +1022,7 @@ function ownerLookupFailure(error2) {
1096
1022
  }
1097
1023
  async function getClaudeUsageOwner(accessToken) {
1098
1024
  if (cachedOwner?.accessToken === accessToken) {
1099
- return { owner: cachedOwner.owner, ownerLookupError: null };
1025
+ return { subscription: cachedOwner.owner, ownerLookupError: null };
1100
1026
  }
1101
1027
  try {
1102
1028
  const response = await fetch(CLAUDE_PROFILE_URL, {
@@ -1108,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
1108
1034
  signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1109
1035
  });
1110
1036
  if (!response.ok) {
1111
- return { owner: null, ownerLookupError: `HTTP ${response.status}` };
1037
+ return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
1112
1038
  }
1113
1039
  let body;
1114
1040
  try {
1115
1041
  body = await response.json();
1116
1042
  } catch (error2) {
1117
- return { owner: null, ownerLookupError: "malformed response" };
1043
+ return { subscription: null, ownerLookupError: "malformed response" };
1118
1044
  }
1119
1045
  const profile = body;
1120
1046
  if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
1121
- return { owner: null, ownerLookupError: "malformed response" };
1047
+ return { subscription: null, ownerLookupError: "malformed response" };
1122
1048
  }
1123
- const owner = {
1124
- email: profile.account.email,
1049
+ const subscription = {
1050
+ ownerEmail: profile.account.email,
1125
1051
  organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
1126
- rateLimitTier: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1052
+ planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1127
1053
  };
1128
- cachedOwner = { accessToken, owner };
1129
- return { owner, ownerLookupError: null };
1054
+ cachedOwner = { accessToken, owner: subscription };
1055
+ return { subscription, ownerLookupError: null };
1130
1056
  } catch (error2) {
1131
- return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
1057
+ return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
1132
1058
  }
1133
1059
  }
1134
1060
  async function getClaudeUsage() {
@@ -1157,11 +1083,11 @@ async function getClaudeUsage() {
1157
1083
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1158
1084
  }
1159
1085
  const body = await res.json();
1160
- const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1086
+ const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1161
1087
  return {
1162
1088
  fiveHour: toWindow(body.five_hour),
1163
1089
  sevenDay: toWindow(body.seven_day),
1164
- owner,
1090
+ subscription,
1165
1091
  ownerLookupError
1166
1092
  };
1167
1093
  }
@@ -1191,10 +1117,10 @@ async function claudeUsage() {
1191
1117
  }
1192
1118
 
1193
1119
  // src/commands/run.ts
1194
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
1195
- import { homedir as homedir5 } from "os";
1196
- import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
1197
- import chalk6 from "chalk";
1120
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
1121
+ import { homedir as homedir6 } from "node:os";
1122
+ import { isAbsolute as isAbsolute3, join as join10, parse, resolve as resolvePath2 } from "node:path";
1123
+ import chalk7 from "chalk";
1198
1124
 
1199
1125
  // ../../packages/types/src/agents/index.ts
1200
1126
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
@@ -1215,6 +1141,7 @@ var TelemetryEventTypes = {
1215
1141
  // ../../packages/types/src/tunnel/index.ts
1216
1142
  var MAX_FRAME_BYTES = 256 * 1024;
1217
1143
  var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
1144
+ var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
1218
1145
 
1219
1146
  // ../../packages/types/src/runner-files.ts
1220
1147
  var MAX_FILE_PUSH_BYTES = 64 * 1024;
@@ -1252,7 +1179,7 @@ function stripQuery(url) {
1252
1179
 
1253
1180
  // src/commands/run.ts
1254
1181
  import ora3 from "ora";
1255
- import { select as select3 } from "@inquirer/prompts";
1182
+ import { select as select4 } from "@inquirer/prompts";
1256
1183
 
1257
1184
  // src/lib/telemetry.ts
1258
1185
  var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
@@ -1425,12 +1352,50 @@ var SEVERITY_BY_LEVEL = {
1425
1352
  warn: "warning",
1426
1353
  error: "error"
1427
1354
  };
1355
+ function parseOpenCodeLogLine(line) {
1356
+ const normalisedLine = line.replace(/\r$/, "");
1357
+ const levelMatch = normalisedLine.match(/(?:^|\s)level=(\w+)/i);
1358
+ if (!levelMatch) return null;
1359
+ const level = levelMatch[1].toUpperCase();
1360
+ if (level !== "WARN" && level !== "ERROR") return null;
1361
+ const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
1362
+ return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
1363
+ }
1364
+ var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1365
+ function createOpenCodeActivityForwarder(getContext) {
1366
+ let buffer = Buffer.alloc(0);
1367
+ const flushLine = (line) => {
1368
+ const parsed = parseOpenCodeLogLine(line);
1369
+ if (!parsed) return;
1370
+ forwardRunnerActivity(
1371
+ {
1372
+ level: parsed.level,
1373
+ error: line,
1374
+ metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
1375
+ source: "opencode"
1376
+ },
1377
+ getContext()
1378
+ );
1379
+ };
1380
+ return (chunk) => {
1381
+ buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
1382
+ let newlineIndex;
1383
+ while ((newlineIndex = buffer.indexOf(10)) !== -1) {
1384
+ flushLine(buffer.subarray(0, newlineIndex).toString("utf-8").replace(/\r$/, ""));
1385
+ buffer = buffer.subarray(newlineIndex + 1);
1386
+ }
1387
+ if (buffer.length > MAX_LINE_BUFFER_BYTES) {
1388
+ flushLine(buffer.toString("utf-8"));
1389
+ buffer = Buffer.alloc(0);
1390
+ }
1391
+ };
1392
+ }
1428
1393
  var MAX_MESSAGE_LENGTH = 500;
1429
1394
  var MAX_METADATA_VALUE_LENGTH = 200;
1430
1395
  var MAX_METADATA_ENTRIES = 20;
1431
1396
  var TRUNCATION_MARKER = "\u2026";
1432
1397
  function redact(message) {
1433
- return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
1398
+ 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>");
1434
1399
  }
1435
1400
  function truncate(message) {
1436
1401
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
@@ -1456,43 +1421,47 @@ function sanitiseMetadata(metadata) {
1456
1421
  }
1457
1422
  var RATE_LIMIT_WINDOW_MS = 6e4;
1458
1423
  var RATE_LIMIT_MAX_EVENTS = 30;
1459
- var windowStartedAt = 0;
1460
- var windowCount = 0;
1461
- var windowDroppedCount = 0;
1462
- function admitUnderRateLimit(now) {
1463
- if (now - windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1464
- if (windowDroppedCount > 0) {
1424
+ var rateWindows = /* @__PURE__ */ new Map();
1425
+ function admitUnderRateLimit(source, now) {
1426
+ let window = rateWindows.get(source);
1427
+ if (!window) {
1428
+ window = { windowStartedAt: 0, windowCount: 0, windowDroppedCount: 0 };
1429
+ rateWindows.set(source, window);
1430
+ }
1431
+ if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1432
+ if (window.windowDroppedCount > 0) {
1465
1433
  console.error(
1466
- `[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)`
1434
+ `[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}"`
1467
1435
  );
1468
1436
  }
1469
- windowStartedAt = now;
1470
- windowCount = 0;
1471
- windowDroppedCount = 0;
1437
+ window.windowStartedAt = now;
1438
+ window.windowCount = 0;
1439
+ window.windowDroppedCount = 0;
1472
1440
  }
1473
- if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
1474
- windowDroppedCount++;
1475
- if (windowDroppedCount === 1) {
1441
+ if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1442
+ window.windowDroppedCount++;
1443
+ if (window.windowDroppedCount === 1) {
1476
1444
  console.error(
1477
- `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
1445
+ `[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}"`
1478
1446
  );
1479
1447
  }
1480
1448
  return false;
1481
1449
  }
1482
- windowCount++;
1450
+ window.windowCount++;
1483
1451
  return true;
1484
1452
  }
1485
1453
  function forwardRunnerActivity(entry, context) {
1486
1454
  try {
1487
1455
  if (!FORWARDED_LEVELS.has(entry.level)) return;
1488
1456
  if (!context.agentId || !context.authHeader) return;
1489
- if (!admitUnderRateLimit(Date.now())) return;
1457
+ const source = entry.source ?? "cli.run";
1458
+ if (!admitUnderRateLimit(source, Date.now())) return;
1490
1459
  const rawMessage = entry.error ?? entry.message ?? "";
1491
1460
  const message = truncate(redact(rawMessage));
1492
1461
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1493
1462
  severity: SEVERITY_BY_LEVEL[entry.level],
1494
1463
  message,
1495
- metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
1464
+ metadata: { ...sanitiseMetadata(entry.metadata), source },
1496
1465
  agentId: context.agentId
1497
1466
  });
1498
1467
  } catch (err) {
@@ -1503,8 +1472,8 @@ function forwardRunnerActivity(entry, context) {
1503
1472
  }
1504
1473
 
1505
1474
  // src/lib/opencode/session-db-recovery-report.ts
1506
- import { readFileSync as readFileSync2, unlinkSync } from "fs";
1507
- import { join as join2 } from "path";
1475
+ import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
1476
+ import { join as join2 } from "node:path";
1508
1477
  function sessionDbRecoveryReportPath(homeDir, env) {
1509
1478
  const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1510
1479
  return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
@@ -1717,13 +1686,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1717
1686
  }
1718
1687
 
1719
1688
  // src/lib/opencode/session-db-boot.ts
1720
- import { spawn as spawn2 } from "child_process";
1721
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1722
- import { homedir as homedir2 } from "os";
1723
- import { dirname as dirname2, resolve as resolvePath } from "path";
1689
+ import { spawn as spawn2 } from "node:child_process";
1690
+ import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
1691
+ import { homedir as homedir2 } from "node:os";
1692
+ import { dirname as dirname2, resolve as resolvePath } from "node:path";
1724
1693
 
1725
1694
  // src/lib/runner-synchroniser.ts
1726
- import { spawn } from "child_process";
1695
+ import { spawn } from "node:child_process";
1727
1696
  function appendError(stderr, error2) {
1728
1697
  const message = error2 instanceof Error ? error2.message : String(error2);
1729
1698
  return stderr === "" ? message : `${stderr}
@@ -2248,9 +2217,9 @@ async function restoreAndVerifySessionDb(options) {
2248
2217
  }
2249
2218
 
2250
2219
  // src/lib/opencode/session-db-provenance.ts
2251
- import { createRequire } from "module";
2252
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2253
- import { dirname as dirname3, join as join3 } from "path";
2220
+ import { createRequire } from "node:module";
2221
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2222
+ import { dirname as dirname3, join as join3 } from "node:path";
2254
2223
  var require2 = createRequire(import.meta.url);
2255
2224
  function readSessionDbMigrationIds(dbPath) {
2256
2225
  let db;
@@ -2444,6 +2413,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2444
2413
 
2445
2414
  // src/lib/opencode/process.ts
2446
2415
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2416
+ var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
2417
+ function resolveOpenCodeLogLevel(env) {
2418
+ const raw = env.OPENCODE_LOG_LEVEL;
2419
+ if (!raw) return "INFO";
2420
+ const upper = raw.toUpperCase();
2421
+ if (VALID_OPENCODE_LOG_LEVELS.has(upper)) return upper;
2422
+ console.warn(
2423
+ `startOpenCode: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected DEBUG|INFO|WARN|ERROR) \u2014 using INFO`
2424
+ );
2425
+ return "INFO";
2426
+ }
2447
2427
  function getProcessCwd(pid) {
2448
2428
  const platform = process.platform;
2449
2429
  try {
@@ -2492,14 +2472,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
2492
2472
  }
2493
2473
  return null;
2494
2474
  }
2495
- function findOpenCodeProcesses() {
2475
+ function findProcessesByPattern(pgrepPattern, psPattern) {
2496
2476
  const instances = [];
2497
2477
  try {
2498
2478
  const platform = process.platform;
2499
2479
  if (platform === "darwin" || platform === "linux") {
2500
2480
  let pids = [];
2501
2481
  try {
2502
- const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
2482
+ const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
2503
2483
  encoding: "utf-8",
2504
2484
  stdio: ["pipe", "pipe", "pipe"]
2505
2485
  }).trim();
@@ -2508,7 +2488,7 @@ function findOpenCodeProcesses() {
2508
2488
  }
2509
2489
  } catch {
2510
2490
  try {
2511
- const psOutput = execSync('ps aux | grep -E "opencode (serve|--port)" | grep -v grep', {
2491
+ const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
2512
2492
  encoding: "utf-8",
2513
2493
  stdio: ["pipe", "pipe", "pipe"]
2514
2494
  }).trim();
@@ -2554,6 +2534,9 @@ function findOpenCodeProcesses() {
2554
2534
  }
2555
2535
  return instances;
2556
2536
  }
2537
+ function findOpenCodeProcesses() {
2538
+ return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
2539
+ }
2557
2540
  async function scanPortsForOpenCode() {
2558
2541
  const instances = [];
2559
2542
  const checks = OPENCODE_PORT_RANGE.map(async (port) => {
@@ -2600,7 +2583,7 @@ async function findHealthyOpenCodeInstances() {
2600
2583
  }
2601
2584
  async function startOpenCode(port, options = {}) {
2602
2585
  let command = "opencode";
2603
- const printLogs = options.inheritStdio ? ["--print-logs"] : [];
2586
+ const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
2604
2587
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
2605
2588
  try {
2606
2589
  execSync("which opencode", { stdio: "ignore" });
@@ -2657,6 +2640,19 @@ function isOpenCodeInstalled() {
2657
2640
  return false;
2658
2641
  }
2659
2642
  }
2643
+ function isOpenCode2Installed() {
2644
+ try {
2645
+ const platform = process.platform;
2646
+ if (platform === "win32") {
2647
+ execSync2("where opencode2", { stdio: "ignore" });
2648
+ } else {
2649
+ execSync2("which opencode2", { stdio: "ignore" });
2650
+ }
2651
+ return true;
2652
+ } catch {
2653
+ return false;
2654
+ }
2655
+ }
2660
2656
  async function promptOpenCodeInstall(interactive) {
2661
2657
  if (!interactive) {
2662
2658
  console.log(
@@ -2666,7 +2662,11 @@ async function promptOpenCodeInstall(interactive) {
2666
2662
  install_url: OPENCODE_INSTALL_URL,
2667
2663
  install_commands: {
2668
2664
  npm: "npm install -g opencode-ai",
2669
- curl: "curl -fsSL https://opencode.ai/install.sh | sh"
2665
+ curl: "curl -fsSL https://opencode.ai/install.sh | sh",
2666
+ v2: {
2667
+ npm: "npm install -g @opencode-ai/cli@beta",
2668
+ curl: "curl -fsSL https://opencode.ai/v2/install | bash"
2669
+ }
2670
2670
  }
2671
2671
  })
2672
2672
  );
@@ -3150,21 +3150,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
3150
3150
  }
3151
3151
  return lastOk ?? last;
3152
3152
  }
3153
- function messageUsage(messages, userMessageId) {
3154
- if (!messages || messages.length === 0) return null;
3155
- const byParentAll = messages.filter(
3156
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3153
+ function collectSubagentSessions(messages, userMessageId) {
3154
+ if (!messages || messages.length === 0) return [];
3155
+ const byParent = messages.filter(
3156
+ (message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
3157
3157
  );
3158
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3159
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3160
- let correlated;
3161
- if (byParent.length > 0) {
3162
- correlated = byParent;
3163
- } else {
3164
- const reply = findAssistantReplyAfter(messages, userMessageId);
3165
- correlated = reply ? [reply] : [];
3158
+ const assistants = byParent.length > 0 ? byParent : [];
3159
+ if (assistants.length === 0) {
3160
+ const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3161
+ if (userIndex === -1) return [];
3162
+ for (let i = userIndex + 1; i < messages.length; i++) {
3163
+ const message = messages[i];
3164
+ if (roleOf(message) === "user") break;
3165
+ if (roleOf(message) === "assistant") assistants.push(message);
3166
+ }
3167
+ }
3168
+ const refs = [];
3169
+ const seen = /* @__PURE__ */ new Set();
3170
+ for (const message of assistants) {
3171
+ const parts = Array.isArray(message.parts) ? message.parts : [];
3172
+ for (const part of parts) {
3173
+ if (!part || typeof part !== "object" || part.type !== "tool" || part.tool !== "task")
3174
+ continue;
3175
+ const state = part.state;
3176
+ if (!state || typeof state !== "object") continue;
3177
+ const metadata = state.metadata;
3178
+ if (!metadata || typeof metadata !== "object") continue;
3179
+ const sessionId = metadata.sessionId;
3180
+ if (typeof sessionId !== "string" || sessionId.length === 0 || seen.has(sessionId)) continue;
3181
+ seen.add(sessionId);
3182
+ const start = state.time?.start;
3183
+ refs.push({
3184
+ sessionId,
3185
+ startedAtMs: typeof start === "number" && Number.isFinite(start) ? start : null
3186
+ });
3187
+ }
3166
3188
  }
3167
- if (correlated.length === 0) return null;
3189
+ return refs;
3190
+ }
3191
+ function finiteNumber(value) {
3192
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3193
+ }
3194
+ function taskCallModel(value) {
3195
+ if (!value || typeof value !== "object") return null;
3196
+ const model = value;
3197
+ const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
3198
+ const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
3199
+ return modelID || providerID ? { modelID, providerID } : null;
3200
+ }
3201
+ function collectTaskCalls(messages, userMessageId) {
3202
+ if (!messages || messages.length === 0) return [];
3203
+ const calls = [];
3204
+ for (const message of messages) {
3205
+ if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
3206
+ for (const part of message.parts ?? []) {
3207
+ if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
3208
+ continue;
3209
+ }
3210
+ const rawName = part.state.input?.subagent_type;
3211
+ const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
3212
+ const metadata = part.state.metadata;
3213
+ calls.push({
3214
+ callID: part.callID,
3215
+ subagentName,
3216
+ childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
3217
+ parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3218
+ model: taskCallModel(metadata?.model),
3219
+ status: part.state.status ?? "unknown",
3220
+ timeStart: finiteNumber(part.state.time?.start),
3221
+ timeEnd: finiteNumber(part.state.time?.end)
3222
+ });
3223
+ }
3224
+ }
3225
+ return calls;
3226
+ }
3227
+ function attributeTaskCallUsage(messages, windows) {
3228
+ const eligibleWindows = windows.filter(
3229
+ (window) => window.timeStart !== null && Number.isFinite(window.timeStart)
3230
+ );
3231
+ const assignments = /* @__PURE__ */ new Map();
3232
+ for (const window of eligibleWindows) assignments.set(window.callID, []);
3233
+ const unattributed = [];
3234
+ for (const message of messages ?? []) {
3235
+ if (roleOf(message) !== "assistant") continue;
3236
+ const created = finiteNumber(createdOf(message));
3237
+ const matching = created === null ? [] : eligibleWindows.filter(
3238
+ (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3239
+ );
3240
+ if (matching.length === 0) {
3241
+ unattributed.push(message);
3242
+ continue;
3243
+ }
3244
+ matching.sort((a, b) => a.timeStart - b.timeStart);
3245
+ assignments.get(matching[0].callID)?.push(message);
3246
+ }
3247
+ return {
3248
+ invocations: eligibleWindows.map((window) => {
3249
+ const assigned = assignments.get(window.callID) ?? [];
3250
+ return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
3251
+ }),
3252
+ unattributed
3253
+ };
3254
+ }
3255
+ function sumAssistantUsage(messages) {
3256
+ if (!messages || messages.length === 0) return null;
3257
+ const nonErrored = messages.filter((message) => errorOf(message) == null);
3258
+ const selected = nonErrored.length > 0 ? nonErrored : messages;
3168
3259
  let sawAnyUsage = false;
3169
3260
  let inputSum = 0;
3170
3261
  let outputSum = 0;
@@ -3175,7 +3266,7 @@ function messageUsage(messages, userMessageId) {
3175
3266
  let sawCost = false;
3176
3267
  let modelId = null;
3177
3268
  let providerId = null;
3178
- for (const m of correlated) {
3269
+ for (const m of selected) {
3179
3270
  const info = m.info;
3180
3271
  if (!info) continue;
3181
3272
  const tokens = info.tokens;
@@ -3210,12 +3301,28 @@ function messageUsage(messages, userMessageId) {
3210
3301
  usage_tokens_reasoning: reasoningSum,
3211
3302
  usage_tokens_cache_read: cacheReadSum,
3212
3303
  usage_tokens_cache_write: cacheWriteSum,
3213
- // NULL means "OpenCode never reported a cost" (never inferred from
3214
- // tokens) distinct from a genuine 0-cost turn, which would set
3215
- // `sawCost` true with `costSum === 0`.
3304
+ // NULL means OpenCode never reported a cost; it is distinct from a genuine
3305
+ // zero-cost message, which sets `sawCost` with `costSum === 0`.
3216
3306
  usage_cost_usd: sawCost ? costSum : null
3217
3307
  };
3218
3308
  }
3309
+ function messageUsage(messages, userMessageId) {
3310
+ if (!messages || messages.length === 0) return null;
3311
+ const byParentAll = messages.filter(
3312
+ (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3313
+ );
3314
+ const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3315
+ const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3316
+ let correlated;
3317
+ if (byParent.length > 0) {
3318
+ correlated = byParent;
3319
+ } else {
3320
+ const reply = findAssistantReplyAfter(messages, userMessageId);
3321
+ correlated = reply ? [reply] : [];
3322
+ }
3323
+ if (correlated.length === 0) return null;
3324
+ return sumAssistantUsage(correlated);
3325
+ }
3219
3326
  function messageRunState(messages, userMessageId) {
3220
3327
  if (!messages || messages.length === 0) return "unknown";
3221
3328
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3278,8 +3385,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
3278
3385
  }
3279
3386
  return false;
3280
3387
  }
3281
- function messageFailure(messages, userMessageId) {
3282
- const reply = findLastAssistantReplyFor(messages, userMessageId);
3388
+ function classifyReplyAuthError(reply) {
3283
3389
  const error2 = errorOf(reply);
3284
3390
  if (error2 == null || typeof error2 !== "object") return null;
3285
3391
  const e = error2;
@@ -3304,6 +3410,32 @@ function messageFailure(messages, userMessageId) {
3304
3410
  }
3305
3411
  return null;
3306
3412
  }
3413
+ function messageFailure(messages, userMessageId) {
3414
+ return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
3415
+ }
3416
+ function findLatestSubagentAuthOutcome(messages, sinceMs) {
3417
+ if (!messages || messages.length === 0) return null;
3418
+ for (let i = messages.length - 1; i >= 0; i--) {
3419
+ const message = messages[i];
3420
+ if (roleOf(message) !== "assistant") continue;
3421
+ const created = createdOf(message);
3422
+ if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
3423
+ const failure = classifyReplyAuthError(message);
3424
+ if (failure) {
3425
+ if (!failure.providerId) return null;
3426
+ return { providerId: failure.providerId, outcome: "failed", failure };
3427
+ }
3428
+ const providerId = message.info?.providerID;
3429
+ if (errorOf(message) == null && typeof providerId === "string" && providerId.length > 0) {
3430
+ return { providerId, outcome: "succeeded" };
3431
+ }
3432
+ return null;
3433
+ }
3434
+ return null;
3435
+ }
3436
+ function findSubagentAuthOutcome(messages, sinceMs) {
3437
+ return findLatestSubagentAuthOutcome(messages, sinceMs);
3438
+ }
3307
3439
  function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
3308
3440
  if (classified != null) return classified;
3309
3441
  if (hasConfiguredProvider !== false) return null;
@@ -3320,6 +3452,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3320
3452
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3321
3453
  );
3322
3454
  }
3455
+ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
3456
+ if (!messages || messages.length === 0) return false;
3457
+ const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3458
+ if (userIndex === -1) return false;
3459
+ let hasLaterUser = false;
3460
+ let hasStartedLaterUser = false;
3461
+ for (let i = userIndex + 1; i < messages.length; i++) {
3462
+ const message = messages[i];
3463
+ if (roleOf(message) !== "user") continue;
3464
+ hasLaterUser = true;
3465
+ const laterUserMessageId = idOf(message);
3466
+ if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
3467
+ return false;
3468
+ }
3469
+ if (messages.some(
3470
+ (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3471
+ )) {
3472
+ hasStartedLaterUser = true;
3473
+ }
3474
+ }
3475
+ return hasLaterUser && hasStartedLaterUser;
3476
+ }
3323
3477
  async function hasAnyConfiguredProvider(port) {
3324
3478
  try {
3325
3479
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
@@ -3351,6 +3505,94 @@ async function hasAnyConfiguredProvider(port) {
3351
3505
  return null;
3352
3506
  }
3353
3507
  }
3508
+ function sessionErrorReason(error2) {
3509
+ const record = typeof error2 === "object" && error2 !== null ? error2 : null;
3510
+ const data = record?.data;
3511
+ const dataRecord = typeof data === "object" && data !== null ? data : null;
3512
+ 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";
3513
+ const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
3514
+ return reason || "OpenCode reported a session error with no details";
3515
+ }
3516
+ function parseSessionErrorFrame(data) {
3517
+ let parsed;
3518
+ try {
3519
+ parsed = JSON.parse(data);
3520
+ } catch (error2) {
3521
+ void error2;
3522
+ return null;
3523
+ }
3524
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3525
+ const parsedRecord = parsed;
3526
+ const payload = parsedRecord.payload;
3527
+ const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
3528
+ if (event.type !== "session.error") return null;
3529
+ const properties = event.properties;
3530
+ if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
3531
+ return null;
3532
+ }
3533
+ const propertiesRecord = properties;
3534
+ const sessionId = propertiesRecord.sessionID;
3535
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
3536
+ return {
3537
+ sessionId,
3538
+ reason: sessionErrorReason(propertiesRecord.error)
3539
+ };
3540
+ }
3541
+ async function readSessionErrorStream(port, options) {
3542
+ let reader = null;
3543
+ try {
3544
+ const response = await fetch(`${opencodeBase(port)}/event`, {
3545
+ headers: { accept: "text/event-stream" },
3546
+ signal: options.signal
3547
+ });
3548
+ if (!response.ok || !response.body) {
3549
+ return { reason: "unavailable", detail: `HTTP ${response.status}` };
3550
+ }
3551
+ reader = response.body.getReader();
3552
+ const decoder = new TextDecoder();
3553
+ let buffer = "";
3554
+ const processLine = (line) => {
3555
+ const trimmed = line.trimEnd();
3556
+ if (!trimmed.startsWith("data:")) return;
3557
+ const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
3558
+ if (event) options.onSessionError(event);
3559
+ };
3560
+ while (true) {
3561
+ const { done, value } = await reader.read();
3562
+ if (done) return { reason: "ended" };
3563
+ buffer += decoder.decode(value, { stream: true });
3564
+ const lines = buffer.split("\n");
3565
+ buffer = lines.pop() ?? "";
3566
+ for (const line of lines) processLine(line);
3567
+ }
3568
+ } catch (err) {
3569
+ if (options.signal.aborted) return { reason: "aborted" };
3570
+ return {
3571
+ reason: "unavailable",
3572
+ detail: err instanceof Error ? err.message : String(err)
3573
+ };
3574
+ } finally {
3575
+ if (reader) void reader.cancel().catch(() => void 0);
3576
+ }
3577
+ }
3578
+ async function reloadProviderCache(port) {
3579
+ try {
3580
+ const res = await timedFetch(`${opencodeBase(port)}/config`, {
3581
+ method: "PATCH",
3582
+ headers: { "Content-Type": "application/json" },
3583
+ body: JSON.stringify({})
3584
+ });
3585
+ if (!res.ok) {
3586
+ console.error(
3587
+ `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
3588
+ );
3589
+ }
3590
+ } catch (err) {
3591
+ console.error(
3592
+ `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3593
+ );
3594
+ }
3595
+ }
3354
3596
 
3355
3597
  // src/lib/opencode/session-cleanup.ts
3356
3598
  var DURATION_UNIT_MS = {
@@ -3457,8 +3699,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
3457
3699
  }
3458
3700
 
3459
3701
  // src/lib/opencode/session-db-size.ts
3460
- import { statSync as statSync3 } from "fs";
3461
- import { join as join4 } from "path";
3702
+ import { statSync as statSync3 } from "node:fs";
3703
+ import { join as join4 } from "node:path";
3462
3704
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
3463
3705
  function statSessionDbBytes(homeDir) {
3464
3706
  const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
@@ -3488,9 +3730,96 @@ function buildSessionStoreSizeWarning(input) {
3488
3730
  return null;
3489
3731
  }
3490
3732
 
3733
+ // src/lib/opencode/log-tail.ts
3734
+ import { statSync as statSync4 } from "node:fs";
3735
+ import { homedir as homedir3 } from "node:os";
3736
+ import { join as join5 } from "node:path";
3737
+ import { open as open2, stat } from "node:fs/promises";
3738
+ var DEFAULT_POLL_INTERVAL_MS = 1e3;
3739
+ function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
3740
+ const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
3741
+ return join5(dataDir, "opencode", "log", "opencode.log");
3742
+ }
3743
+ function isEnoent(error2) {
3744
+ return error2?.code === "ENOENT";
3745
+ }
3746
+ function reportFailure(operation, logPath, error2) {
3747
+ console.error(
3748
+ `[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
3749
+ );
3750
+ }
3751
+ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
3752
+ const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
3753
+ let offset = 0;
3754
+ let inode = null;
3755
+ let baselineReady = true;
3756
+ try {
3757
+ const initial = statSync4(logPath);
3758
+ offset = initial.size;
3759
+ inode = initial.ino;
3760
+ } catch (error2) {
3761
+ if (!isEnoent(error2)) {
3762
+ reportFailure("initial stat", logPath, error2);
3763
+ baselineReady = false;
3764
+ }
3765
+ }
3766
+ let polling = false;
3767
+ let stopped = false;
3768
+ const poll = async () => {
3769
+ if (polling || stopped) return;
3770
+ polling = true;
3771
+ try {
3772
+ let current;
3773
+ try {
3774
+ current = await stat(logPath);
3775
+ } catch (error2) {
3776
+ if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
3777
+ return;
3778
+ }
3779
+ if (!baselineReady) {
3780
+ offset = current.size;
3781
+ inode = current.ino;
3782
+ baselineReady = true;
3783
+ return;
3784
+ }
3785
+ if (inode !== null && current.ino !== inode || current.size < offset) {
3786
+ offset = 0;
3787
+ }
3788
+ inode = current.ino;
3789
+ if (current.size === offset) return;
3790
+ const length = current.size - offset;
3791
+ const fh = await open2(logPath, "r");
3792
+ try {
3793
+ const buf = Buffer.alloc(length);
3794
+ const { bytesRead } = await fh.read(buf, 0, length, offset);
3795
+ offset += bytesRead;
3796
+ if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
3797
+ } finally {
3798
+ await fh.close();
3799
+ }
3800
+ } catch (error2) {
3801
+ if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
3802
+ } finally {
3803
+ polling = false;
3804
+ }
3805
+ };
3806
+ const interval = setInterval(() => void poll(), pollIntervalMs);
3807
+ void poll();
3808
+ return {
3809
+ stop: () => {
3810
+ stopped = true;
3811
+ clearInterval(interval);
3812
+ }
3813
+ };
3814
+ }
3815
+
3491
3816
  // src/lib/opencode/session-db-reclaim.ts
3492
- import { statSync as statSync4, statfsSync } from "fs";
3493
- import { dirname as dirname4 } from "path";
3817
+ import { statSync as statSync5, statfsSync } from "node:fs";
3818
+ import { dirname as dirname4 } from "node:path";
3819
+ function errorMessage(error2) {
3820
+ if (!(error2 instanceof Error)) return String(error2);
3821
+ return error2.cause instanceof Error ? error2.cause.message : error2.message;
3822
+ }
3494
3823
  function insufficientSpaceReason(dbPath, requiredBytes) {
3495
3824
  try {
3496
3825
  const fsStats = statfsSync(dirname4(dbPath));
@@ -3516,17 +3845,17 @@ async function probeReclaimAvailability(input) {
3516
3845
  const { dbPath, requiredBytes } = input;
3517
3846
  let sqlite;
3518
3847
  try {
3519
- sqlite = await import("sqlite");
3848
+ sqlite = await import("node:sqlite");
3520
3849
  } catch (err) {
3521
- console.warn(
3522
- `[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3523
- );
3524
- return "sqlite-unavailable";
3850
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
3851
+ console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
3852
+ return { reason: "sqlite-unavailable", detail };
3525
3853
  }
3526
3854
  let autoVacuum = null;
3527
3855
  try {
3528
3856
  const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
3529
3857
  try {
3858
+ db.exec("PRAGMA busy_timeout=5000");
3530
3859
  autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3531
3860
  } finally {
3532
3861
  db.close();
@@ -3537,23 +3866,25 @@ async function probeReclaimAvailability(input) {
3537
3866
  );
3538
3867
  }
3539
3868
  if (autoVacuum !== 0) return null;
3540
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
3869
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
3541
3870
  }
3542
3871
  async function reclaimSessionDbSpace(input) {
3543
3872
  const { dbPath, maxPages, allowFullVacuum = true } = input;
3544
3873
  let sqlite;
3545
3874
  try {
3546
- sqlite = await import("sqlite");
3875
+ sqlite = await import("node:sqlite");
3547
3876
  } catch (err) {
3877
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
3548
3878
  console.warn(
3549
- `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3879
+ `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
3550
3880
  );
3551
- return { ok: false, skipped: "sqlite-unavailable" };
3881
+ return { ok: false, skipped: "sqlite-unavailable", detail };
3552
3882
  }
3553
3883
  const { DatabaseSync } = sqlite;
3554
3884
  let db;
3555
3885
  try {
3556
3886
  db = new DatabaseSync(dbPath);
3887
+ db.exec("PRAGMA busy_timeout=5000");
3557
3888
  const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3558
3889
  if (autoVacuum === 0) {
3559
3890
  if (!allowFullVacuum) {
@@ -3562,7 +3893,7 @@ async function reclaimSessionDbSpace(input) {
3562
3893
  );
3563
3894
  return { ok: false, skipped: "full-vacuum-blocked" };
3564
3895
  }
3565
- const fileBytesForGuard = statSync4(dbPath).size;
3896
+ const fileBytesForGuard = statSync5(dbPath).size;
3566
3897
  const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
3567
3898
  if (skipReason !== null) {
3568
3899
  console.warn(
@@ -3590,10 +3921,12 @@ async function reclaimSessionDbSpace(input) {
3590
3921
  );
3591
3922
  return { ok: false, skipped: "auto-vacuum-not-applicable" };
3592
3923
  } catch (err) {
3593
- console.error(
3594
- `[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3595
- );
3596
- return { ok: false, skipped: "reclaim-error" };
3924
+ console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
3925
+ return {
3926
+ ok: false,
3927
+ skipped: "reclaim-error",
3928
+ detail: errorMessage(err)
3929
+ };
3597
3930
  } finally {
3598
3931
  db?.close();
3599
3932
  }
@@ -3634,7 +3967,6 @@ var StreamForwarder = class {
3634
3967
  handleFrame(frame) {
3635
3968
  switch (frame.type) {
3636
3969
  case "open":
3637
- this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
3638
3970
  void this.handleOpen(frame);
3639
3971
  break;
3640
3972
  case "req_data":
@@ -3670,12 +4002,21 @@ var StreamForwarder = class {
3670
4002
  const { sid, method, path, headers, has_body } = frame;
3671
4003
  const correlationId = headers?.[CORRELATION_ID_HEADER];
3672
4004
  const startedAt = Date.now();
4005
+ if (path !== TUNNEL_DRAIN_PING_PATH && path !== TUNNEL_USAGE_REARM_PING_PATH) {
4006
+ this.callbacks.onOpen?.(sid, method, path);
4007
+ }
3673
4008
  if (path === TUNNEL_DRAIN_PING_PATH) {
3674
4009
  this.callbacks.onDrainPing?.();
3675
4010
  this.send({ type: "head", sid, status: 204, headers: {} });
3676
4011
  this.send({ type: "res_end", sid });
3677
4012
  return;
3678
4013
  }
4014
+ if (path === TUNNEL_USAGE_REARM_PING_PATH) {
4015
+ this.callbacks.onUsageRearmPing?.();
4016
+ this.send({ type: "head", sid, status: 204, headers: {} });
4017
+ this.send({ type: "res_end", sid });
4018
+ return;
4019
+ }
3679
4020
  if (process.env.DEBUG) {
3680
4021
  log("debug", "agent_request", {
3681
4022
  correlation_id: correlationId,
@@ -3820,7 +4161,8 @@ function connectTunnel(options) {
3820
4161
  onResponse,
3821
4162
  onInfo,
3822
4163
  onWarning,
3823
- onDrainPing
4164
+ onDrainPing,
4165
+ onUsageRearmPing
3824
4166
  } = options;
3825
4167
  const tunnelUrl = getTunnelUrlConfig();
3826
4168
  const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
@@ -3832,7 +4174,8 @@ function connectTunnel(options) {
3832
4174
  });
3833
4175
  const forwarder = new StreamForwarder(ws, port, {
3834
4176
  onHead: () => onResponse?.(),
3835
- onDrainPing: () => onDrainPing?.()
4177
+ onDrainPing: () => onDrainPing?.(),
4178
+ onUsageRearmPing: () => onUsageRearmPing?.()
3836
4179
  });
3837
4180
  const connectionTimeout = setTimeout(() => {
3838
4181
  ws.close();
@@ -3875,8 +4218,8 @@ function connectTunnel(options) {
3875
4218
  try {
3876
4219
  message = JSON.parse(data.toString());
3877
4220
  } catch (error2) {
3878
- const errorMessage2 = error2 instanceof Error ? error2.message : "Unknown error";
3879
- onError?.(`Failed to handle message: ${errorMessage2}`);
4221
+ const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
4222
+ onError?.(`Failed to handle message: ${errorMessage3}`);
3880
4223
  return;
3881
4224
  }
3882
4225
  if (isStreamFrame(message)) {
@@ -3993,6 +4336,7 @@ var RunnerConnection = class {
3993
4336
  onError: (error2) => events.onError?.(error2),
3994
4337
  onResponse: () => events.onResponse?.(),
3995
4338
  onDrainPing: () => events.onDrainPing?.(),
4339
+ onUsageRearmPing: () => events.onUsageRearmPing?.(),
3996
4340
  onInfo: (message) => events.onInfo?.(message),
3997
4341
  onWarning: (message) => events.onWarning?.(message)
3998
4342
  });
@@ -4019,7 +4363,7 @@ var RunnerConnection = class {
4019
4363
  };
4020
4364
 
4021
4365
  // src/lib/tunnel/ready-marker.ts
4022
- import { writeFileSync as writeFileSync3 } from "fs";
4366
+ import { writeFileSync as writeFileSync3 } from "node:fs";
4023
4367
  function writeTunnelReadyMarker(path, agentId) {
4024
4368
  try {
4025
4369
  writeFileSync3(path, `${agentId}
@@ -4031,7 +4375,7 @@ function writeTunnelReadyMarker(path, agentId) {
4031
4375
  }
4032
4376
 
4033
4377
  // src/lib/replication.ts
4034
- import { spawn as spawn4 } from "child_process";
4378
+ import { spawn as spawn4 } from "node:child_process";
4035
4379
  function startSessionDbReplication(configPath) {
4036
4380
  return spawn4("litestream", ["replicate", "-config", configPath], {
4037
4381
  stdio: "inherit"
@@ -4047,7 +4391,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
4047
4391
  }
4048
4392
 
4049
4393
  // src/lib/process-liveness.ts
4050
- import { readFileSync as readFileSync4 } from "fs";
4394
+ import { readFileSync as readFileSync4 } from "node:fs";
4051
4395
  function isProcessAlive(pid) {
4052
4396
  try {
4053
4397
  process.kill(pid, 0);
@@ -4073,9 +4417,9 @@ function isProcessAlive(pid) {
4073
4417
  }
4074
4418
 
4075
4419
  // src/lib/openai-usage.ts
4076
- import { readFileSync as readFileSync5 } from "fs";
4077
- import { homedir as homedir3 } from "os";
4078
- import { join as join5 } from "path";
4420
+ import { readFileSync as readFileSync5 } from "node:fs";
4421
+ import { homedir as homedir4 } from "node:os";
4422
+ import { join as join6 } from "node:path";
4079
4423
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4080
4424
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4081
4425
  var OpenAiUsageError = class extends Error {
@@ -4089,7 +4433,7 @@ function isLocalCredentialProblem2(err) {
4089
4433
  }
4090
4434
  function readOpenCodeChatGptCredentials() {
4091
4435
  try {
4092
- const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4436
+ const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4093
4437
  let parsed;
4094
4438
  try {
4095
4439
  parsed = JSON.parse(raw);
@@ -4126,7 +4470,7 @@ function parseChatGptIdentity(accessToken) {
4126
4470
  const auth = payload["https://api.openai.com/auth"];
4127
4471
  const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4128
4472
  const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4129
- return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
4473
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
4130
4474
  }
4131
4475
  function toWindow2(headers, name) {
4132
4476
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
@@ -4308,13 +4652,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
4308
4652
  envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
4309
4653
  });
4310
4654
  }
4311
- function nextReportDelayMs(random = Math.random) {
4312
- return usageReportDelayMs(random);
4313
- }
4314
- var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
4315
- function claudeUsageFailureLogLevel(consecutiveFailures) {
4316
- return usageReportFailureLogLevel(consecutiveFailures);
4317
- }
4318
4655
 
4319
4656
  // src/lib/openai-usage-reporting.ts
4320
4657
  function resolveOpenAiUsageReportingMode(flagValue, env) {
@@ -4351,8 +4688,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
4351
4688
  }
4352
4689
 
4353
4690
  // src/lib/resource-usage.ts
4354
- import { cpus, totalmem, freemem } from "os";
4355
- import { statfsSync as statfsSync2 } from "fs";
4691
+ import { cpus, totalmem, freemem } from "node:os";
4692
+ import { statfsSync as statfsSync2 } from "node:fs";
4356
4693
 
4357
4694
  // src/lib/ecs-task-metadata.ts
4358
4695
  var ECS_METADATA_TIMEOUT_MS = 2e3;
@@ -4437,58 +4774,97 @@ function readDisk(homeDir) {
4437
4774
  };
4438
4775
  }
4439
4776
  }
4440
- function createResourceUsageCollector(homeDir) {
4441
- let previous = readCpuSample();
4442
- return async () => {
4777
+ var CPU_PEAK_WINDOW_MS = 6e4;
4778
+ var CPU_PEAK_WINDOW_SAMPLE_COUNT = 4;
4779
+ var CPU_PEAK_SAMPLE_INTERVAL_MS = CPU_PEAK_WINDOW_MS / CPU_PEAK_WINDOW_SAMPLE_COUNT;
4780
+ function createCpuPeakSampler() {
4781
+ const sampleHistory = new Array(CPU_PEAK_WINDOW_SAMPLE_COUNT);
4782
+ sampleHistory[0] = readCpuSample();
4783
+ let nextSampleIndex = 1;
4784
+ let sampleCount = 1;
4785
+ let peak = null;
4786
+ const timer = setInterval(() => {
4443
4787
  const current = readCpuSample();
4444
- const hostCpuPercent = cpuPercentBetween(previous, current);
4445
- const hostCpuCount = cpus().length;
4446
- previous = current;
4447
- const disk = readDisk(homeDir);
4448
- const opencodeDbBytes = statSessionDbBytes(homeDir);
4449
- const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
4450
- const warnings = [];
4451
- if (disk.warning) warnings.push(disk.warning);
4452
- if (ecsWarning) warnings.push(ecsWarning);
4453
- let cpuPercent = hostCpuPercent;
4454
- let cpuCount = hostCpuCount;
4455
- let memoryTotalBytes = totalmem();
4456
- let memoryAvailableBytes = freemem();
4457
- if (limits !== null) {
4458
- cpuCount = limits.cpuCount;
4459
- memoryTotalBytes = limits.memoryTotalBytes;
4460
- memoryAvailableBytes = clamp(
4461
- limits.memoryTotalBytes - (totalmem() - freemem()),
4462
- 0,
4463
- limits.memoryTotalBytes
4464
- );
4465
- cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
4788
+ const sampleFromWindowAgo = sampleCount === CPU_PEAK_WINDOW_SAMPLE_COUNT ? sampleHistory[nextSampleIndex] : void 0;
4789
+ if (sampleFromWindowAgo !== void 0) {
4790
+ const percentage = cpuPercentBetween(sampleFromWindowAgo, current);
4791
+ if (percentage !== null) {
4792
+ peak = peak === null ? percentage : Math.max(peak, percentage);
4793
+ }
4466
4794
  }
4467
- return {
4468
- usage: {
4469
- cpuPercent,
4470
- cpuCount,
4471
- memoryTotalBytes,
4472
- memoryAvailableBytes,
4473
- diskTotalBytes: disk.totalBytes,
4474
- diskFreeBytes: disk.freeBytes,
4475
- opencodeDbBytes
4476
- },
4477
- warnings
4478
- };
4795
+ sampleHistory[nextSampleIndex] = current;
4796
+ nextSampleIndex = (nextSampleIndex + 1) % CPU_PEAK_WINDOW_SAMPLE_COUNT;
4797
+ sampleCount = Math.min(sampleCount + 1, CPU_PEAK_WINDOW_SAMPLE_COUNT);
4798
+ }, CPU_PEAK_SAMPLE_INTERVAL_MS);
4799
+ return {
4800
+ takeAndReset: () => {
4801
+ const currentPeak = peak;
4802
+ peak = null;
4803
+ return currentPeak;
4804
+ },
4805
+ stop: () => clearInterval(timer)
4806
+ };
4807
+ }
4808
+ function createResourceUsageCollector(homeDir) {
4809
+ let previous = readCpuSample();
4810
+ const cpuPeakSampler = createCpuPeakSampler();
4811
+ return {
4812
+ collect: async () => {
4813
+ const current = readCpuSample();
4814
+ const hostCpuPercent = cpuPercentBetween(previous, current);
4815
+ const hostCpuPeakPercent = cpuPeakSampler.takeAndReset();
4816
+ const hostCpuCount = cpus().length;
4817
+ previous = current;
4818
+ const disk = readDisk(homeDir);
4819
+ const opencodeDbBytes = statSessionDbBytes(homeDir);
4820
+ const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
4821
+ const warnings = [];
4822
+ if (disk.warning) warnings.push(disk.warning);
4823
+ if (ecsWarning) warnings.push(ecsWarning);
4824
+ let cpuPercent = hostCpuPercent;
4825
+ let cpuPeakPercent = hostCpuPeakPercent;
4826
+ let cpuCount = hostCpuCount;
4827
+ let memoryTotalBytes = totalmem();
4828
+ let memoryAvailableBytes = freemem();
4829
+ if (limits !== null) {
4830
+ cpuCount = limits.cpuCount;
4831
+ memoryTotalBytes = limits.memoryTotalBytes;
4832
+ memoryAvailableBytes = clamp(
4833
+ limits.memoryTotalBytes - (totalmem() - freemem()),
4834
+ 0,
4835
+ limits.memoryTotalBytes
4836
+ );
4837
+ cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
4838
+ cpuPeakPercent = hostCpuPeakPercent === null ? null : clamp(round2(hostCpuPeakPercent * hostCpuCount / limits.cpuCount), 0, 100);
4839
+ }
4840
+ return {
4841
+ usage: {
4842
+ cpuPercent,
4843
+ cpuPeakPercent,
4844
+ cpuCount,
4845
+ memoryTotalBytes,
4846
+ memoryAvailableBytes,
4847
+ diskTotalBytes: disk.totalBytes,
4848
+ diskFreeBytes: disk.freeBytes,
4849
+ opencodeDbBytes
4850
+ },
4851
+ warnings
4852
+ };
4853
+ },
4854
+ stop: cpuPeakSampler.stop
4479
4855
  };
4480
4856
  }
4481
4857
 
4482
4858
  // src/lib/channels/driver.ts
4483
- import { homedir as homedir4 } from "os";
4859
+ import { homedir as homedir5 } from "node:os";
4484
4860
 
4485
4861
  // src/lib/runner-file-sync.ts
4486
- import { join as join7 } from "path";
4862
+ import { join as join8 } from "node:path";
4487
4863
 
4488
4864
  // src/lib/file-push.ts
4489
- import { randomUUID } from "crypto";
4490
- import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
4491
- import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
4865
+ import { randomUUID } from "node:crypto";
4866
+ import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
4867
+ import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
4492
4868
  var FILE_MODE = 384;
4493
4869
  var DIRECTORY_MODE = 448;
4494
4870
  async function writePushedFile(request) {
@@ -4521,7 +4897,7 @@ async function writePushedFile(request) {
4521
4897
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
4522
4898
  dirname5(candidate)
4523
4899
  );
4524
- const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
4900
+ const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
4525
4901
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
4526
4902
  if (allowedDirectory === null) {
4527
4903
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -4557,7 +4933,7 @@ function expandAndValidate(requestedPath, homeDir) {
4557
4933
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
4558
4934
  return null;
4559
4935
  }
4560
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
4936
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4561
4937
  if (expanded.split(/[/\\]/).includes("..")) {
4562
4938
  return null;
4563
4939
  }
@@ -4630,16 +5006,16 @@ function contains(realDirectory, realTarget) {
4630
5006
  async function createMissingDirectories(existingAncestor, missingSegments) {
4631
5007
  let current = existingAncestor;
4632
5008
  for (const segment of missingSegments) {
4633
- current = join6(current, segment);
5009
+ current = join7(current, segment);
4634
5010
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
4635
5011
  await chmod(current, DIRECTORY_MODE);
4636
5012
  }
4637
5013
  }
4638
5014
  async function writeAtomically(realTarget, content) {
4639
- const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
5015
+ const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
4640
5016
  let handle;
4641
5017
  try {
4642
- handle = await open2(temporaryPath, "wx", FILE_MODE);
5018
+ handle = await open3(temporaryPath, "wx", FILE_MODE);
4643
5019
  await handle.writeFile(content);
4644
5020
  await handle.chmod(FILE_MODE);
4645
5021
  await handle.close();
@@ -4766,12 +5142,12 @@ var NOT_APPLIED = {
4766
5142
  opencodeAuthApplied: false
4767
5143
  };
4768
5144
  function isClaudeCredentialPath(requestedPath, homeDir) {
4769
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4770
- return expanded === join7(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
5145
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5146
+ return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4771
5147
  }
4772
5148
  function isOpenCodeAuthPath(requestedPath, homeDir) {
4773
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4774
- return expanded === join7(homeDir, ...OPENCODE_AUTH_SEGMENTS);
5149
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5150
+ return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
4775
5151
  }
4776
5152
  async function applyOne(options, file) {
4777
5153
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -4931,6 +5307,10 @@ var DEFAULT_RETRY_POLICY = {
4931
5307
  baseDelayMs: 500,
4932
5308
  maxDelayMs: 3e4
4933
5309
  };
5310
+ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
5311
+ var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
5312
+ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
5313
+ var MAX_BUFFERED_SESSION_ERRORS = 256;
4934
5314
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
4935
5315
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
4936
5316
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5071,6 +5451,17 @@ var ChannelDriver = class _ChannelDriver {
5071
5451
  * message; it is removed once its in-flight set empties.
5072
5452
  */
5073
5453
  watchers = /* @__PURE__ */ new Map();
5454
+ sessionErrorStream = null;
5455
+ /**
5456
+ * Session-error failures currently being reported; entries are empty at rest
5457
+ * because each handoff deletes its id in `finally`.
5458
+ */
5459
+ sessionErrorHandled = /* @__PURE__ */ new Set();
5460
+ /**
5461
+ * Session errors that arrived before their dispatch was registered. Bounded FIFO
5462
+ * with a short TTL so an unmatched session cannot retain an event indefinitely.
5463
+ */
5464
+ bufferedSessionErrors = /* @__PURE__ */ new Map();
5074
5465
  /**
5075
5466
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5076
5467
  * dispatched and are still in-flight. A message in this set is never
@@ -5254,6 +5645,13 @@ var ChannelDriver = class _ChannelDriver {
5254
5645
  * no watcher) can resolve the title.
5255
5646
  */
5256
5647
  sessionTitles = /* @__PURE__ */ new Map();
5648
+ /** One best-effort terminal subagent collection per Evident message id. */
5649
+ subagentInvocationCollections = /* @__PURE__ */ new Map();
5650
+ /**
5651
+ * Early snapshots are only liveness hints; they must not become the terminal
5652
+ * collection when the task parts or child transcript have advanced.
5653
+ */
5654
+ subagentInvocationPrefetches = /* @__PURE__ */ new Map();
5257
5655
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5258
5656
  draining = false;
5259
5657
  /**
@@ -5318,7 +5716,7 @@ var ChannelDriver = class _ChannelDriver {
5318
5716
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
5319
5717
  this.now = config.now ?? (() => Date.now());
5320
5718
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
5321
- this.homeDir = config.homeDir ?? homedir4();
5719
+ this.homeDir = config.homeDir ?? homedir5();
5322
5720
  this.maxActiveSessions = config.maxActiveSessions;
5323
5721
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5324
5722
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -5468,6 +5866,21 @@ var ChannelDriver = class _ChannelDriver {
5468
5866
  }
5469
5867
  return ids;
5470
5868
  }
5869
+ /**
5870
+ * OpenCode user-message ids tracked for other Evident messages in a session.
5871
+ * Excluding this message makes an unattributed later row fail safe; a missing
5872
+ * watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
5873
+ */
5874
+ siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
5875
+ const ids = /* @__PURE__ */ new Set();
5876
+ if (!watcher) return ids;
5877
+ for (const inFlight of watcher.inFlight.values()) {
5878
+ if (inFlight.evidentMessageId !== ownEvidentMessageId) {
5879
+ ids.add(inFlight.opencodeMessageId);
5880
+ }
5881
+ }
5882
+ return ids;
5883
+ }
5471
5884
  /**
5472
5885
  * File-pull work, for `run.ts`'s idle accounting (#559).
5473
5886
  *
@@ -5534,6 +5947,8 @@ var ChannelDriver = class _ChannelDriver {
5534
5947
  */
5535
5948
  stop() {
5536
5949
  this.stopped = true;
5950
+ this.sessionErrorStream?.abort.abort();
5951
+ this.sessionErrorStream = null;
5537
5952
  }
5538
5953
  /**
5539
5954
  * The server clears this request when a new MicroVM identity is recorded, so a
@@ -5608,6 +6023,7 @@ var ChannelDriver = class _ChannelDriver {
5608
6023
  */
5609
6024
  async processConversation(conv) {
5610
6025
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6026
+ this.ensureSessionErrorStream();
5611
6027
  const messages = await this.getPendingMessages(conv.id);
5612
6028
  let dispatched = 0;
5613
6029
  let skippedAlreadyDispatched = 0;
@@ -5680,7 +6096,7 @@ var ChannelDriver = class _ChannelDriver {
5680
6096
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
5681
6097
  break;
5682
6098
  }
5683
- const errorMessage2 = err instanceof Error ? err.message : String(err);
6099
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
5684
6100
  this.sessions.delete(conv.id);
5685
6101
  this.supersede(conv.id, sessionId);
5686
6102
  this.log({
@@ -5689,7 +6105,7 @@ var ChannelDriver = class _ChannelDriver {
5689
6105
  conversation_id: conv.id,
5690
6106
  message_id: message.id
5691
6107
  });
5692
- await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
6108
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5693
6109
  this.log({
5694
6110
  level: "warn",
5695
6111
  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)}`,
@@ -5700,7 +6116,7 @@ var ChannelDriver = class _ChannelDriver {
5700
6116
  });
5701
6117
  this.log({
5702
6118
  level: "error",
5703
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage2}`,
6119
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
5704
6120
  conversation_id: conv.id,
5705
6121
  message_id: message.id
5706
6122
  });
@@ -5721,14 +6137,14 @@ var ChannelDriver = class _ChannelDriver {
5721
6137
  this.unconfirmedDispatchFailures.delete(message.id);
5722
6138
  this.sessions.delete(conv.id);
5723
6139
  this.supersede(conv.id, sessionId);
5724
- 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.`;
6140
+ 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.`;
5725
6141
  this.log({
5726
6142
  level: "error",
5727
- message: errorMessage2,
6143
+ message: errorMessage3,
5728
6144
  conversation_id: conv.id,
5729
6145
  message_id: message.id
5730
6146
  });
5731
- await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
6147
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5732
6148
  this.log({
5733
6149
  level: "warn",
5734
6150
  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)}`,
@@ -5954,6 +6370,23 @@ var ChannelDriver = class _ChannelDriver {
5954
6370
  if (state === "running" || state === "queued") {
5955
6371
  const ongoing = await isSessionOngoing(this.port, sessionId);
5956
6372
  if (ongoing === true) {
6373
+ if (state === "queued") {
6374
+ const siblingOcIds = this.siblingOpencodeMessageIds(
6375
+ this.watchers.get(sessionId),
6376
+ message.id
6377
+ );
6378
+ if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
6379
+ this.log({
6380
+ level: "warn",
6381
+ 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`,
6382
+ conversation_id: conv.id,
6383
+ message_id: message.id
6384
+ });
6385
+ this.clearRedriveUnresolved(message.id);
6386
+ void this.postSignal(conv.id, message.id, "redrive_redispatched");
6387
+ return "dispatch";
6388
+ }
6389
+ }
5957
6390
  return this.reattachRedrive(conv, sessionId, message, ocId);
5958
6391
  }
5959
6392
  if (ongoing === false) {
@@ -6043,16 +6476,37 @@ var ChannelDriver = class _ChannelDriver {
6043
6476
  if (state === "done") {
6044
6477
  const title = await this.resolveSessionTitle(sessionId, conv.id);
6045
6478
  const usage = messageUsage(messages, ocId ?? "");
6479
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
6480
+ const subagentInvocations = await this.resolveSubagentInvocations(
6481
+ messages,
6482
+ ocId ?? "",
6483
+ message.id
6484
+ );
6046
6485
  this.log({
6047
6486
  level: "info",
6048
6487
  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`,
6049
6488
  conversation_id: conv.id,
6050
6489
  message_id: message.id
6051
6490
  });
6052
- await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);
6491
+ await this.markDone(
6492
+ conv.id,
6493
+ message.id,
6494
+ sessionId,
6495
+ ocId,
6496
+ title,
6497
+ usage,
6498
+ usageAgentName,
6499
+ subagentInvocations
6500
+ );
6053
6501
  } else {
6054
6502
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
6055
6503
  const usage = messageUsage(messages, ocId ?? "");
6504
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
6505
+ const subagentInvocations = await this.resolveSubagentInvocations(
6506
+ messages,
6507
+ ocId ?? "",
6508
+ message.id
6509
+ );
6056
6510
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
6057
6511
  this.log({
6058
6512
  level: "error",
@@ -6060,7 +6514,19 @@ var ChannelDriver = class _ChannelDriver {
6060
6514
  conversation_id: conv.id,
6061
6515
  message_id: message.id
6062
6516
  });
6063
- await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
6517
+ await this.markFailed(
6518
+ conv.id,
6519
+ message.id,
6520
+ sessionId,
6521
+ error2,
6522
+ usage,
6523
+ failure,
6524
+ usageAgentName,
6525
+ subagentInvocations
6526
+ );
6527
+ }
6528
+ if (ocId !== null) {
6529
+ await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
6064
6530
  }
6065
6531
  } catch (err) {
6066
6532
  if (err instanceof ChannelAuthError) throw err;
@@ -6601,6 +7067,24 @@ var ChannelDriver = class _ChannelDriver {
6601
7067
  ambiguousPinnedSinceMs: 0,
6602
7068
  ambiguousResolved: false
6603
7069
  });
7070
+ const buffered = this.bufferedSessionErrors.get(sessionId);
7071
+ if (!buffered) return;
7072
+ this.bufferedSessionErrors.delete(sessionId);
7073
+ if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
7074
+ this.handleSessionError(buffered.event);
7075
+ }
7076
+ }
7077
+ bufferSessionError(event) {
7078
+ this.bufferedSessionErrors.delete(event.sessionId);
7079
+ this.bufferedSessionErrors.set(event.sessionId, {
7080
+ event,
7081
+ receivedAt: this.now()
7082
+ });
7083
+ while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
7084
+ const oldest = this.bufferedSessionErrors.keys().next().value;
7085
+ if (typeof oldest !== "string") break;
7086
+ this.bufferedSessionErrors.delete(oldest);
7087
+ }
6604
7088
  }
6605
7089
  /**
6606
7090
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
@@ -6805,6 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
6805
7289
  ensureWatcherRunning(sessionId) {
6806
7290
  const watcher = this.watchers.get(sessionId);
6807
7291
  if (!watcher) return;
7292
+ this.ensureSessionErrorStream();
6808
7293
  if (watcher.loop) return;
6809
7294
  if (watcher.inFlight.size === 0) {
6810
7295
  this.watchers.delete(sessionId);
@@ -6820,6 +7305,154 @@ var ChannelDriver = class _ChannelDriver {
6820
7305
  });
6821
7306
  watcher.loop = loop;
6822
7307
  }
7308
+ ensureSessionErrorStream() {
7309
+ if (this.sessionErrorStream || this.stopped) return;
7310
+ const abort = new AbortController();
7311
+ const loop = this.runSessionErrorStream(abort.signal);
7312
+ this.sessionErrorStream = { abort, loop };
7313
+ }
7314
+ async runSessionErrorStream(signal) {
7315
+ let attempt = 0;
7316
+ let warned = false;
7317
+ while (!this.stopped && !signal.aborted) {
7318
+ const openedAt = this.now();
7319
+ try {
7320
+ const outcome = await readSessionErrorStream(this.port, {
7321
+ signal,
7322
+ onSessionError: (event) => this.handleSessionError(event)
7323
+ });
7324
+ if (outcome.reason === "aborted" || signal.aborted) return;
7325
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
7326
+ if (outcome.reason === "unavailable" || outcome.reason === "ended") {
7327
+ if (!healthy) {
7328
+ const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
7329
+ this.log({
7330
+ level: warned ? "debug" : "warn",
7331
+ message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
7332
+ });
7333
+ warned = true;
7334
+ }
7335
+ }
7336
+ if (healthy) {
7337
+ if (warned) {
7338
+ this.log({
7339
+ level: "info",
7340
+ message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
7341
+ });
7342
+ warned = false;
7343
+ }
7344
+ attempt = 0;
7345
+ } else {
7346
+ attempt += 1;
7347
+ }
7348
+ if (this.stopped || signal.aborted) return;
7349
+ await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
7350
+ } catch (err) {
7351
+ if (this.stopped || signal.aborted) return;
7352
+ this.log({
7353
+ level: "error",
7354
+ message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
7355
+ });
7356
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
7357
+ const delayAttempt = healthy ? 0 : attempt;
7358
+ attempt = healthy ? 0 : attempt + 1;
7359
+ try {
7360
+ await this.sleep(backoffDelay(delayAttempt, this.retry));
7361
+ } catch (sleepErr) {
7362
+ this.log({
7363
+ level: "error",
7364
+ message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
7365
+ });
7366
+ }
7367
+ }
7368
+ }
7369
+ }
7370
+ handleSessionError(event) {
7371
+ try {
7372
+ const watcher = this.watchers.get(event.sessionId);
7373
+ if (!watcher) {
7374
+ this.bufferSessionError(event);
7375
+ this.log({
7376
+ level: "debug",
7377
+ message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
7378
+ });
7379
+ return;
7380
+ }
7381
+ if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
7382
+ this.log({
7383
+ level: "debug",
7384
+ message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
7385
+ conversation_id: watcher.conv.id
7386
+ });
7387
+ return;
7388
+ }
7389
+ const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
7390
+ if (!inFlight) {
7391
+ this.bufferSessionError(event);
7392
+ this.log({
7393
+ level: "debug",
7394
+ message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
7395
+ conversation_id: watcher.conv.id
7396
+ });
7397
+ return;
7398
+ }
7399
+ if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
7400
+ this.sessionErrorHandled.add(inFlight.evidentMessageId);
7401
+ void this.failFromSessionError(watcher, event, inFlight);
7402
+ } catch (err) {
7403
+ this.log({
7404
+ level: "error",
7405
+ message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
7406
+ });
7407
+ }
7408
+ }
7409
+ async failFromSessionError(watcher, event, inFlight) {
7410
+ try {
7411
+ const messages = await getSessionMessages(this.port, event.sessionId);
7412
+ const state = messageRunState(messages, inFlight.opencodeMessageId);
7413
+ if (state !== "queued") {
7414
+ this.log({
7415
+ level: "debug",
7416
+ message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
7417
+ conversation_id: watcher.conv.id,
7418
+ message_id: inFlight.evidentMessageId
7419
+ });
7420
+ return;
7421
+ }
7422
+ this.log({
7423
+ level: "error",
7424
+ message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
7425
+ conversation_id: watcher.conv.id,
7426
+ message_id: inFlight.evidentMessageId
7427
+ });
7428
+ await this.markFailed(
7429
+ watcher.conv.id,
7430
+ inFlight.evidentMessageId,
7431
+ event.sessionId,
7432
+ `OpenCode could not run this turn: ${event.reason}`
7433
+ );
7434
+ inFlight.done = true;
7435
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
7436
+ } catch (err) {
7437
+ if (err instanceof ChannelAuthError) {
7438
+ this.log({
7439
+ level: "warn",
7440
+ 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`,
7441
+ conversation_id: watcher.conv.id,
7442
+ message_id: inFlight.evidentMessageId
7443
+ });
7444
+ } else {
7445
+ this.log({
7446
+ level: "warn",
7447
+ 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`,
7448
+ conversation_id: watcher.conv.id,
7449
+ message_id: inFlight.evidentMessageId
7450
+ });
7451
+ }
7452
+ } finally {
7453
+ this.sessionErrorHandled.delete(inFlight.evidentMessageId);
7454
+ }
7455
+ }
6823
7456
  /**
6824
7457
  * The per-session polling loop (WI-3). Once per tick it:
6825
7458
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -6932,6 +7565,21 @@ var ChannelDriver = class _ChannelDriver {
6932
7565
  const conv = watcher.conv;
6933
7566
  const state = messageRunState(messages, inFlight.opencodeMessageId);
6934
7567
  const id = inFlight.evidentMessageId;
7568
+ if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
7569
+ void this.resolveSubagentInvocations(
7570
+ messages,
7571
+ inFlight.opencodeMessageId,
7572
+ id,
7573
+ "prefetch"
7574
+ ).catch((err) => {
7575
+ this.log({
7576
+ level: "warn",
7577
+ message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
7578
+ conversation_id: conv.id,
7579
+ message_id: id
7580
+ });
7581
+ });
7582
+ }
6935
7583
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
6936
7584
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
6937
7585
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -6985,6 +7633,12 @@ var ChannelDriver = class _ChannelDriver {
6985
7633
  message_id: inFlight.evidentMessageId
6986
7634
  });
6987
7635
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
7636
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
7637
+ const subagentInvocations = await this.resolveSubagentInvocations(
7638
+ messages,
7639
+ inFlight.opencodeMessageId,
7640
+ inFlight.evidentMessageId
7641
+ );
6988
7642
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
6989
7643
  try {
6990
7644
  await this.markFailed(
@@ -6993,7 +7647,9 @@ var ChannelDriver = class _ChannelDriver {
6993
7647
  sessionId,
6994
7648
  error2,
6995
7649
  usage,
6996
- failure
7650
+ failure,
7651
+ usageAgentName,
7652
+ subagentInvocations
6997
7653
  );
6998
7654
  } catch (err) {
6999
7655
  if (err instanceof ChannelAuthError) throw err;
@@ -7026,13 +7682,21 @@ var ChannelDriver = class _ChannelDriver {
7026
7682
  return;
7027
7683
  }
7028
7684
  inFlight.done = true;
7685
+ await this.reportSubagentAuthFailures(
7686
+ watcher.conv.id,
7687
+ inFlight.opencodeMessageId,
7688
+ inFlight.evidentMessageId,
7689
+ messages
7690
+ );
7029
7691
  }
7030
7692
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7031
7693
  return;
7032
7694
  }
7695
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
7696
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
7033
7697
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
7034
7698
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
7035
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
7699
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
7036
7700
  inFlight.stuckReported = true;
7037
7701
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
7038
7702
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7193,7 +7857,7 @@ var ChannelDriver = class _ChannelDriver {
7193
7857
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7194
7858
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7195
7859
  );
7196
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
7860
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7197
7861
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7198
7862
  this.log({
7199
7863
  level: "debug",
@@ -7228,6 +7892,12 @@ var ChannelDriver = class _ChannelDriver {
7228
7892
  });
7229
7893
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7230
7894
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
7895
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
7896
+ const subagentInvocations = await this.resolveSubagentInvocations(
7897
+ messages,
7898
+ inFlight.opencodeMessageId,
7899
+ inFlight.evidentMessageId
7900
+ );
7231
7901
  try {
7232
7902
  await this.markDone(
7233
7903
  conv.id,
@@ -7235,7 +7905,9 @@ var ChannelDriver = class _ChannelDriver {
7235
7905
  sessionId,
7236
7906
  inFlight.opencodeMessageId,
7237
7907
  title,
7238
- usage
7908
+ usage,
7909
+ usageAgentName,
7910
+ subagentInvocations
7239
7911
  );
7240
7912
  } catch (err) {
7241
7913
  if (err instanceof ChannelAuthError) throw err;
@@ -7268,6 +7940,12 @@ var ChannelDriver = class _ChannelDriver {
7268
7940
  return;
7269
7941
  }
7270
7942
  inFlight.done = true;
7943
+ await this.reportSubagentAuthFailures(
7944
+ watcher.conv.id,
7945
+ inFlight.opencodeMessageId,
7946
+ inFlight.evidentMessageId,
7947
+ messages
7948
+ );
7271
7949
  }
7272
7950
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7273
7951
  }
@@ -7408,6 +8086,12 @@ var ChannelDriver = class _ChannelDriver {
7408
8086
  if (state === "failed" && !restartAborted) {
7409
8087
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7410
8088
  const usage = messageUsage(messages, ocId ?? "");
8089
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
8090
+ const subagentInvocations = await this.resolveSubagentInvocations(
8091
+ messages,
8092
+ ocId ?? "",
8093
+ row.id
8094
+ );
7411
8095
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7412
8096
  this.log({
7413
8097
  level: "error",
@@ -7416,7 +8100,16 @@ var ChannelDriver = class _ChannelDriver {
7416
8100
  message_id: row.id
7417
8101
  });
7418
8102
  try {
7419
- await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage, failure);
8103
+ await this.markFailed(
8104
+ row.conversation_id,
8105
+ row.id,
8106
+ sessionId,
8107
+ error2,
8108
+ usage,
8109
+ failure,
8110
+ usageAgentName,
8111
+ subagentInvocations
8112
+ );
7420
8113
  } catch (err) {
7421
8114
  if (err instanceof ChannelAuthError) throw err;
7422
8115
  if (err instanceof ChannelTerminalError) {
@@ -7438,6 +8131,7 @@ var ChannelDriver = class _ChannelDriver {
7438
8131
  });
7439
8132
  return;
7440
8133
  }
8134
+ await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
7441
8135
  this.dontRedispatch.delete(row.id);
7442
8136
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
7443
8137
  return;
@@ -7577,7 +8271,22 @@ var ChannelDriver = class _ChannelDriver {
7577
8271
  try {
7578
8272
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7579
8273
  const usage = messageUsage(messages, ocId ?? "");
7580
- await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
8274
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
8275
+ const subagentInvocations = await this.resolveSubagentInvocations(
8276
+ messages,
8277
+ ocId ?? "",
8278
+ row.id
8279
+ );
8280
+ await this.markDone(
8281
+ row.conversation_id,
8282
+ row.id,
8283
+ sessionId,
8284
+ ocId,
8285
+ title,
8286
+ usage,
8287
+ usageAgentName,
8288
+ subagentInvocations
8289
+ );
7581
8290
  } catch (err) {
7582
8291
  if (err instanceof ChannelAuthError) throw err;
7583
8292
  if (err instanceof ChannelTerminalError) {
@@ -7599,6 +8308,9 @@ var ChannelDriver = class _ChannelDriver {
7599
8308
  });
7600
8309
  return;
7601
8310
  }
8311
+ if (ocId !== null) {
8312
+ await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
8313
+ }
7602
8314
  this.dontRedispatch.delete(row.id);
7603
8315
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
7604
8316
  }
@@ -7692,14 +8404,14 @@ var ChannelDriver = class _ChannelDriver {
7692
8404
  this.unconfirmedDispatchFailures.delete(row.id);
7693
8405
  this.sessions.delete(readoptConv.id);
7694
8406
  this.supersede(readoptConv.id, sessionId);
7695
- 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.`;
8407
+ 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.`;
7696
8408
  this.log({
7697
8409
  level: "error",
7698
- message: errorMessage2,
8410
+ message: errorMessage3,
7699
8411
  conversation_id: row.conversation_id,
7700
8412
  message_id: row.id
7701
8413
  });
7702
- await this.markFailed(row.conversation_id, row.id, null, errorMessage2).catch((markErr) => {
8414
+ await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
7703
8415
  this.log({
7704
8416
  level: "warn",
7705
8417
  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)}`,
@@ -7978,6 +8690,166 @@ var ChannelDriver = class _ChannelDriver {
7978
8690
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
7979
8691
  return parent;
7980
8692
  }
8693
+ usageAgentName(messages, userMessageId) {
8694
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
8695
+ const mode = reply?.info?.mode;
8696
+ if (typeof mode === "string" && mode.length > 0) return mode;
8697
+ const agent = reply?.info?.agent;
8698
+ return typeof agent === "string" && agent.length > 0 ? agent : null;
8699
+ }
8700
+ async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
8701
+ if (!messages) return void 0;
8702
+ const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
8703
+ const cached = cache.get(messageId);
8704
+ if (cached) return cached;
8705
+ const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
8706
+ (err) => {
8707
+ this.log({
8708
+ level: "warn",
8709
+ message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
8710
+ message_id: messageId
8711
+ });
8712
+ return void 0;
8713
+ }
8714
+ );
8715
+ cache.set(messageId, collection);
8716
+ const result = await collection;
8717
+ if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
8718
+ return result;
8719
+ }
8720
+ clearSubagentInvocationCaches(messageId) {
8721
+ this.subagentInvocationCollections.delete(messageId);
8722
+ this.subagentInvocationPrefetches.delete(messageId);
8723
+ }
8724
+ async buildSubagentInvocations(messages, userMessageId, messageId) {
8725
+ const rootCalls = collectTaskCalls(messages, userMessageId);
8726
+ if (rootCalls.length === 0) return void 0;
8727
+ const childMessages = /* @__PURE__ */ new Map();
8728
+ const seenCallIds = new Set(rootCalls.map((call) => call.callID));
8729
+ const work = rootCalls.map((call) => ({
8730
+ call,
8731
+ depth: 1
8732
+ }));
8733
+ const payload = [];
8734
+ const fetchChildMessages = (sessionId) => {
8735
+ const cached = childMessages.get(sessionId);
8736
+ if (cached) return cached;
8737
+ const pending = (async () => {
8738
+ try {
8739
+ const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8740
+ if (!res.ok) {
8741
+ this.log({
8742
+ level: "warn",
8743
+ 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`,
8744
+ message_id: messageId
8745
+ });
8746
+ return null;
8747
+ }
8748
+ const body = await res.json();
8749
+ if (!Array.isArray(body)) throw new Error("response body was not a message array");
8750
+ return body;
8751
+ } catch (err) {
8752
+ this.log({
8753
+ level: "warn",
8754
+ 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)}`,
8755
+ message_id: messageId
8756
+ });
8757
+ return null;
8758
+ }
8759
+ })();
8760
+ childMessages.set(sessionId, pending);
8761
+ return pending;
8762
+ };
8763
+ const fetchChildWithoutBlocking = async (sessionId) => {
8764
+ const pending = fetchChildMessages(sessionId);
8765
+ let timer;
8766
+ const timeout = new Promise((resolve4) => {
8767
+ timer = setTimeout(() => {
8768
+ this.log({
8769
+ level: "warn",
8770
+ 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`,
8771
+ message_id: messageId
8772
+ });
8773
+ resolve4(null);
8774
+ }, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
8775
+ });
8776
+ try {
8777
+ return await Promise.race([pending, timeout]);
8778
+ } finally {
8779
+ if (timer !== void 0) clearTimeout(timer);
8780
+ }
8781
+ };
8782
+ while (work.length > 0) {
8783
+ const groups = /* @__PURE__ */ new Map();
8784
+ for (const item of work.splice(0)) {
8785
+ const group = groups.get(item.call.childSessionId) ?? [];
8786
+ group.push(item);
8787
+ groups.set(item.call.childSessionId, group);
8788
+ }
8789
+ const groupResults = await Promise.all(
8790
+ [...groups].map(async ([sessionId, items]) => ({
8791
+ sessionId,
8792
+ items,
8793
+ messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
8794
+ }))
8795
+ );
8796
+ for (const { sessionId, items, messages: child } of groupResults) {
8797
+ if (sessionId !== null && child === null) continue;
8798
+ const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
8799
+ child,
8800
+ items.map(({ call }) => ({
8801
+ callID: call.callID,
8802
+ timeStart: call.timeStart,
8803
+ timeEnd: call.timeEnd
8804
+ }))
8805
+ );
8806
+ if (sessionId !== null && attribution.unattributed.length > 0) {
8807
+ this.log({
8808
+ level: "warn",
8809
+ 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`,
8810
+ message_id: messageId
8811
+ });
8812
+ }
8813
+ const usageByCall = new Map(
8814
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
8815
+ );
8816
+ const messagesByCall = new Map(
8817
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
8818
+ );
8819
+ for (const { call, depth } of items) {
8820
+ const usage = usageByCall.get(call.callID) ?? null;
8821
+ payload.push({
8822
+ tool_call_id: call.callID,
8823
+ agent_name: call.subagentName,
8824
+ opencode_session_id: call.childSessionId,
8825
+ parent_opencode_session_id: call.parentSessionId,
8826
+ depth,
8827
+ status: call.status,
8828
+ started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
8829
+ ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
8830
+ usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
8831
+ usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
8832
+ usage_tokens_input: usage?.usage_tokens_input ?? null,
8833
+ usage_tokens_output: usage?.usage_tokens_output ?? null,
8834
+ usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
8835
+ usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
8836
+ usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
8837
+ usage_cost_usd: usage?.usage_cost_usd ?? null
8838
+ });
8839
+ for (const assigned of messagesByCall.get(call.callID) ?? []) {
8840
+ const parentId = assigned.info?.parentID ?? assigned.parentID;
8841
+ if (!parentId) continue;
8842
+ for (const nested of collectTaskCalls([assigned], parentId)) {
8843
+ if (seenCallIds.has(nested.callID)) continue;
8844
+ seenCallIds.add(nested.callID);
8845
+ work.push({ call: nested, depth: depth + 1 });
8846
+ }
8847
+ }
8848
+ }
8849
+ }
8850
+ }
8851
+ return payload.length > 0 ? payload : void 0;
8852
+ }
7981
8853
  /**
7982
8854
  * OpenCode's synchronous default session title (e.g.
7983
8855
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8461,7 +9333,7 @@ var ChannelDriver = class _ChannelDriver {
8461
9333
  * watcher retries next tick within the
8462
9334
  * deadline, Finding 4).
8463
9335
  */
8464
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
9336
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8465
9337
  const res = await this.fetchImpl(
8466
9338
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8467
9339
  {
@@ -8477,15 +9349,21 @@ var ChannelDriver = class _ChannelDriver {
8477
9349
  opencode_session_id: sessionId,
8478
9350
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8479
9351
  ...title ? { title } : {},
8480
- ...usage ? usage : {}
9352
+ ...usage ? usage : {},
9353
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
9354
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8481
9355
  })
8482
9356
  }
8483
9357
  );
8484
9358
  this.assertAuth(res, "marking message as done");
8485
- if (res.ok) return;
9359
+ if (res.ok) {
9360
+ this.clearSubagentInvocationCaches(messageId);
9361
+ return;
9362
+ }
8486
9363
  if (isRetryableStatus(res.status)) {
8487
9364
  throw new Error(`marking message as done: HTTP ${res.status}`);
8488
9365
  }
9366
+ this.clearSubagentInvocationCaches(messageId);
8489
9367
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8490
9368
  }
8491
9369
  /**
@@ -8500,7 +9378,7 @@ var ChannelDriver = class _ChannelDriver {
8500
9378
  * exists but is wedged, so the next attempt must get a fresh one
8501
9379
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8502
9380
  */
8503
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
9381
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8504
9382
  const body = { status: "failed" };
8505
9383
  if (sessionId === null) {
8506
9384
  body.opencode_session_id = null;
@@ -8509,23 +9387,33 @@ var ChannelDriver = class _ChannelDriver {
8509
9387
  }
8510
9388
  if (error2 !== void 0) body.error = error2;
8511
9389
  if (usage) Object.assign(body, usage);
9390
+ if (usageAgentName) body.usage_agent_name = usageAgentName;
9391
+ if (subagentInvocations && subagentInvocations.length > 0) {
9392
+ body.subagent_invocations = subagentInvocations;
9393
+ }
8512
9394
  if (failure) {
8513
9395
  body.failure_kind = failure.kind;
8514
9396
  body.failure_provider_id = failure.providerId;
8515
9397
  body.failure_model_id = failure.modelId;
8516
9398
  body.failure_reason = failure.reason;
8517
9399
  }
8518
- await this.callWithRetry(
8519
- "marking message as failed",
8520
- () => this.fetchImpl(
8521
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8522
- {
8523
- method: "PATCH",
8524
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8525
- body: JSON.stringify(body)
8526
- }
8527
- )
8528
- );
9400
+ try {
9401
+ await this.callWithRetry(
9402
+ "marking message as failed",
9403
+ () => this.fetchImpl(
9404
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
9405
+ {
9406
+ method: "PATCH",
9407
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
9408
+ body: JSON.stringify(body)
9409
+ }
9410
+ )
9411
+ );
9412
+ } catch (err) {
9413
+ if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
9414
+ throw err;
9415
+ }
9416
+ this.clearSubagentInvocationCaches(messageId);
8529
9417
  }
8530
9418
  /**
8531
9419
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -8550,6 +9438,111 @@ var ChannelDriver = class _ChannelDriver {
8550
9438
  reply?.info?.modelID ?? null
8551
9439
  );
8552
9440
  }
9441
+ async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
9442
+ const providerId = failure.providerId ?? "(unknown)";
9443
+ try {
9444
+ const res = await this.fetchImpl(
9445
+ `${this.apiUrl}/runners/${this.agentId}/model-auth-failures`,
9446
+ {
9447
+ method: "POST",
9448
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
9449
+ body: JSON.stringify({
9450
+ provider_id: failure.providerId,
9451
+ model_id: failure.modelId,
9452
+ reason: failure.reason
9453
+ })
9454
+ }
9455
+ );
9456
+ if (!res.ok) {
9457
+ this.log({
9458
+ level: "warn",
9459
+ message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
9460
+ conversation_id: conversationId,
9461
+ message_id: messageId
9462
+ });
9463
+ }
9464
+ } catch (err) {
9465
+ this.log({
9466
+ level: "warn",
9467
+ message: `Sub-agent model-auth report for provider ${providerId} (${failure.reason}) failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
9468
+ conversation_id: conversationId,
9469
+ message_id: messageId
9470
+ });
9471
+ }
9472
+ }
9473
+ async clearSubagentModelAuthFailure(providerId, conversationId, messageId) {
9474
+ try {
9475
+ const res = await this.fetchImpl(
9476
+ `${this.apiUrl}/runners/${this.agentId}/model-auth-failures/${encodeURIComponent(providerId)}`,
9477
+ {
9478
+ method: "DELETE",
9479
+ headers: { Authorization: this.getAuthHeader() }
9480
+ }
9481
+ );
9482
+ if (!res.ok) {
9483
+ this.log({
9484
+ level: "warn",
9485
+ message: `Sub-agent model-auth clear for provider ${providerId} returned HTTP ${res.status} (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)})`,
9486
+ conversation_id: conversationId,
9487
+ message_id: messageId
9488
+ });
9489
+ }
9490
+ } catch (err) {
9491
+ this.log({
9492
+ level: "warn",
9493
+ message: `Sub-agent model-auth clear for provider ${providerId} failed with status network-error (conversation ${conversationId.slice(0, 8)}, message ${messageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
9494
+ conversation_id: conversationId,
9495
+ message_id: messageId
9496
+ });
9497
+ }
9498
+ }
9499
+ async reportSubagentAuthFailures(conversationId, opencodeMessageId, evidentMessageId, messages) {
9500
+ const refs = collectSubagentSessions(messages, opencodeMessageId);
9501
+ if (refs.length === 0) return;
9502
+ const failedProviders = /* @__PURE__ */ new Map();
9503
+ const succeededProviders = /* @__PURE__ */ new Set();
9504
+ for (const ref of refs) {
9505
+ try {
9506
+ const childMessages = await getSessionMessages(this.port, ref.sessionId);
9507
+ if (childMessages === null) {
9508
+ this.log({
9509
+ level: "debug",
9510
+ message: `Could not read sub-agent session ${ref.sessionId} while checking credential failures \u2014 skipping it`,
9511
+ conversation_id: conversationId,
9512
+ message_id: evidentMessageId
9513
+ });
9514
+ continue;
9515
+ }
9516
+ const outcome = findSubagentAuthOutcome(childMessages, ref.startedAtMs);
9517
+ if (!outcome) continue;
9518
+ if (outcome.outcome === "failed") {
9519
+ failedProviders.set(outcome.providerId, outcome.failure);
9520
+ } else {
9521
+ succeededProviders.add(outcome.providerId);
9522
+ }
9523
+ } catch (err) {
9524
+ this.log({
9525
+ level: "warn",
9526
+ message: `Failed to inspect sub-agent session ${ref.sessionId} for credential failures (conversation ${conversationId.slice(0, 8)}, message ${evidentMessageId.slice(0, 8)}): ${err instanceof Error ? err.message : String(err)}`,
9527
+ conversation_id: conversationId,
9528
+ message_id: evidentMessageId
9529
+ });
9530
+ }
9531
+ }
9532
+ for (const [providerId, failure] of failedProviders) {
9533
+ this.log({
9534
+ level: "warn",
9535
+ message: `Sub-agent turn failed on provider ${providerId} (${failure.reason}) \u2014 recording credential evidence`,
9536
+ conversation_id: conversationId,
9537
+ message_id: evidentMessageId
9538
+ });
9539
+ await this.recordSubagentModelAuthFailure(failure, conversationId, evidentMessageId);
9540
+ }
9541
+ for (const providerId of succeededProviders) {
9542
+ if (failedProviders.has(providerId)) continue;
9543
+ await this.clearSubagentModelAuthFailure(providerId, conversationId, evidentMessageId);
9544
+ }
9545
+ }
8553
9546
  /**
8554
9547
  * Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
8555
9548
  * (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
@@ -8703,6 +9696,13 @@ import chalk5 from "chalk";
8703
9696
  import ora2 from "ora";
8704
9697
  import { select as select2 } from "@inquirer/prompts";
8705
9698
  var INTERACTIVE_START_TIMEOUT_MS = 3e4;
9699
+ function checkNonInteractivePortConflict(port, isPortInUseFn) {
9700
+ if (isPortInUseFn(port)) {
9701
+ throw new Error(
9702
+ `Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
9703
+ );
9704
+ }
9705
+ }
8706
9706
  async function ensureOpenCodeRunning(ctx) {
8707
9707
  const healthCheck = await checkOpenCodeHealth(ctx.port);
8708
9708
  if (healthCheck.healthy) {
@@ -8750,6 +9750,7 @@ async function ensureOpenCodeRunning(ctx) {
8750
9750
  }
8751
9751
  }
8752
9752
  if (!ctx.interactive) {
9753
+ checkNonInteractivePortConflict(ctx.port, isPortInUse);
8753
9754
  ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
8754
9755
  const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
8755
9756
  const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
@@ -8831,9 +9832,119 @@ Port ${port} is already in use.`));
8831
9832
  return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
8832
9833
  }
8833
9834
 
9835
+ // src/commands/ensure-opencode-v2.ts
9836
+ import chalk6 from "chalk";
9837
+ import { select as select3 } from "@inquirer/prompts";
9838
+ async function probeOpenCode2WithoutPassword(port) {
9839
+ try {
9840
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
9841
+ signal: AbortSignal.timeout(2e3)
9842
+ });
9843
+ if (response.status === 401) {
9844
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
9845
+ }
9846
+ if (!response.ok) {
9847
+ return { healthy: false, error: `HTTP ${response.status}` };
9848
+ }
9849
+ return { healthy: true };
9850
+ } catch (error2) {
9851
+ return {
9852
+ healthy: false,
9853
+ error: error2 instanceof Error ? error2.message : "Unknown error"
9854
+ };
9855
+ }
9856
+ }
9857
+ function unknownPasswordError(port) {
9858
+ return new Error(
9859
+ `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
9860
+ );
9861
+ }
9862
+ function v2SessionSupportIncompleteError() {
9863
+ return new Error(
9864
+ "OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
9865
+ );
9866
+ }
9867
+ async function ensureOpenCode2Running(ctx) {
9868
+ const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
9869
+ if (initialHealth.authFailed) {
9870
+ throw unknownPasswordError(ctx.port);
9871
+ }
9872
+ if (initialHealth.healthy) {
9873
+ return {
9874
+ port: ctx.port,
9875
+ process: null,
9876
+ version: null,
9877
+ notReadyReason: null,
9878
+ password: null
9879
+ };
9880
+ }
9881
+ if (!isOpenCode2Installed()) {
9882
+ throw new Error(
9883
+ "OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
9884
+ );
9885
+ }
9886
+ let port = ctx.port;
9887
+ if (!ctx.interactive) {
9888
+ checkNonInteractivePortConflict(port, isPortInUse);
9889
+ } else if (isPortInUse(port)) {
9890
+ console.log(chalk6.yellow(`
9891
+ Port ${port} is already in use.`));
9892
+ const alternativePort = findAvailablePort(port + 1);
9893
+ if (alternativePort) {
9894
+ const useAlternative = await select3({
9895
+ message: `Use port ${alternativePort} instead?`,
9896
+ choices: [
9897
+ { name: `Yes, use port ${alternativePort}`, value: "yes" },
9898
+ { name: "No, I will free the port manually", value: "no" }
9899
+ ]
9900
+ });
9901
+ if (useAlternative === "yes") {
9902
+ port = alternativePort;
9903
+ } else {
9904
+ throw new Error(`Port ${ctx.port} is in use`);
9905
+ }
9906
+ }
9907
+ }
9908
+ if (!ctx.interactive) {
9909
+ throw v2SessionSupportIncompleteError();
9910
+ }
9911
+ console.log(chalk6.yellow(`
9912
+ ${v2SessionSupportIncompleteError().message}`));
9913
+ const action = await select3({
9914
+ message: "OpenCode V2 is not running. What would you like to do?",
9915
+ choices: [
9916
+ {
9917
+ name: "Show me the command",
9918
+ value: "manual",
9919
+ description: "Display the command to run manually"
9920
+ },
9921
+ {
9922
+ name: "Continue without OpenCode V2",
9923
+ value: "continue",
9924
+ description: "Requests will fail until OpenCode V2 starts"
9925
+ }
9926
+ ]
9927
+ });
9928
+ if (action === "manual") {
9929
+ blank();
9930
+ console.log(chalk6.bold("Run this command in another terminal:"));
9931
+ blank();
9932
+ console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
9933
+ blank();
9934
+ throw new Error("Please start OpenCode V2 manually");
9935
+ }
9936
+ return {
9937
+ port,
9938
+ process: null,
9939
+ version: null,
9940
+ notReadyReason: "you chose to continue without OpenCode V2",
9941
+ password: null
9942
+ };
9943
+ }
9944
+
8834
9945
  // src/lib/runner-credentials.ts
8835
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
8836
- import { spawn as spawn5 } from "child_process";
9946
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
9947
+ import { spawn as spawn5 } from "node:child_process";
8837
9948
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
8838
9949
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
8839
9950
  var GITHUB_PROBE_TIMEOUT_MS = 1e4;
@@ -9105,11 +10216,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
9105
10216
  }
9106
10217
 
9107
10218
  // src/lib/opencode/config-overlay.ts
9108
- import { execFileSync as execFileSync2 } from "child_process";
9109
- import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
9110
- import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
10219
+ import { execFileSync as execFileSync2 } from "node:child_process";
10220
+ import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
10221
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
9111
10222
  function isFile(filePath) {
9112
- return existsSync2(filePath) && statSync5(filePath).isFile();
10223
+ return existsSync2(filePath) && statSync6(filePath).isFile();
9113
10224
  }
9114
10225
  function applyRunnerOpenCodeConfig({
9115
10226
  overlayPath,
@@ -9121,7 +10232,7 @@ function applyRunnerOpenCodeConfig({
9121
10232
  return;
9122
10233
  }
9123
10234
  const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
9124
- const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
10235
+ const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
9125
10236
  if (!isFile(source)) {
9126
10237
  log3(
9127
10238
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -9129,7 +10240,7 @@ function applyRunnerOpenCodeConfig({
9129
10240
  );
9130
10241
  return;
9131
10242
  }
9132
- copyFileSync(source, join8(cwd, target));
10243
+ copyFileSync(source, join9(cwd, target));
9133
10244
  try {
9134
10245
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
9135
10246
  stdio: "ignore"
@@ -9138,11 +10249,11 @@ function applyRunnerOpenCodeConfig({
9138
10249
  const detail = error2 instanceof Error ? error2.message : String(error2);
9139
10250
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
9140
10251
  }
9141
- log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
10252
+ log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
9142
10253
  }
9143
10254
 
9144
10255
  // src/lib/credential-sync.ts
9145
- import { renameSync, writeFileSync as writeFileSync5 } from "fs";
10256
+ import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
9146
10257
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
9147
10258
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
9148
10259
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -9152,7 +10263,7 @@ var MAX_FLUSH_PASSES = 2;
9152
10263
  function outcomesWith(outcome) {
9153
10264
  return { claude: outcome, opencode: outcome };
9154
10265
  }
9155
- function errorMessage(error2) {
10266
+ function errorMessage2(error2) {
9156
10267
  return error2 instanceof Error ? error2.message : String(error2);
9157
10268
  }
9158
10269
  function waitForSettlement(promise, timeoutMs) {
@@ -9179,7 +10290,7 @@ function writeMarker(markerPath, outcomes, log3) {
9179
10290
  writeFileSync5(temporaryPath, body, { mode: 384 });
9180
10291
  renameSync(temporaryPath, markerPath);
9181
10292
  } catch (error2) {
9182
- log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage(error2)}`, "warn");
10293
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
9183
10294
  }
9184
10295
  }
9185
10296
  function intervalSeconds(env, log3) {
@@ -9211,7 +10322,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
9211
10322
  },
9212
10323
  (error2) => {
9213
10324
  failed = true;
9214
- log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage(error2)}`, "warn");
10325
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
9215
10326
  }
9216
10327
  );
9217
10328
  const abortTimer = setTimeout(() => controller.abort(), remainingMs);
@@ -9271,7 +10382,7 @@ function createCredentialSync({
9271
10382
  outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
9272
10383
  } catch (error2) {
9273
10384
  outcomes[store] = "failed";
9274
- log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage(error2)}`, "debug");
10385
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
9275
10386
  }
9276
10387
  }
9277
10388
  const failed = STORES.some((store) => outcomes[store] === "failed");
@@ -9413,7 +10524,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
9413
10524
  if (trimmed === "") {
9414
10525
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
9415
10526
  }
9416
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join9(homeDir, trimmed.slice(2)) : trimmed;
10527
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
9417
10528
  if (!isAbsolute3(expanded)) {
9418
10529
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
9419
10530
  }
@@ -9437,6 +10548,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
9437
10548
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
9438
10549
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
9439
10550
  var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
10551
+ var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
10552
+ function resolveOpenCodeVersion(options, env = process.env) {
10553
+ let raw;
10554
+ let source;
10555
+ if (options.opencodeVersion !== void 0) {
10556
+ raw = options.opencodeVersion;
10557
+ source = "--opencode-version";
10558
+ } else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
10559
+ raw = env[OPENCODE_VERSION_ENV];
10560
+ source = OPENCODE_VERSION_ENV;
10561
+ } else {
10562
+ return { version: "v1", warnings: [] };
10563
+ }
10564
+ const normalized = raw.trim().toLowerCase();
10565
+ if (normalized !== "v1" && normalized !== "v2") {
10566
+ return {
10567
+ version: "v1",
10568
+ warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
10569
+ };
10570
+ }
10571
+ return { version: normalized, warnings: [] };
10572
+ }
9440
10573
  function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
9441
10574
  const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
9442
10575
  let raw;
@@ -9503,7 +10636,7 @@ function log2(state, message, level = "info") {
9503
10636
  })
9504
10637
  );
9505
10638
  } else if (!state.interactive) {
9506
- const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
10639
+ const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
9507
10640
  console.log(`${prefix} ${message}`);
9508
10641
  }
9509
10642
  }
@@ -9533,7 +10666,7 @@ function logActivity(state, entry) {
9533
10666
  }
9534
10667
  function reportSessionDbRecovery(state) {
9535
10668
  try {
9536
- const report = drainSessionDbRecoveryReport({ homeDir: homedir5(), env: process.env });
10669
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
9537
10670
  for (const record of report.records) {
9538
10671
  const activity = buildSessionDbRecoveryActivity(record);
9539
10672
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -9564,18 +10697,18 @@ function reportSessionDbRecoveryRecord(state, record) {
9564
10697
  function displayStatus(state) {
9565
10698
  if (!state.interactive) return;
9566
10699
  const attempt = state.connection?.reconnectAttempt ?? 0;
9567
- const tunnel = state.connected ? chalk6.green("tunnel: connected") : attempt > 0 ? chalk6.yellow(`tunnel: reconnecting (#${attempt})`) : chalk6.yellow("tunnel: connecting");
9568
- const opencode = state.opencodeConnected ? chalk6.green(`opencode: :${state.port}`) : chalk6.red(`opencode: :${state.port} (down)`);
9569
- const messages = state.messageCount > 0 ? chalk6.dim(` \xB7 ${state.messageCount} processed`) : "";
10700
+ const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
10701
+ const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
10702
+ const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
9570
10703
  const last = state.activityLog[state.activityLog.length - 1];
9571
- const detail = last ? chalk6.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
10704
+ const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
9572
10705
  const agent = state.agentName ?? state.agentId;
9573
10706
  console.log(
9574
- `${chalk6.bold("Evident")} ${chalk6.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
10707
+ `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
9575
10708
  );
9576
10709
  }
9577
10710
  async function promptForLogin(promptMessage, successMessage) {
9578
- const action = await select3({
10711
+ const action = await select4({
9579
10712
  message: promptMessage,
9580
10713
  choices: [
9581
10714
  {
@@ -9591,7 +10724,7 @@ async function promptForLogin(promptMessage, successMessage) {
9591
10724
  ]
9592
10725
  });
9593
10726
  if (action === "exit") {
9594
- console.log(chalk6.dim(`
10727
+ console.log(chalk7.dim(`
9595
10728
  You can log in later by running: ${getCliName()} login`));
9596
10729
  process.exit(0);
9597
10730
  }
@@ -9602,7 +10735,7 @@ You can log in later by running: ${getCliName()} login`));
9602
10735
  process.exit(1);
9603
10736
  }
9604
10737
  blank();
9605
- console.log(chalk6.green(successMessage));
10738
+ console.log(chalk7.green(successMessage));
9606
10739
  blank();
9607
10740
  return { token: credentials2.token, authType: "bearer", user: credentials2.user };
9608
10741
  }
@@ -9615,12 +10748,12 @@ async function handleAuthError(state, error2) {
9615
10748
  if (state.interactive) displayStatus(state);
9616
10749
  if (!state.interactive) {
9617
10750
  blank();
9618
- console.log(chalk6.red("Authentication expired"));
9619
- console.log(chalk6.dim("Your authentication token is no longer valid."));
10751
+ console.log(chalk7.red("Authentication expired"));
10752
+ console.log(chalk7.dim("Your authentication token is no longer valid."));
9620
10753
  blank();
9621
- console.log(chalk6.dim("To fix this:"));
9622
- console.log(chalk6.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
9623
- console.log(chalk6.dim(" 2. Restart this command"));
10754
+ console.log(chalk7.dim("To fix this:"));
10755
+ console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
10756
+ console.log(chalk7.dim(" 2. Restart this command"));
9624
10757
  blank();
9625
10758
  await cleanup(state);
9626
10759
  await shutdownTelemetry();
@@ -9628,7 +10761,7 @@ async function handleAuthError(state, error2) {
9628
10761
  return { success: false };
9629
10762
  }
9630
10763
  blank();
9631
- console.log(chalk6.yellow("Your authentication has expired."));
10764
+ console.log(chalk7.yellow("Your authentication has expired."));
9632
10765
  blank();
9633
10766
  try {
9634
10767
  const credentials2 = await promptForLogin(
@@ -9692,6 +10825,14 @@ async function driveChannels(state, driver) {
9692
10825
  const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
9693
10826
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
9694
10827
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
10828
+ if (claudeCredentialApplied || opencodeAuthApplied) {
10829
+ void reloadProviderCache(state.port).catch(
10830
+ (error2) => logActivity(state, {
10831
+ type: "error",
10832
+ error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
10833
+ })
10834
+ );
10835
+ }
9695
10836
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
9696
10837
  idlePolls = 0;
9697
10838
  idleMs = 0;
@@ -9719,8 +10860,8 @@ async function driveChannels(state, driver) {
9719
10860
  state.running = false;
9720
10861
  break;
9721
10862
  }
9722
- const errorMessage2 = error2 instanceof Error ? error2.message : String(error2);
9723
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage2}` });
10863
+ const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
10864
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
9724
10865
  if (state.interactive) displayStatus(state);
9725
10866
  if (driver.hasInFlightWatchers()) {
9726
10867
  consecutiveDrainFailures = 0;
@@ -9758,9 +10899,18 @@ async function driveChannels(state, driver) {
9758
10899
  }
9759
10900
  }
9760
10901
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
9761
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
10902
+ var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
10903
+ function shouldWarnForReclaimSkip(reason) {
10904
+ if (reason !== "sqlite-unavailable") return false;
10905
+ const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
10906
+ if (!version2) return false;
10907
+ const major = Number(version2[1]);
10908
+ const minor = Number(version2[2]);
10909
+ const patch = Number(version2[3]);
10910
+ return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
10911
+ }
9762
10912
  function sessionDbPath() {
9763
- return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
10913
+ return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
9764
10914
  }
9765
10915
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
9766
10916
  const record = {
@@ -9856,7 +11006,7 @@ async function runSweep(state, driver, config) {
9856
11006
  } else {
9857
11007
  logActivity(state, {
9858
11008
  type: "info",
9859
- message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
11009
+ message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
9860
11010
  });
9861
11011
  }
9862
11012
  } catch (error2) {
@@ -9879,13 +11029,20 @@ function scheduleSessionCleanup(state, driver, options) {
9879
11029
  for (const warning2 of config.warnings) {
9880
11030
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
9881
11031
  }
9882
- const dbBytes = statSessionDbBytes(homedir5());
11032
+ const dbBytes = statSessionDbBytes(homedir6());
9883
11033
  void (async () => {
9884
- const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
11034
+ const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
11035
+ if (reclaimAvailability !== null) {
11036
+ logActivity(state, {
11037
+ type: "info",
11038
+ level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
11039
+ message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
11040
+ });
11041
+ }
9885
11042
  const sizeWarning = buildSessionStoreSizeWarning({
9886
11043
  dbBytes,
9887
11044
  cleanupEnabled: config.enabled,
9888
- reclaimSkipReason
11045
+ reclaimSkipReason: reclaimAvailability?.reason ?? null
9889
11046
  });
9890
11047
  if (sizeWarning !== null) {
9891
11048
  logActivity(state, { type: "info", level: "warn", message: sizeWarning });
@@ -10052,14 +11209,11 @@ function scheduleClaudeUsageReporting(state, options) {
10052
11209
  report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
10053
11210
  isLocalCredentialProblem,
10054
11211
  forcedOnHint: "run `claude` to sign in",
10055
- firstDelayMs: () => FIRST_REPORT_DELAY_MS,
10056
- nextDelayMs: nextReportDelayMs,
10057
- failureLogLevel: claudeUsageFailureLogLevel
11212
+ firstDelayMs: firstReportDelayMs,
11213
+ nextDelayMs: usageReportDelayMs,
11214
+ failureLogLevel: usageReportFailureLogLevel
10058
11215
  });
10059
11216
  }
10060
- var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
10061
- var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
10062
- var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
10063
11217
  function scheduleResourceUsageReporting(state, options) {
10064
11218
  const { enabled, warnings } = resolveResourceUsageReportingEnabled(
10065
11219
  options.resourceUsageReporting,
@@ -10080,7 +11234,8 @@ function scheduleResourceUsageReporting(state, options) {
10080
11234
  });
10081
11235
  return;
10082
11236
  }
10083
- const collect = createResourceUsageCollector(homedir5());
11237
+ const { collect, stop } = createResourceUsageCollector(homedir6());
11238
+ state.stopResourceUsageSampling = stop;
10084
11239
  let consecutiveFailures = 0;
10085
11240
  const tick = async () => {
10086
11241
  try {
@@ -10111,10 +11266,7 @@ function scheduleResourceUsageReporting(state, options) {
10111
11266
  consecutiveFailures++;
10112
11267
  logActivity(state, {
10113
11268
  type: "info",
10114
- level: reportFailureLogLevel(
10115
- consecutiveFailures,
10116
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10117
- ),
11269
+ level: usageReportFailureLogLevel(consecutiveFailures),
10118
11270
  message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
10119
11271
  });
10120
11272
  }
@@ -10123,20 +11275,11 @@ function scheduleResourceUsageReporting(state, options) {
10123
11275
  const message = error2 instanceof Error ? error2.message : String(error2);
10124
11276
  logActivity(state, {
10125
11277
  type: "info",
10126
- level: reportFailureLogLevel(
10127
- consecutiveFailures,
10128
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10129
- ),
11278
+ level: usageReportFailureLogLevel(consecutiveFailures),
10130
11279
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
10131
11280
  });
10132
11281
  } finally {
10133
- state.resourceUsageTimer = setTimeout(
10134
- () => void tick(),
10135
- jitteredDelayMs(
10136
- RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
10137
- RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
10138
- )
10139
- );
11282
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
10140
11283
  }
10141
11284
  };
10142
11285
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -10176,6 +11319,8 @@ async function cleanup(state, opts = {}) {
10176
11319
  clearTimeout(timer);
10177
11320
  }
10178
11321
  state.sessionCleanupTimers = [];
11322
+ state.stopOpenCodeLogTail?.();
11323
+ state.stopOpenCodeLogTail = null;
10179
11324
  if (state.claudeUsageTimer) {
10180
11325
  clearTimeout(state.claudeUsageTimer);
10181
11326
  state.claudeUsageTimer = null;
@@ -10190,6 +11335,8 @@ async function cleanup(state, opts = {}) {
10190
11335
  clearTimeout(state.resourceUsageTimer);
10191
11336
  state.resourceUsageTimer = null;
10192
11337
  }
11338
+ state.stopResourceUsageSampling?.();
11339
+ state.stopResourceUsageSampling = null;
10193
11340
  const credentialSync = state.credentialSync;
10194
11341
  const flushCredentials = credentialSync ? async (phase, publish) => {
10195
11342
  await timeShutdownPhase(state, durations, phase, async () => {
@@ -10312,7 +11459,7 @@ async function run(options) {
10312
11459
  let fileSyncDirectories;
10313
11460
  try {
10314
11461
  logLevel = resolveLogLevel(options);
10315
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir5());
11462
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
10316
11463
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
10317
11464
  throw new Error(
10318
11465
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -10343,6 +11490,7 @@ async function run(options) {
10343
11490
  opencodeVersion: null,
10344
11491
  sessionDbProvenanceAnomaly: false,
10345
11492
  opencodeProcess: null,
11493
+ stopOpenCodeLogTail: null,
10346
11494
  litestreamProcess: null,
10347
11495
  connection: null,
10348
11496
  channelDriver: null,
@@ -10357,6 +11505,7 @@ async function run(options) {
10357
11505
  openaiUsageTimer: null,
10358
11506
  openaiUsageRearm: null,
10359
11507
  resourceUsageTimer: null,
11508
+ stopResourceUsageSampling: null,
10360
11509
  credentialSync: null,
10361
11510
  authHeader: ""
10362
11511
  };
@@ -10409,15 +11558,15 @@ async function run(options) {
10409
11558
  printError("Authentication required");
10410
11559
  blank();
10411
11560
  console.log(
10412
- chalk6.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
11561
+ chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
10413
11562
  );
10414
- console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
11563
+ console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
10415
11564
  blank();
10416
11565
  process.exit(1);
10417
11566
  return;
10418
11567
  }
10419
11568
  blank();
10420
- console.log(chalk6.yellow("You are not logged in to Evident."));
11569
+ console.log(chalk7.yellow("You are not logged in to Evident."));
10421
11570
  blank();
10422
11571
  credentials2 = await promptForLogin(
10423
11572
  "Would you like to log in now?",
@@ -10467,7 +11616,7 @@ async function run(options) {
10467
11616
  );
10468
11617
  blank();
10469
11618
  console.log(
10470
- chalk6.dim(
11619
+ chalk7.dim(
10471
11620
  "Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
10472
11621
  )
10473
11622
  );
@@ -10490,15 +11639,15 @@ async function run(options) {
10490
11639
  );
10491
11640
  if (interactive && !state.json) {
10492
11641
  blank();
10493
- console.log(chalk6.bold("Evident Run"));
10494
- console.log(chalk6.dim("-".repeat(40)));
11642
+ console.log(chalk7.bold("Evident Run"));
11643
+ console.log(chalk7.dim("-".repeat(40)));
10495
11644
  }
10496
11645
  const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
10497
11646
  let validation = await getAgentInfo(state.agentId, state.authHeader);
10498
11647
  if (!validation.valid && validation.authFailed && interactive) {
10499
11648
  spinner?.fail("Authentication failed");
10500
11649
  blank();
10501
- console.log(chalk6.yellow("Your authentication token is invalid or expired."));
11650
+ console.log(chalk7.yellow("Your authentication token is invalid or expired."));
10502
11651
  blank();
10503
11652
  credentials2 = await promptForLogin(
10504
11653
  "Would you like to log in again?",
@@ -10546,6 +11695,13 @@ async function run(options) {
10546
11695
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
10547
11696
  }
10548
11697
  state.credentialSync?.arm();
11698
+ state.stopOpenCodeLogTail = tailOpenCodeLogFile(
11699
+ resolveOpenCodeLogPath(homedir6(), process.env),
11700
+ createOpenCodeActivityForwarder(() => ({
11701
+ agentId: state.agentId,
11702
+ authHeader: state.authHeader
11703
+ }))
11704
+ ).stop;
10549
11705
  let sessionDbVerifyFatal = false;
10550
11706
  if (!options.restoreSessionDb) {
10551
11707
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -10595,6 +11751,13 @@ async function run(options) {
10595
11751
  for (const warning2 of opencodeStartTimeoutWarnings) {
10596
11752
  logActivity(state, { type: "info", level: "warn", message: warning2 });
10597
11753
  }
11754
+ const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
11755
+ options,
11756
+ process.env
11757
+ );
11758
+ for (const warning2 of opencodeVersionWarnings) {
11759
+ logActivity(state, { type: "info", level: "warn", message: warning2 });
11760
+ }
10598
11761
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
10599
11762
  for (const warning2 of maxActiveSessionsWarnings) {
10600
11763
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -10602,7 +11765,14 @@ async function run(options) {
10602
11765
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
10603
11766
  const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
10604
11767
  try {
10605
- const oc = await ensureOpenCodeRunning({
11768
+ const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
11769
+ port: state.port,
11770
+ interactive: state.interactive,
11771
+ agentId: state.agentId,
11772
+ log: (message) => log2(state, message),
11773
+ startTimeoutMs: opencodeStartTimeoutMs,
11774
+ inheritStdio: Boolean(options.opencodePidFile)
11775
+ }) : await ensureOpenCodeRunning({
10606
11776
  port: state.port,
10607
11777
  interactive: state.interactive,
10608
11778
  agentId: state.agentId,
@@ -10629,7 +11799,7 @@ async function run(options) {
10629
11799
  const provenance = checkSessionDbProvenance({
10630
11800
  dbPath: sessionDbPath(),
10631
11801
  currentVersion: state.opencodeVersion,
10632
- homeDir: homedir5(),
11802
+ homeDir: homedir6(),
10633
11803
  env: process.env
10634
11804
  });
10635
11805
  if (provenance.anomaly) {
@@ -10656,6 +11826,7 @@ async function run(options) {
10656
11826
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
10657
11827
  }
10658
11828
  }
11829
+ await reloadProviderCache(state.port);
10659
11830
  const noProviderWarning = buildNoProviderWarning(
10660
11831
  await hasAnyConfiguredProvider(state.port)
10661
11832
  );
@@ -10664,10 +11835,10 @@ async function run(options) {
10664
11835
  if (state.interactive && !state.json) {
10665
11836
  logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
10666
11837
  blank();
10667
- console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
11838
+ console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
10668
11839
  console.log(
10669
- chalk6.dim(
10670
- `Run ${chalk6.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
11840
+ chalk7.dim(
11841
+ `Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
10671
11842
  )
10672
11843
  );
10673
11844
  blank();
@@ -10791,7 +11962,7 @@ async function run(options) {
10791
11962
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
10792
11963
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
10793
11964
  fileSyncDirectories,
10794
- homeDir: homedir5(),
11965
+ homeDir: homedir6(),
10795
11966
  maxActiveSessions,
10796
11967
  log: (entry) => (
10797
11968
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -10917,6 +12088,18 @@ async function run(options) {
10917
12088
  if (state.interactive) displayStatus(state);
10918
12089
  });
10919
12090
  },
12091
+ // Both loops are rearmed because `rearm()` is idempotent for the
12092
+ // provider that did not just connect, and is a no-op when reporting is off.
12093
+ onUsageRearmPing: () => {
12094
+ if (!state.running) return;
12095
+ logActivity(state, {
12096
+ type: "info",
12097
+ level: "debug",
12098
+ message: "Usage rearm ping received"
12099
+ });
12100
+ state.claudeUsageRearm?.();
12101
+ state.openaiUsageRearm?.();
12102
+ },
10920
12103
  onInfo: (message) => logActivity(state, { type: "info", message })
10921
12104
  }
10922
12105
  });
@@ -11021,6 +12204,9 @@ program.command("run").description("Connect to Evident and process messages").op
11021
12204
  ).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(
11022
12205
  "--opencode-start-timeout <seconds>",
11023
12206
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
12207
+ ).option(
12208
+ "--opencode-version <v1|v2>",
12209
+ "Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
11024
12210
  ).option("--json", "Output in JSON format").option(
11025
12211
  "--session-cleanup-max-age <duration>",
11026
12212
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -11089,6 +12275,7 @@ program.command("run").description("Connect to Evident and process messages").op
11089
12275
  // Raw string — validation/precedence is single-sourced in run.ts's
11090
12276
  // resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
11091
12277
  opencodeStartTimeout: options.opencodeStartTimeout,
12278
+ opencodeVersion: options.opencodeVersion,
11092
12279
  json: options.json,
11093
12280
  // Raw strings — the resolver in run.ts single-sources parsing (M1).
11094
12281
  sessionCleanupMaxAge: options.sessionCleanupMaxAge,