@evident-ai/cli 3.4.1-dev.fb713ba → 3.4.1-dev.fb92ede

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;
@@ -764,59 +739,25 @@ function toReportedOpenAiWindow(window) {
764
739
  };
765
740
  }
766
741
  async function reportOpenAiUsage(agentId, authHeader, snapshot) {
767
- try {
768
- const apiUrl = getApiUrlConfig();
769
- const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
770
- method: "POST",
771
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
772
- body: JSON.stringify({
773
- primary: toReportedOpenAiWindow(snapshot.primary),
774
- secondary: toReportedOpenAiWindow(snapshot.secondary),
775
- has_credits: snapshot.hasCredits,
776
- credits_unlimited: snapshot.creditsUnlimited
777
- }),
778
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
779
- });
780
- if (!response.ok) {
781
- const serverMessage = await readErrorMessage(response);
782
- return {
783
- ok: false,
784
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
785
- };
786
- }
787
- return { ok: true };
788
- } catch (error2) {
789
- return { ok: false, error: describeBestEffortError(error2) };
790
- }
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
+ });
791
749
  }
792
750
  async function reportResourceUsage(agentId, authHeader, usage) {
793
- try {
794
- const apiUrl = getApiUrlConfig();
795
- const response = await fetch(`${apiUrl}/runners/${agentId}/resource-usage`, {
796
- method: "POST",
797
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
798
- body: JSON.stringify({
799
- cpu_percent: usage.cpuPercent,
800
- cpu_count: usage.cpuCount,
801
- memory_total_bytes: usage.memoryTotalBytes,
802
- memory_available_bytes: usage.memoryAvailableBytes,
803
- disk_total_bytes: usage.diskTotalBytes,
804
- disk_free_bytes: usage.diskFreeBytes,
805
- opencode_db_bytes: usage.opencodeDbBytes
806
- }),
807
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
808
- });
809
- if (!response.ok) {
810
- const serverMessage = await readErrorMessage(response);
811
- return {
812
- ok: false,
813
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
814
- };
815
- }
816
- return { ok: true };
817
- } catch (error2) {
818
- return { ok: false, error: describeBestEffortError(error2) };
819
- }
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
+ });
820
761
  }
821
762
  async function getAgentInfo(agentId, authHeader) {
822
763
  const apiUrl = getApiUrlConfig();
@@ -868,13 +809,6 @@ function authLabelFor(credentials2) {
868
809
  }
869
810
  return "user token";
870
811
  }
871
- function describeFetchError(error2) {
872
- const name = error2?.name;
873
- if (name === "TimeoutError" || name === "AbortError") {
874
- return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
875
- }
876
- return error2 instanceof Error ? error2.message : String(error2);
877
- }
878
812
  async function checkStatus(jsonMode) {
879
813
  const apiUrl = getApiUrlConfig();
880
814
  const credentials2 = await getAuthCredentials();
@@ -902,7 +836,7 @@ async function checkStatus(jsonMode) {
902
836
  endpoint: apiUrl,
903
837
  authLabel: authLabelFor(credentials2),
904
838
  reason: "unreachable",
905
- 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.`,
906
840
  exitCode: 75
907
841
  };
908
842
  }
@@ -1000,10 +934,10 @@ async function status(options = {}) {
1000
934
  }
1001
935
 
1002
936
  // src/lib/claude-usage.ts
1003
- import { execFileSync } from "child_process";
1004
- import { readFileSync } from "fs";
1005
- import { homedir } from "os";
1006
- import { join } from "path";
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";
1007
941
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1008
942
  var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
1009
943
  var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
@@ -1088,7 +1022,7 @@ function ownerLookupFailure(error2) {
1088
1022
  }
1089
1023
  async function getClaudeUsageOwner(accessToken) {
1090
1024
  if (cachedOwner?.accessToken === accessToken) {
1091
- return { owner: cachedOwner.owner, ownerLookupError: null };
1025
+ return { subscription: cachedOwner.owner, ownerLookupError: null };
1092
1026
  }
1093
1027
  try {
1094
1028
  const response = await fetch(CLAUDE_PROFILE_URL, {
@@ -1100,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
1100
1034
  signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1101
1035
  });
1102
1036
  if (!response.ok) {
1103
- return { owner: null, ownerLookupError: `HTTP ${response.status}` };
1037
+ return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
1104
1038
  }
1105
1039
  let body;
1106
1040
  try {
1107
1041
  body = await response.json();
1108
1042
  } catch (error2) {
1109
- return { owner: null, ownerLookupError: "malformed response" };
1043
+ return { subscription: null, ownerLookupError: "malformed response" };
1110
1044
  }
1111
1045
  const profile = body;
1112
1046
  if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
1113
- return { owner: null, ownerLookupError: "malformed response" };
1047
+ return { subscription: null, ownerLookupError: "malformed response" };
1114
1048
  }
1115
- const owner = {
1116
- email: profile.account.email,
1049
+ const subscription = {
1050
+ ownerEmail: profile.account.email,
1117
1051
  organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
1118
- rateLimitTier: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1052
+ planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1119
1053
  };
1120
- cachedOwner = { accessToken, owner };
1121
- return { owner, ownerLookupError: null };
1054
+ cachedOwner = { accessToken, owner: subscription };
1055
+ return { subscription, ownerLookupError: null };
1122
1056
  } catch (error2) {
1123
- return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
1057
+ return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
1124
1058
  }
1125
1059
  }
1126
1060
  async function getClaudeUsage() {
@@ -1149,11 +1083,11 @@ async function getClaudeUsage() {
1149
1083
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1150
1084
  }
1151
1085
  const body = await res.json();
1152
- const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1086
+ const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1153
1087
  return {
1154
1088
  fiveHour: toWindow(body.five_hour),
1155
1089
  sevenDay: toWindow(body.seven_day),
1156
- owner,
1090
+ subscription,
1157
1091
  ownerLookupError
1158
1092
  };
1159
1093
  }
@@ -1183,10 +1117,10 @@ async function claudeUsage() {
1183
1117
  }
1184
1118
 
1185
1119
  // src/commands/run.ts
1186
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1187
- import { homedir as homedir5 } from "os";
1188
- import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
1189
- import chalk6 from "chalk";
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";
1190
1124
 
1191
1125
  // ../../packages/types/src/agents/index.ts
1192
1126
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
@@ -1207,6 +1141,7 @@ var TelemetryEventTypes = {
1207
1141
  // ../../packages/types/src/tunnel/index.ts
1208
1142
  var MAX_FRAME_BYTES = 256 * 1024;
1209
1143
  var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
1144
+ var TUNNEL_USAGE_REARM_PING_PATH = "/__evident/usage-rearm";
1210
1145
 
1211
1146
  // ../../packages/types/src/runner-files.ts
1212
1147
  var MAX_FILE_PUSH_BYTES = 64 * 1024;
@@ -1244,7 +1179,7 @@ function stripQuery(url) {
1244
1179
 
1245
1180
  // src/commands/run.ts
1246
1181
  import ora3 from "ora";
1247
- import { select as select3 } from "@inquirer/prompts";
1182
+ import { select as select4 } from "@inquirer/prompts";
1248
1183
 
1249
1184
  // src/lib/telemetry.ts
1250
1185
  var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
@@ -1417,12 +1352,50 @@ var SEVERITY_BY_LEVEL = {
1417
1352
  warn: "warning",
1418
1353
  error: "error"
1419
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
+ }
1420
1393
  var MAX_MESSAGE_LENGTH = 500;
1421
1394
  var MAX_METADATA_VALUE_LENGTH = 200;
1422
1395
  var MAX_METADATA_ENTRIES = 20;
1423
1396
  var TRUNCATION_MARKER = "\u2026";
1424
1397
  function redact(message) {
1425
- return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
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>");
1426
1399
  }
1427
1400
  function truncate(message) {
1428
1401
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
@@ -1448,43 +1421,47 @@ function sanitiseMetadata(metadata) {
1448
1421
  }
1449
1422
  var RATE_LIMIT_WINDOW_MS = 6e4;
1450
1423
  var RATE_LIMIT_MAX_EVENTS = 30;
1451
- var windowStartedAt = 0;
1452
- var windowCount = 0;
1453
- var windowDroppedCount = 0;
1454
- function admitUnderRateLimit(now) {
1455
- if (now - windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1456
- if (windowDroppedCount > 0) {
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) {
1457
1433
  console.error(
1458
- `[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ${windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`
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}"`
1459
1435
  );
1460
1436
  }
1461
- windowStartedAt = now;
1462
- windowCount = 0;
1463
- windowDroppedCount = 0;
1437
+ window.windowStartedAt = now;
1438
+ window.windowCount = 0;
1439
+ window.windowDroppedCount = 0;
1464
1440
  }
1465
- if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
1466
- windowDroppedCount++;
1467
- if (windowDroppedCount === 1) {
1441
+ if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1442
+ window.windowDroppedCount++;
1443
+ if (window.windowDroppedCount === 1) {
1468
1444
  console.error(
1469
- `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
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}"`
1470
1446
  );
1471
1447
  }
1472
1448
  return false;
1473
1449
  }
1474
- windowCount++;
1450
+ window.windowCount++;
1475
1451
  return true;
1476
1452
  }
1477
1453
  function forwardRunnerActivity(entry, context) {
1478
1454
  try {
1479
1455
  if (!FORWARDED_LEVELS.has(entry.level)) return;
1480
1456
  if (!context.agentId || !context.authHeader) return;
1481
- if (!admitUnderRateLimit(Date.now())) return;
1457
+ const source = entry.source ?? "cli.run";
1458
+ if (!admitUnderRateLimit(source, Date.now())) return;
1482
1459
  const rawMessage = entry.error ?? entry.message ?? "";
1483
1460
  const message = truncate(redact(rawMessage));
1484
1461
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1485
1462
  severity: SEVERITY_BY_LEVEL[entry.level],
1486
1463
  message,
1487
- metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
1464
+ metadata: { ...sanitiseMetadata(entry.metadata), source },
1488
1465
  agentId: context.agentId
1489
1466
  });
1490
1467
  } catch (err) {
@@ -1495,8 +1472,8 @@ function forwardRunnerActivity(entry, context) {
1495
1472
  }
1496
1473
 
1497
1474
  // src/lib/opencode/session-db-recovery-report.ts
1498
- import { readFileSync as readFileSync2, unlinkSync } from "fs";
1499
- import { join as join2 } from "path";
1475
+ import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
1476
+ import { join as join2 } from "node:path";
1500
1477
  function sessionDbRecoveryReportPath(homeDir, env) {
1501
1478
  const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1502
1479
  return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
@@ -1709,13 +1686,13 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1709
1686
  }
1710
1687
 
1711
1688
  // src/lib/opencode/session-db-boot.ts
1712
- import { spawn as spawn2 } from "child_process";
1713
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1714
- import { homedir as homedir2 } from "os";
1715
- import { dirname as dirname2, resolve as resolvePath } from "path";
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";
1716
1693
 
1717
1694
  // src/lib/runner-synchroniser.ts
1718
- import { spawn } from "child_process";
1695
+ import { spawn } from "node:child_process";
1719
1696
  function appendError(stderr, error2) {
1720
1697
  const message = error2 instanceof Error ? error2.message : String(error2);
1721
1698
  return stderr === "" ? message : `${stderr}
@@ -1728,10 +1705,14 @@ function runSynchroniser(args, opts) {
1728
1705
  let stderr = "";
1729
1706
  let settled = false;
1730
1707
  const timer = {};
1708
+ let abortListener;
1709
+ let spawnListener;
1731
1710
  const finish = (result) => {
1732
1711
  if (settled) return;
1733
1712
  settled = true;
1734
1713
  if (timer.handle) clearTimeout(timer.handle);
1714
+ if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
1715
+ if (spawnListener) child.removeListener("spawn", spawnListener);
1735
1716
  resolve4(result);
1736
1717
  };
1737
1718
  try {
@@ -1757,6 +1738,25 @@ function runSynchroniser(args, opts) {
1757
1738
  child.once("close", (code) => {
1758
1739
  finish({ code, stdout, stderr, timedOut: false });
1759
1740
  });
1741
+ if (opts.signal) {
1742
+ const killChild = () => {
1743
+ if (child.pid === void 0) {
1744
+ if (!spawnListener) {
1745
+ spawnListener = killChild;
1746
+ child.once("spawn", spawnListener);
1747
+ }
1748
+ return;
1749
+ }
1750
+ child.kill("SIGKILL");
1751
+ };
1752
+ abortListener = killChild;
1753
+ if (opts.signal.aborted) {
1754
+ abortListener();
1755
+ } else {
1756
+ opts.signal.addEventListener("abort", abortListener, { once: true });
1757
+ if (opts.signal.aborted) abortListener();
1758
+ }
1759
+ }
1760
1760
  timer.handle = setTimeout(
1761
1761
  () => {
1762
1762
  child.kill("SIGKILL");
@@ -2217,9 +2217,9 @@ async function restoreAndVerifySessionDb(options) {
2217
2217
  }
2218
2218
 
2219
2219
  // src/lib/opencode/session-db-provenance.ts
2220
- import { createRequire } from "module";
2221
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2222
- import { dirname as dirname3, join as join3 } from "path";
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";
2223
2223
  var require2 = createRequire(import.meta.url);
2224
2224
  function readSessionDbMigrationIds(dbPath) {
2225
2225
  let db;
@@ -2413,6 +2413,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2413
2413
 
2414
2414
  // src/lib/opencode/process.ts
2415
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
+ }
2416
2427
  function getProcessCwd(pid) {
2417
2428
  const platform = process.platform;
2418
2429
  try {
@@ -2461,14 +2472,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
2461
2472
  }
2462
2473
  return null;
2463
2474
  }
2464
- function findOpenCodeProcesses() {
2475
+ function findProcessesByPattern(pgrepPattern, psPattern) {
2465
2476
  const instances = [];
2466
2477
  try {
2467
2478
  const platform = process.platform;
2468
2479
  if (platform === "darwin" || platform === "linux") {
2469
2480
  let pids = [];
2470
2481
  try {
2471
- const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
2482
+ const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
2472
2483
  encoding: "utf-8",
2473
2484
  stdio: ["pipe", "pipe", "pipe"]
2474
2485
  }).trim();
@@ -2477,7 +2488,7 @@ function findOpenCodeProcesses() {
2477
2488
  }
2478
2489
  } catch {
2479
2490
  try {
2480
- 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`, {
2481
2492
  encoding: "utf-8",
2482
2493
  stdio: ["pipe", "pipe", "pipe"]
2483
2494
  }).trim();
@@ -2523,6 +2534,9 @@ function findOpenCodeProcesses() {
2523
2534
  }
2524
2535
  return instances;
2525
2536
  }
2537
+ function findOpenCodeProcesses() {
2538
+ return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
2539
+ }
2526
2540
  async function scanPortsForOpenCode() {
2527
2541
  const instances = [];
2528
2542
  const checks = OPENCODE_PORT_RANGE.map(async (port) => {
@@ -2569,7 +2583,7 @@ async function findHealthyOpenCodeInstances() {
2569
2583
  }
2570
2584
  async function startOpenCode(port, options = {}) {
2571
2585
  let command = "opencode";
2572
- const printLogs = options.inheritStdio ? ["--print-logs"] : [];
2586
+ const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
2573
2587
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
2574
2588
  try {
2575
2589
  execSync("which opencode", { stdio: "ignore" });
@@ -2626,6 +2640,19 @@ function isOpenCodeInstalled() {
2626
2640
  return false;
2627
2641
  }
2628
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
+ }
2629
2656
  async function promptOpenCodeInstall(interactive) {
2630
2657
  if (!interactive) {
2631
2658
  console.log(
@@ -2635,7 +2662,11 @@ async function promptOpenCodeInstall(interactive) {
2635
2662
  install_url: OPENCODE_INSTALL_URL,
2636
2663
  install_commands: {
2637
2664
  npm: "npm install -g opencode-ai",
2638
- 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
+ }
2639
2670
  }
2640
2671
  })
2641
2672
  );
@@ -3119,21 +3150,112 @@ function findLastAssistantReplyFor(messages, userMessageId) {
3119
3150
  }
3120
3151
  return lastOk ?? last;
3121
3152
  }
3122
- function messageUsage(messages, userMessageId) {
3123
- if (!messages || messages.length === 0) return null;
3124
- const byParentAll = messages.filter(
3125
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
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
3126
3157
  );
3127
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3128
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3129
- let correlated;
3130
- if (byParent.length > 0) {
3131
- correlated = byParent;
3132
- } else {
3133
- const reply = findAssistantReplyAfter(messages, userMessageId);
3134
- correlated = reply ? [reply] : [];
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
+ }
3135
3188
  }
3136
- 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;
3137
3259
  let sawAnyUsage = false;
3138
3260
  let inputSum = 0;
3139
3261
  let outputSum = 0;
@@ -3144,7 +3266,7 @@ function messageUsage(messages, userMessageId) {
3144
3266
  let sawCost = false;
3145
3267
  let modelId = null;
3146
3268
  let providerId = null;
3147
- for (const m of correlated) {
3269
+ for (const m of selected) {
3148
3270
  const info = m.info;
3149
3271
  if (!info) continue;
3150
3272
  const tokens = info.tokens;
@@ -3179,12 +3301,28 @@ function messageUsage(messages, userMessageId) {
3179
3301
  usage_tokens_reasoning: reasoningSum,
3180
3302
  usage_tokens_cache_read: cacheReadSum,
3181
3303
  usage_tokens_cache_write: cacheWriteSum,
3182
- // NULL means "OpenCode never reported a cost" (never inferred from
3183
- // tokens) distinct from a genuine 0-cost turn, which would set
3184
- // `sawCost` true with `costSum === 0`.
3304
+ // NULL means OpenCode never reported a cost; it is distinct from a genuine
3305
+ // zero-cost message, which sets `sawCost` with `costSum === 0`.
3185
3306
  usage_cost_usd: sawCost ? costSum : null
3186
3307
  };
3187
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
+ }
3188
3326
  function messageRunState(messages, userMessageId) {
3189
3327
  if (!messages || messages.length === 0) return "unknown";
3190
3328
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3247,8 +3385,7 @@ function isAbortedTerminalReply(messages, userMessageId) {
3247
3385
  }
3248
3386
  return false;
3249
3387
  }
3250
- function messageFailure(messages, userMessageId) {
3251
- const reply = findLastAssistantReplyFor(messages, userMessageId);
3388
+ function classifyReplyAuthError(reply) {
3252
3389
  const error2 = errorOf(reply);
3253
3390
  if (error2 == null || typeof error2 !== "object") return null;
3254
3391
  const e = error2;
@@ -3273,6 +3410,32 @@ function messageFailure(messages, userMessageId) {
3273
3410
  }
3274
3411
  return null;
3275
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
+ }
3276
3439
  function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
3277
3440
  if (classified != null) return classified;
3278
3441
  if (hasConfiguredProvider !== false) return null;
@@ -3289,6 +3452,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3289
3452
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3290
3453
  );
3291
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
+ }
3292
3477
  async function hasAnyConfiguredProvider(port) {
3293
3478
  try {
3294
3479
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
@@ -3320,6 +3505,94 @@ async function hasAnyConfiguredProvider(port) {
3320
3505
  return null;
3321
3506
  }
3322
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
+ }
3323
3596
 
3324
3597
  // src/lib/opencode/session-cleanup.ts
3325
3598
  var DURATION_UNIT_MS = {
@@ -3426,8 +3699,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
3426
3699
  }
3427
3700
 
3428
3701
  // src/lib/opencode/session-db-size.ts
3429
- import { statSync as statSync3 } from "fs";
3430
- import { join as join4 } from "path";
3702
+ import { statSync as statSync3 } from "node:fs";
3703
+ import { join as join4 } from "node:path";
3431
3704
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
3432
3705
  function statSessionDbBytes(homeDir) {
3433
3706
  const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
@@ -3457,9 +3730,96 @@ function buildSessionStoreSizeWarning(input) {
3457
3730
  return null;
3458
3731
  }
3459
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
+
3460
3816
  // src/lib/opencode/session-db-reclaim.ts
3461
- import { statSync as statSync4, statfsSync } from "fs";
3462
- 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
+ }
3463
3823
  function insufficientSpaceReason(dbPath, requiredBytes) {
3464
3824
  try {
3465
3825
  const fsStats = statfsSync(dirname4(dbPath));
@@ -3485,17 +3845,17 @@ async function probeReclaimAvailability(input) {
3485
3845
  const { dbPath, requiredBytes } = input;
3486
3846
  let sqlite;
3487
3847
  try {
3488
- sqlite = await import("sqlite");
3848
+ sqlite = await import("node:sqlite");
3489
3849
  } catch (err) {
3490
- console.warn(
3491
- `[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3492
- );
3493
- 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 };
3494
3853
  }
3495
3854
  let autoVacuum = null;
3496
3855
  try {
3497
3856
  const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
3498
3857
  try {
3858
+ db.exec("PRAGMA busy_timeout=5000");
3499
3859
  autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3500
3860
  } finally {
3501
3861
  db.close();
@@ -3506,23 +3866,25 @@ async function probeReclaimAvailability(input) {
3506
3866
  );
3507
3867
  }
3508
3868
  if (autoVacuum !== 0) return null;
3509
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
3869
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
3510
3870
  }
3511
3871
  async function reclaimSessionDbSpace(input) {
3512
3872
  const { dbPath, maxPages, allowFullVacuum = true } = input;
3513
3873
  let sqlite;
3514
3874
  try {
3515
- sqlite = await import("sqlite");
3875
+ sqlite = await import("node:sqlite");
3516
3876
  } catch (err) {
3877
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
3517
3878
  console.warn(
3518
- `[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}`
3519
3880
  );
3520
- return { ok: false, skipped: "sqlite-unavailable" };
3881
+ return { ok: false, skipped: "sqlite-unavailable", detail };
3521
3882
  }
3522
3883
  const { DatabaseSync } = sqlite;
3523
3884
  let db;
3524
3885
  try {
3525
3886
  db = new DatabaseSync(dbPath);
3887
+ db.exec("PRAGMA busy_timeout=5000");
3526
3888
  const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3527
3889
  if (autoVacuum === 0) {
3528
3890
  if (!allowFullVacuum) {
@@ -3531,7 +3893,7 @@ async function reclaimSessionDbSpace(input) {
3531
3893
  );
3532
3894
  return { ok: false, skipped: "full-vacuum-blocked" };
3533
3895
  }
3534
- const fileBytesForGuard = statSync4(dbPath).size;
3896
+ const fileBytesForGuard = statSync5(dbPath).size;
3535
3897
  const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
3536
3898
  if (skipReason !== null) {
3537
3899
  console.warn(
@@ -3559,10 +3921,12 @@ async function reclaimSessionDbSpace(input) {
3559
3921
  );
3560
3922
  return { ok: false, skipped: "auto-vacuum-not-applicable" };
3561
3923
  } catch (err) {
3562
- console.error(
3563
- `[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3564
- );
3565
- return { ok: false, skipped: "reclaim-error" };
3924
+ console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
3925
+ return {
3926
+ ok: false,
3927
+ skipped: "reclaim-error",
3928
+ detail: errorMessage(err)
3929
+ };
3566
3930
  } finally {
3567
3931
  db?.close();
3568
3932
  }
@@ -3603,7 +3967,6 @@ var StreamForwarder = class {
3603
3967
  handleFrame(frame) {
3604
3968
  switch (frame.type) {
3605
3969
  case "open":
3606
- this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
3607
3970
  void this.handleOpen(frame);
3608
3971
  break;
3609
3972
  case "req_data":
@@ -3639,12 +4002,21 @@ var StreamForwarder = class {
3639
4002
  const { sid, method, path, headers, has_body } = frame;
3640
4003
  const correlationId = headers?.[CORRELATION_ID_HEADER];
3641
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
+ }
3642
4008
  if (path === TUNNEL_DRAIN_PING_PATH) {
3643
4009
  this.callbacks.onDrainPing?.();
3644
4010
  this.send({ type: "head", sid, status: 204, headers: {} });
3645
4011
  this.send({ type: "res_end", sid });
3646
4012
  return;
3647
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
+ }
3648
4020
  if (process.env.DEBUG) {
3649
4021
  log("debug", "agent_request", {
3650
4022
  correlation_id: correlationId,
@@ -3789,7 +4161,8 @@ function connectTunnel(options) {
3789
4161
  onResponse,
3790
4162
  onInfo,
3791
4163
  onWarning,
3792
- onDrainPing
4164
+ onDrainPing,
4165
+ onUsageRearmPing
3793
4166
  } = options;
3794
4167
  const tunnelUrl = getTunnelUrlConfig();
3795
4168
  const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
@@ -3801,7 +4174,8 @@ function connectTunnel(options) {
3801
4174
  });
3802
4175
  const forwarder = new StreamForwarder(ws, port, {
3803
4176
  onHead: () => onResponse?.(),
3804
- onDrainPing: () => onDrainPing?.()
4177
+ onDrainPing: () => onDrainPing?.(),
4178
+ onUsageRearmPing: () => onUsageRearmPing?.()
3805
4179
  });
3806
4180
  const connectionTimeout = setTimeout(() => {
3807
4181
  ws.close();
@@ -3844,8 +4218,8 @@ function connectTunnel(options) {
3844
4218
  try {
3845
4219
  message = JSON.parse(data.toString());
3846
4220
  } catch (error2) {
3847
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
3848
- onError?.(`Failed to handle message: ${errorMessage}`);
4221
+ const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
4222
+ onError?.(`Failed to handle message: ${errorMessage3}`);
3849
4223
  return;
3850
4224
  }
3851
4225
  if (isStreamFrame(message)) {
@@ -3962,6 +4336,7 @@ var RunnerConnection = class {
3962
4336
  onError: (error2) => events.onError?.(error2),
3963
4337
  onResponse: () => events.onResponse?.(),
3964
4338
  onDrainPing: () => events.onDrainPing?.(),
4339
+ onUsageRearmPing: () => events.onUsageRearmPing?.(),
3965
4340
  onInfo: (message) => events.onInfo?.(message),
3966
4341
  onWarning: (message) => events.onWarning?.(message)
3967
4342
  });
@@ -3988,7 +4363,7 @@ var RunnerConnection = class {
3988
4363
  };
3989
4364
 
3990
4365
  // src/lib/tunnel/ready-marker.ts
3991
- import { writeFileSync as writeFileSync3 } from "fs";
4366
+ import { writeFileSync as writeFileSync3 } from "node:fs";
3992
4367
  function writeTunnelReadyMarker(path, agentId) {
3993
4368
  try {
3994
4369
  writeFileSync3(path, `${agentId}
@@ -4000,7 +4375,7 @@ function writeTunnelReadyMarker(path, agentId) {
4000
4375
  }
4001
4376
 
4002
4377
  // src/lib/replication.ts
4003
- import { spawn as spawn4 } from "child_process";
4378
+ import { spawn as spawn4 } from "node:child_process";
4004
4379
  function startSessionDbReplication(configPath) {
4005
4380
  return spawn4("litestream", ["replicate", "-config", configPath], {
4006
4381
  stdio: "inherit"
@@ -4016,7 +4391,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
4016
4391
  }
4017
4392
 
4018
4393
  // src/lib/process-liveness.ts
4019
- import { readFileSync as readFileSync4 } from "fs";
4394
+ import { readFileSync as readFileSync4 } from "node:fs";
4020
4395
  function isProcessAlive(pid) {
4021
4396
  try {
4022
4397
  process.kill(pid, 0);
@@ -4042,9 +4417,9 @@ function isProcessAlive(pid) {
4042
4417
  }
4043
4418
 
4044
4419
  // src/lib/openai-usage.ts
4045
- import { readFileSync as readFileSync5 } from "fs";
4046
- import { homedir as homedir3 } from "os";
4047
- import { join as join5 } from "path";
4420
+ import { readFileSync as readFileSync5 } from "node:fs";
4421
+ import { homedir as homedir4 } from "node:os";
4422
+ import { join as join6 } from "node:path";
4048
4423
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4049
4424
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4050
4425
  var OpenAiUsageError = class extends Error {
@@ -4058,7 +4433,7 @@ function isLocalCredentialProblem2(err) {
4058
4433
  }
4059
4434
  function readOpenCodeChatGptCredentials() {
4060
4435
  try {
4061
- const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4436
+ const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4062
4437
  let parsed;
4063
4438
  try {
4064
4439
  parsed = JSON.parse(raw);
@@ -4080,6 +4455,23 @@ function readOpenCodeChatGptCredentials() {
4080
4455
  return null;
4081
4456
  }
4082
4457
  }
4458
+ function parseChatGptIdentity(accessToken) {
4459
+ const segments = accessToken.split(".");
4460
+ if (segments.length !== 3) return null;
4461
+ let payload;
4462
+ try {
4463
+ const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
4464
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
4465
+ payload = parsed;
4466
+ } catch {
4467
+ return null;
4468
+ }
4469
+ const profile = payload["https://api.openai.com/profile"];
4470
+ const auth = payload["https://api.openai.com/auth"];
4471
+ const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4472
+ const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4473
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
4474
+ }
4083
4475
  function toWindow2(headers, name) {
4084
4476
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
4085
4477
  const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
@@ -4155,6 +4547,7 @@ async function getOpenAiUsage(port) {
4155
4547
  "credentials_expired"
4156
4548
  );
4157
4549
  }
4550
+ const subscription = parseChatGptIdentity(credentials2.accessToken);
4158
4551
  const models = await resolveProbeModels(port);
4159
4552
  if (models.length === 0) {
4160
4553
  throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
@@ -4187,7 +4580,7 @@ async function getOpenAiUsage(port) {
4187
4580
  "no_usable_window"
4188
4581
  );
4189
4582
  }
4190
- return usage;
4583
+ return { ...usage, subscription };
4191
4584
  }
4192
4585
  if (res.status === 401) {
4193
4586
  throw new OpenAiUsageError(
@@ -4259,13 +4652,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
4259
4652
  envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
4260
4653
  });
4261
4654
  }
4262
- function nextReportDelayMs(random = Math.random) {
4263
- return usageReportDelayMs(random);
4264
- }
4265
- var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
4266
- function claudeUsageFailureLogLevel(consecutiveFailures) {
4267
- return usageReportFailureLogLevel(consecutiveFailures);
4268
- }
4269
4655
 
4270
4656
  // src/lib/openai-usage-reporting.ts
4271
4657
  function resolveOpenAiUsageReportingMode(flagValue, env) {
@@ -4302,8 +4688,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
4302
4688
  }
4303
4689
 
4304
4690
  // src/lib/resource-usage.ts
4305
- import { cpus, totalmem, freemem } from "os";
4306
- import { statfsSync as statfsSync2 } from "fs";
4691
+ import { cpus, totalmem, freemem } from "node:os";
4692
+ import { statfsSync as statfsSync2 } from "node:fs";
4307
4693
 
4308
4694
  // src/lib/ecs-task-metadata.ts
4309
4695
  var ECS_METADATA_TIMEOUT_MS = 2e3;
@@ -4388,58 +4774,97 @@ function readDisk(homeDir) {
4388
4774
  };
4389
4775
  }
4390
4776
  }
4391
- function createResourceUsageCollector(homeDir) {
4392
- let previous = readCpuSample();
4393
- 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(() => {
4394
4787
  const current = readCpuSample();
4395
- const hostCpuPercent = cpuPercentBetween(previous, current);
4396
- const hostCpuCount = cpus().length;
4397
- previous = current;
4398
- const disk = readDisk(homeDir);
4399
- const opencodeDbBytes = statSessionDbBytes(homeDir);
4400
- const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
4401
- const warnings = [];
4402
- if (disk.warning) warnings.push(disk.warning);
4403
- if (ecsWarning) warnings.push(ecsWarning);
4404
- let cpuPercent = hostCpuPercent;
4405
- let cpuCount = hostCpuCount;
4406
- let memoryTotalBytes = totalmem();
4407
- let memoryAvailableBytes = freemem();
4408
- if (limits !== null) {
4409
- cpuCount = limits.cpuCount;
4410
- memoryTotalBytes = limits.memoryTotalBytes;
4411
- memoryAvailableBytes = clamp(
4412
- limits.memoryTotalBytes - (totalmem() - freemem()),
4413
- 0,
4414
- limits.memoryTotalBytes
4415
- );
4416
- cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
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
+ }
4417
4794
  }
4418
- return {
4419
- usage: {
4420
- cpuPercent,
4421
- cpuCount,
4422
- memoryTotalBytes,
4423
- memoryAvailableBytes,
4424
- diskTotalBytes: disk.totalBytes,
4425
- diskFreeBytes: disk.freeBytes,
4426
- opencodeDbBytes
4427
- },
4428
- warnings
4429
- };
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
4430
4855
  };
4431
4856
  }
4432
4857
 
4433
4858
  // src/lib/channels/driver.ts
4434
- import { homedir as homedir4 } from "os";
4859
+ import { homedir as homedir5 } from "node:os";
4435
4860
 
4436
4861
  // src/lib/runner-file-sync.ts
4437
- import { join as join7 } from "path";
4862
+ import { join as join8 } from "node:path";
4438
4863
 
4439
4864
  // src/lib/file-push.ts
4440
- import { randomUUID } from "crypto";
4441
- import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
4442
- import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
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";
4443
4868
  var FILE_MODE = 384;
4444
4869
  var DIRECTORY_MODE = 448;
4445
4870
  async function writePushedFile(request) {
@@ -4472,7 +4897,7 @@ async function writePushedFile(request) {
4472
4897
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
4473
4898
  dirname5(candidate)
4474
4899
  );
4475
- const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
4900
+ const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
4476
4901
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
4477
4902
  if (allowedDirectory === null) {
4478
4903
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -4508,7 +4933,7 @@ function expandAndValidate(requestedPath, homeDir) {
4508
4933
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
4509
4934
  return null;
4510
4935
  }
4511
- 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;
4512
4937
  if (expanded.split(/[/\\]/).includes("..")) {
4513
4938
  return null;
4514
4939
  }
@@ -4581,16 +5006,16 @@ function contains(realDirectory, realTarget) {
4581
5006
  async function createMissingDirectories(existingAncestor, missingSegments) {
4582
5007
  let current = existingAncestor;
4583
5008
  for (const segment of missingSegments) {
4584
- current = join6(current, segment);
5009
+ current = join7(current, segment);
4585
5010
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
4586
5011
  await chmod(current, DIRECTORY_MODE);
4587
5012
  }
4588
5013
  }
4589
5014
  async function writeAtomically(realTarget, content) {
4590
- const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
5015
+ const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
4591
5016
  let handle;
4592
5017
  try {
4593
- handle = await open2(temporaryPath, "wx", FILE_MODE);
5018
+ handle = await open3(temporaryPath, "wx", FILE_MODE);
4594
5019
  await handle.writeFile(content);
4595
5020
  await handle.chmod(FILE_MODE);
4596
5021
  await handle.close();
@@ -4717,12 +5142,12 @@ var NOT_APPLIED = {
4717
5142
  opencodeAuthApplied: false
4718
5143
  };
4719
5144
  function isClaudeCredentialPath(requestedPath, homeDir) {
4720
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4721
- return expanded === join7(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
5145
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5146
+ return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4722
5147
  }
4723
5148
  function isOpenCodeAuthPath(requestedPath, homeDir) {
4724
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4725
- return expanded === join7(homeDir, ...OPENCODE_AUTH_SEGMENTS);
5149
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5150
+ return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
4726
5151
  }
4727
5152
  async function applyOne(options, file) {
4728
5153
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -4882,6 +5307,10 @@ var DEFAULT_RETRY_POLICY = {
4882
5307
  baseDelayMs: 500,
4883
5308
  maxDelayMs: 3e4
4884
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;
4885
5314
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
4886
5315
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
4887
5316
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5022,6 +5451,17 @@ var ChannelDriver = class _ChannelDriver {
5022
5451
  * message; it is removed once its in-flight set empties.
5023
5452
  */
5024
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();
5025
5465
  /**
5026
5466
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5027
5467
  * dispatched and are still in-flight. A message in this set is never
@@ -5205,6 +5645,13 @@ var ChannelDriver = class _ChannelDriver {
5205
5645
  * no watcher) can resolve the title.
5206
5646
  */
5207
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();
5208
5655
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5209
5656
  draining = false;
5210
5657
  /**
@@ -5249,6 +5696,7 @@ var ChannelDriver = class _ChannelDriver {
5249
5696
  * and stops opencode.
5250
5697
  */
5251
5698
  stopped = false;
5699
+ recycleRequestedFlag = false;
5252
5700
  constructor(config) {
5253
5701
  this.agentId = config.agentId;
5254
5702
  this.port = config.port;
@@ -5268,7 +5716,7 @@ var ChannelDriver = class _ChannelDriver {
5268
5716
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
5269
5717
  this.now = config.now ?? (() => Date.now());
5270
5718
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
5271
- this.homeDir = config.homeDir ?? homedir4();
5719
+ this.homeDir = config.homeDir ?? homedir5();
5272
5720
  this.maxActiveSessions = config.maxActiveSessions;
5273
5721
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5274
5722
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -5354,6 +5802,9 @@ var ChannelDriver = class _ChannelDriver {
5354
5802
  let dispatched = 0;
5355
5803
  try {
5356
5804
  const conversations = await this.getPendingConversations();
5805
+ if (this.recycleRequestedFlag) {
5806
+ this.stop();
5807
+ }
5357
5808
  if (conversations.length > 0) {
5358
5809
  const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
5359
5810
  this.log({
@@ -5415,6 +5866,21 @@ var ChannelDriver = class _ChannelDriver {
5415
5866
  }
5416
5867
  return ids;
5417
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
+ }
5418
5884
  /**
5419
5885
  * File-pull work, for `run.ts`'s idle accounting (#559).
5420
5886
  *
@@ -5481,6 +5947,16 @@ var ChannelDriver = class _ChannelDriver {
5481
5947
  */
5482
5948
  stop() {
5483
5949
  this.stopped = true;
5950
+ this.sessionErrorStream?.abort.abort();
5951
+ this.sessionErrorStream = null;
5952
+ }
5953
+ /**
5954
+ * The server clears this request when a new MicroVM identity is recorded, so a
5955
+ * same-VM tunnel reconnect does not consume it. This is a plain read rather
5956
+ * than a consume; `run.ts` guards the action once-only.
5957
+ */
5958
+ get recycleRequested() {
5959
+ return this.recycleRequestedFlag;
5484
5960
  }
5485
5961
  /**
5486
5962
  * Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
@@ -5547,6 +6023,7 @@ var ChannelDriver = class _ChannelDriver {
5547
6023
  */
5548
6024
  async processConversation(conv) {
5549
6025
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6026
+ this.ensureSessionErrorStream();
5550
6027
  const messages = await this.getPendingMessages(conv.id);
5551
6028
  let dispatched = 0;
5552
6029
  let skippedAlreadyDispatched = 0;
@@ -5619,7 +6096,7 @@ var ChannelDriver = class _ChannelDriver {
5619
6096
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
5620
6097
  break;
5621
6098
  }
5622
- const errorMessage = err instanceof Error ? err.message : String(err);
6099
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
5623
6100
  this.sessions.delete(conv.id);
5624
6101
  this.supersede(conv.id, sessionId);
5625
6102
  this.log({
@@ -5628,7 +6105,7 @@ var ChannelDriver = class _ChannelDriver {
5628
6105
  conversation_id: conv.id,
5629
6106
  message_id: message.id
5630
6107
  });
5631
- await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
6108
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5632
6109
  this.log({
5633
6110
  level: "warn",
5634
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)}`,
@@ -5639,7 +6116,7 @@ var ChannelDriver = class _ChannelDriver {
5639
6116
  });
5640
6117
  this.log({
5641
6118
  level: "error",
5642
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage}`,
6119
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
5643
6120
  conversation_id: conv.id,
5644
6121
  message_id: message.id
5645
6122
  });
@@ -5660,14 +6137,14 @@ var ChannelDriver = class _ChannelDriver {
5660
6137
  this.unconfirmedDispatchFailures.delete(message.id);
5661
6138
  this.sessions.delete(conv.id);
5662
6139
  this.supersede(conv.id, sessionId);
5663
- const errorMessage = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
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.`;
5664
6141
  this.log({
5665
6142
  level: "error",
5666
- message: errorMessage,
6143
+ message: errorMessage3,
5667
6144
  conversation_id: conv.id,
5668
6145
  message_id: message.id
5669
6146
  });
5670
- await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
6147
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5671
6148
  this.log({
5672
6149
  level: "warn",
5673
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)}`,
@@ -5893,6 +6370,23 @@ var ChannelDriver = class _ChannelDriver {
5893
6370
  if (state === "running" || state === "queued") {
5894
6371
  const ongoing = await isSessionOngoing(this.port, sessionId);
5895
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
+ }
5896
6390
  return this.reattachRedrive(conv, sessionId, message, ocId);
5897
6391
  }
5898
6392
  if (ongoing === false) {
@@ -5982,16 +6476,37 @@ var ChannelDriver = class _ChannelDriver {
5982
6476
  if (state === "done") {
5983
6477
  const title = await this.resolveSessionTitle(sessionId, conv.id);
5984
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
+ );
5985
6485
  this.log({
5986
6486
  level: "info",
5987
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`,
5988
6488
  conversation_id: conv.id,
5989
6489
  message_id: message.id
5990
6490
  });
5991
- 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
+ );
5992
6501
  } else {
5993
6502
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
5994
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
+ );
5995
6510
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
5996
6511
  this.log({
5997
6512
  level: "error",
@@ -5999,7 +6514,19 @@ var ChannelDriver = class _ChannelDriver {
5999
6514
  conversation_id: conv.id,
6000
6515
  message_id: message.id
6001
6516
  });
6002
- 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);
6003
6530
  }
6004
6531
  } catch (err) {
6005
6532
  if (err instanceof ChannelAuthError) throw err;
@@ -6540,6 +7067,24 @@ var ChannelDriver = class _ChannelDriver {
6540
7067
  ambiguousPinnedSinceMs: 0,
6541
7068
  ambiguousResolved: false
6542
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
+ }
6543
7088
  }
6544
7089
  /**
6545
7090
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
@@ -6744,6 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
6744
7289
  ensureWatcherRunning(sessionId) {
6745
7290
  const watcher = this.watchers.get(sessionId);
6746
7291
  if (!watcher) return;
7292
+ this.ensureSessionErrorStream();
6747
7293
  if (watcher.loop) return;
6748
7294
  if (watcher.inFlight.size === 0) {
6749
7295
  this.watchers.delete(sessionId);
@@ -6759,6 +7305,154 @@ var ChannelDriver = class _ChannelDriver {
6759
7305
  });
6760
7306
  watcher.loop = loop;
6761
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
+ }
6762
7456
  /**
6763
7457
  * The per-session polling loop (WI-3). Once per tick it:
6764
7458
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -6871,6 +7565,21 @@ var ChannelDriver = class _ChannelDriver {
6871
7565
  const conv = watcher.conv;
6872
7566
  const state = messageRunState(messages, inFlight.opencodeMessageId);
6873
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
+ }
6874
7583
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
6875
7584
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
6876
7585
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -6924,6 +7633,12 @@ var ChannelDriver = class _ChannelDriver {
6924
7633
  message_id: inFlight.evidentMessageId
6925
7634
  });
6926
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
+ );
6927
7642
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
6928
7643
  try {
6929
7644
  await this.markFailed(
@@ -6932,7 +7647,9 @@ var ChannelDriver = class _ChannelDriver {
6932
7647
  sessionId,
6933
7648
  error2,
6934
7649
  usage,
6935
- failure
7650
+ failure,
7651
+ usageAgentName,
7652
+ subagentInvocations
6936
7653
  );
6937
7654
  } catch (err) {
6938
7655
  if (err instanceof ChannelAuthError) throw err;
@@ -6965,13 +7682,21 @@ var ChannelDriver = class _ChannelDriver {
6965
7682
  return;
6966
7683
  }
6967
7684
  inFlight.done = true;
7685
+ await this.reportSubagentAuthFailures(
7686
+ watcher.conv.id,
7687
+ inFlight.opencodeMessageId,
7688
+ inFlight.evidentMessageId,
7689
+ messages
7690
+ );
6968
7691
  }
6969
7692
  this.removeInFlight(watcher, inFlight.evidentMessageId);
6970
7693
  return;
6971
7694
  }
7695
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
7696
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
6972
7697
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
6973
7698
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
6974
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
7699
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
6975
7700
  inFlight.stuckReported = true;
6976
7701
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
6977
7702
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7132,7 +7857,7 @@ var ChannelDriver = class _ChannelDriver {
7132
7857
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7133
7858
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7134
7859
  );
7135
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
7860
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7136
7861
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7137
7862
  this.log({
7138
7863
  level: "debug",
@@ -7167,6 +7892,12 @@ var ChannelDriver = class _ChannelDriver {
7167
7892
  });
7168
7893
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7169
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
+ );
7170
7901
  try {
7171
7902
  await this.markDone(
7172
7903
  conv.id,
@@ -7174,7 +7905,9 @@ var ChannelDriver = class _ChannelDriver {
7174
7905
  sessionId,
7175
7906
  inFlight.opencodeMessageId,
7176
7907
  title,
7177
- usage
7908
+ usage,
7909
+ usageAgentName,
7910
+ subagentInvocations
7178
7911
  );
7179
7912
  } catch (err) {
7180
7913
  if (err instanceof ChannelAuthError) throw err;
@@ -7207,6 +7940,12 @@ var ChannelDriver = class _ChannelDriver {
7207
7940
  return;
7208
7941
  }
7209
7942
  inFlight.done = true;
7943
+ await this.reportSubagentAuthFailures(
7944
+ watcher.conv.id,
7945
+ inFlight.opencodeMessageId,
7946
+ inFlight.evidentMessageId,
7947
+ messages
7948
+ );
7210
7949
  }
7211
7950
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7212
7951
  }
@@ -7347,6 +8086,12 @@ var ChannelDriver = class _ChannelDriver {
7347
8086
  if (state === "failed" && !restartAborted) {
7348
8087
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7349
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
+ );
7350
8095
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7351
8096
  this.log({
7352
8097
  level: "error",
@@ -7355,7 +8100,16 @@ var ChannelDriver = class _ChannelDriver {
7355
8100
  message_id: row.id
7356
8101
  });
7357
8102
  try {
7358
- 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
+ );
7359
8113
  } catch (err) {
7360
8114
  if (err instanceof ChannelAuthError) throw err;
7361
8115
  if (err instanceof ChannelTerminalError) {
@@ -7377,6 +8131,7 @@ var ChannelDriver = class _ChannelDriver {
7377
8131
  });
7378
8132
  return;
7379
8133
  }
8134
+ await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
7380
8135
  this.dontRedispatch.delete(row.id);
7381
8136
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
7382
8137
  return;
@@ -7516,7 +8271,22 @@ var ChannelDriver = class _ChannelDriver {
7516
8271
  try {
7517
8272
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7518
8273
  const usage = messageUsage(messages, ocId ?? "");
7519
- 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
+ );
7520
8290
  } catch (err) {
7521
8291
  if (err instanceof ChannelAuthError) throw err;
7522
8292
  if (err instanceof ChannelTerminalError) {
@@ -7538,6 +8308,9 @@ var ChannelDriver = class _ChannelDriver {
7538
8308
  });
7539
8309
  return;
7540
8310
  }
8311
+ if (ocId !== null) {
8312
+ await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
8313
+ }
7541
8314
  this.dontRedispatch.delete(row.id);
7542
8315
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
7543
8316
  }
@@ -7631,14 +8404,14 @@ var ChannelDriver = class _ChannelDriver {
7631
8404
  this.unconfirmedDispatchFailures.delete(row.id);
7632
8405
  this.sessions.delete(readoptConv.id);
7633
8406
  this.supersede(readoptConv.id, sessionId);
7634
- const errorMessage = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
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.`;
7635
8408
  this.log({
7636
8409
  level: "error",
7637
- message: errorMessage,
8410
+ message: errorMessage3,
7638
8411
  conversation_id: row.conversation_id,
7639
8412
  message_id: row.id
7640
8413
  });
7641
- await this.markFailed(row.conversation_id, row.id, null, errorMessage).catch((markErr) => {
8414
+ await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
7642
8415
  this.log({
7643
8416
  level: "warn",
7644
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)}`,
@@ -7917,6 +8690,166 @@ var ChannelDriver = class _ChannelDriver {
7917
8690
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
7918
8691
  return parent;
7919
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
+ }
7920
8853
  /**
7921
8854
  * OpenCode's synchronous default session title (e.g.
7922
8855
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8253,6 +9186,7 @@ var ChannelDriver = class _ChannelDriver {
8253
9186
  throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
8254
9187
  }
8255
9188
  const data = await res.json();
9189
+ this.recycleRequestedFlag = data.recycle_requested === true;
8256
9190
  let conversations = data.conversations;
8257
9191
  if (this.conversationFilter) {
8258
9192
  conversations = conversations.filter((c) => c.id === this.conversationFilter);
@@ -8399,7 +9333,7 @@ var ChannelDriver = class _ChannelDriver {
8399
9333
  * watcher retries next tick within the
8400
9334
  * deadline, Finding 4).
8401
9335
  */
8402
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
9336
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8403
9337
  const res = await this.fetchImpl(
8404
9338
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8405
9339
  {
@@ -8415,15 +9349,21 @@ var ChannelDriver = class _ChannelDriver {
8415
9349
  opencode_session_id: sessionId,
8416
9350
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8417
9351
  ...title ? { title } : {},
8418
- ...usage ? usage : {}
9352
+ ...usage ? usage : {},
9353
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
9354
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8419
9355
  })
8420
9356
  }
8421
9357
  );
8422
9358
  this.assertAuth(res, "marking message as done");
8423
- if (res.ok) return;
9359
+ if (res.ok) {
9360
+ this.clearSubagentInvocationCaches(messageId);
9361
+ return;
9362
+ }
8424
9363
  if (isRetryableStatus(res.status)) {
8425
9364
  throw new Error(`marking message as done: HTTP ${res.status}`);
8426
9365
  }
9366
+ this.clearSubagentInvocationCaches(messageId);
8427
9367
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8428
9368
  }
8429
9369
  /**
@@ -8438,7 +9378,7 @@ var ChannelDriver = class _ChannelDriver {
8438
9378
  * exists but is wedged, so the next attempt must get a fresh one
8439
9379
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8440
9380
  */
8441
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
9381
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8442
9382
  const body = { status: "failed" };
8443
9383
  if (sessionId === null) {
8444
9384
  body.opencode_session_id = null;
@@ -8447,23 +9387,33 @@ var ChannelDriver = class _ChannelDriver {
8447
9387
  }
8448
9388
  if (error2 !== void 0) body.error = error2;
8449
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
+ }
8450
9394
  if (failure) {
8451
9395
  body.failure_kind = failure.kind;
8452
9396
  body.failure_provider_id = failure.providerId;
8453
9397
  body.failure_model_id = failure.modelId;
8454
9398
  body.failure_reason = failure.reason;
8455
9399
  }
8456
- await this.callWithRetry(
8457
- "marking message as failed",
8458
- () => this.fetchImpl(
8459
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8460
- {
8461
- method: "PATCH",
8462
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8463
- body: JSON.stringify(body)
8464
- }
8465
- )
8466
- );
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);
8467
9417
  }
8468
9418
  /**
8469
9419
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -8488,14 +9438,119 @@ var ChannelDriver = class _ChannelDriver {
8488
9438
  reply?.info?.modelID ?? null
8489
9439
  );
8490
9440
  }
8491
- /**
8492
- * Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
8493
- * (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
8494
- * the server records it via `log()` (no DB write, no notification). This is
8495
- * fire-and-forget: it MUST NEVER throw into the drain or the watcher tick, and
8496
- * MUST NOT use `callWithRetry` (a telemetry ping must not block the sequential
8497
- * watcher tick — one attempt is enough). A failure is SWALLOWED but LOGGED with
8498
- * context (no silent catch, per development-workflow).
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
+ }
9546
+ /**
9547
+ * Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
9548
+ * (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
9549
+ * — the server records it via `log()` (no DB write, no notification). This is
9550
+ * fire-and-forget: it MUST NEVER throw into the drain or the watcher tick, and
9551
+ * MUST NOT use `callWithRetry` (a telemetry ping must not block the sequential
9552
+ * watcher tick — one attempt is enough). A failure is SWALLOWED but LOGGED with
9553
+ * context (no silent catch, per development-workflow).
8499
9554
  *
8500
9555
  * Returns whether the POST SUCCEEDED (2xx). Most callers ignore this (pure
8501
9556
  * telemetry), but the `paused` liveness-clear uses it to know whether to
@@ -8641,6 +9696,13 @@ import chalk5 from "chalk";
8641
9696
  import ora2 from "ora";
8642
9697
  import { select as select2 } from "@inquirer/prompts";
8643
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
+ }
8644
9706
  async function ensureOpenCodeRunning(ctx) {
8645
9707
  const healthCheck = await checkOpenCodeHealth(ctx.port);
8646
9708
  if (healthCheck.healthy) {
@@ -8688,6 +9750,7 @@ async function ensureOpenCodeRunning(ctx) {
8688
9750
  }
8689
9751
  }
8690
9752
  if (!ctx.interactive) {
9753
+ checkNonInteractivePortConflict(ctx.port, isPortInUse);
8691
9754
  ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
8692
9755
  const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
8693
9756
  const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
@@ -8769,9 +9832,119 @@ Port ${port} is already in use.`));
8769
9832
  return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
8770
9833
  }
8771
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
+
8772
9945
  // src/lib/runner-credentials.ts
8773
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
8774
- 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";
8775
9948
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
8776
9949
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
8777
9950
  var GITHUB_PROBE_TIMEOUT_MS = 1e4;
@@ -9043,11 +10216,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
9043
10216
  }
9044
10217
 
9045
10218
  // src/lib/opencode/config-overlay.ts
9046
- import { execFileSync as execFileSync2 } from "child_process";
9047
- import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
9048
- import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
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";
9049
10222
  function isFile(filePath) {
9050
- return existsSync2(filePath) && statSync5(filePath).isFile();
10223
+ return existsSync2(filePath) && statSync6(filePath).isFile();
9051
10224
  }
9052
10225
  function applyRunnerOpenCodeConfig({
9053
10226
  overlayPath,
@@ -9059,7 +10232,7 @@ function applyRunnerOpenCodeConfig({
9059
10232
  return;
9060
10233
  }
9061
10234
  const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
9062
- const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
10235
+ const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
9063
10236
  if (!isFile(source)) {
9064
10237
  log3(
9065
10238
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -9067,7 +10240,7 @@ function applyRunnerOpenCodeConfig({
9067
10240
  );
9068
10241
  return;
9069
10242
  }
9070
- copyFileSync(source, join8(cwd, target));
10243
+ copyFileSync(source, join9(cwd, target));
9071
10244
  try {
9072
10245
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
9073
10246
  stdio: "ignore"
@@ -9076,7 +10249,242 @@ function applyRunnerOpenCodeConfig({
9076
10249
  const detail = error2 instanceof Error ? error2.message : String(error2);
9077
10250
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
9078
10251
  }
9079
- log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
10252
+ log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
10253
+ }
10254
+
10255
+ // src/lib/credential-sync.ts
10256
+ import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
10257
+ var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
10258
+ var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
10259
+ var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
10260
+ var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
10261
+ var STORES = ["claude", "opencode"];
10262
+ var MAX_FLUSH_PASSES = 2;
10263
+ function outcomesWith(outcome) {
10264
+ return { claude: outcome, opencode: outcome };
10265
+ }
10266
+ function errorMessage2(error2) {
10267
+ return error2 instanceof Error ? error2.message : String(error2);
10268
+ }
10269
+ function waitForSettlement(promise, timeoutMs) {
10270
+ return new Promise((resolve4) => {
10271
+ let settled = false;
10272
+ const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
10273
+ const finish = (value) => {
10274
+ if (settled) return;
10275
+ settled = true;
10276
+ clearTimeout(timer);
10277
+ resolve4(value);
10278
+ };
10279
+ promise.then(
10280
+ () => finish(true),
10281
+ () => finish(true)
10282
+ );
10283
+ });
10284
+ }
10285
+ function writeMarker(markerPath, outcomes, log3) {
10286
+ const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
10287
+ `;
10288
+ const temporaryPath = `${markerPath}.tmp`;
10289
+ try {
10290
+ writeFileSync5(temporaryPath, body, { mode: 384 });
10291
+ renameSync(temporaryPath, markerPath);
10292
+ } catch (error2) {
10293
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
10294
+ }
10295
+ }
10296
+ function intervalSeconds(env, log3) {
10297
+ const raw = env.CREDS_SYNC_INTERVAL;
10298
+ if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
10299
+ return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
10300
+ }
10301
+ log3(
10302
+ `CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
10303
+ "warn"
10304
+ );
10305
+ return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
10306
+ }
10307
+ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
10308
+ const remainingMs = deadlineAt - Date.now();
10309
+ if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
10310
+ const controller = new AbortController();
10311
+ let result;
10312
+ let failed = false;
10313
+ const completion = Promise.resolve().then(
10314
+ () => synchroniserRunner(["sync-once", store], {
10315
+ timeoutMs: remainingMs,
10316
+ env,
10317
+ signal: controller.signal
10318
+ })
10319
+ ).then(
10320
+ (value) => {
10321
+ result = value;
10322
+ },
10323
+ (error2) => {
10324
+ failed = true;
10325
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
10326
+ }
10327
+ );
10328
+ const abortTimer = setTimeout(() => controller.abort(), remainingMs);
10329
+ const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
10330
+ clearTimeout(abortTimer);
10331
+ if (!settledBeforeDeadline) {
10332
+ controller.abort();
10333
+ const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
10334
+ if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
10335
+ return { outcome: "timeout", orphaned: false };
10336
+ }
10337
+ if (failed || !result) return { outcome: "failed", orphaned: false };
10338
+ if (result.timedOut || Date.now() >= deadlineAt) {
10339
+ return { outcome: "timeout", orphaned: false };
10340
+ }
10341
+ return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
10342
+ }
10343
+ function createCredentialSync({
10344
+ markerPath,
10345
+ env,
10346
+ log: log3,
10347
+ synchroniserRunner = runSynchroniser
10348
+ }) {
10349
+ const persistenceDisabled = !env.PERSISTENCE_BUCKET;
10350
+ let disabled = persistenceDisabled;
10351
+ let armed = false;
10352
+ let stopped = false;
10353
+ let timer;
10354
+ let inFlight;
10355
+ let activeTickAbort;
10356
+ let lastTickFailed;
10357
+ let flushPromise;
10358
+ const scheduleTick = (intervalMs, startTick2) => {
10359
+ if (stopped) return;
10360
+ timer = setTimeout(() => {
10361
+ timer = void 0;
10362
+ startTick2();
10363
+ }, intervalMs);
10364
+ };
10365
+ const startTick = (intervalMs) => {
10366
+ if (stopped) return;
10367
+ const controller = new AbortController();
10368
+ activeTickAbort = controller;
10369
+ const tick = (async () => {
10370
+ const outcomes = {
10371
+ claude: "failed",
10372
+ opencode: "failed"
10373
+ };
10374
+ for (const store of STORES) {
10375
+ if (controller.signal.aborted) break;
10376
+ try {
10377
+ const result = await synchroniserRunner(["sync-once", store], {
10378
+ timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
10379
+ env,
10380
+ signal: controller.signal
10381
+ });
10382
+ outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
10383
+ } catch (error2) {
10384
+ outcomes[store] = "failed";
10385
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
10386
+ }
10387
+ }
10388
+ const failed = STORES.some((store) => outcomes[store] === "failed");
10389
+ log3(
10390
+ `CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
10391
+ "debug"
10392
+ );
10393
+ if (failed && lastTickFailed !== true) {
10394
+ log3(
10395
+ "CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
10396
+ "warn"
10397
+ );
10398
+ } else if (!failed && lastTickFailed === true) {
10399
+ log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
10400
+ }
10401
+ lastTickFailed = failed;
10402
+ })().finally(() => {
10403
+ if (activeTickAbort === controller) activeTickAbort = void 0;
10404
+ if (inFlight === tick) inFlight = void 0;
10405
+ scheduleTick(intervalMs, () => startTick(intervalMs));
10406
+ });
10407
+ inFlight = tick;
10408
+ };
10409
+ const performFlush = async () => {
10410
+ stopped = true;
10411
+ if (timer) {
10412
+ clearTimeout(timer);
10413
+ timer = void 0;
10414
+ }
10415
+ const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
10416
+ if (inFlight) {
10417
+ const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
10418
+ if (!settled) {
10419
+ activeTickAbort?.abort();
10420
+ const settledAfterAbort = await waitForSettlement(
10421
+ inFlight,
10422
+ CREDENTIAL_FLUSH_ABORT_GRACE_MS
10423
+ );
10424
+ if (!settledAfterAbort) {
10425
+ log3(
10426
+ "CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
10427
+ "warn"
10428
+ );
10429
+ return { outcomes: outcomesWith("timeout"), orphaned: true };
10430
+ }
10431
+ }
10432
+ }
10433
+ if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
10434
+ const outcomes = outcomesWith("timeout");
10435
+ for (const store of STORES) {
10436
+ const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
10437
+ if (result.orphaned) {
10438
+ log3(
10439
+ "CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
10440
+ "warn"
10441
+ );
10442
+ return { outcomes: outcomesWith("timeout"), orphaned: true };
10443
+ }
10444
+ outcomes[store] = result.outcome;
10445
+ }
10446
+ return { outcomes, orphaned: false };
10447
+ };
10448
+ let flushPasses = 0;
10449
+ let lastFlush;
10450
+ return {
10451
+ arm() {
10452
+ if (stopped || armed) return;
10453
+ armed = true;
10454
+ if (persistenceDisabled) {
10455
+ disabled = true;
10456
+ log3(
10457
+ "CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
10458
+ "warn"
10459
+ );
10460
+ return;
10461
+ }
10462
+ disabled = false;
10463
+ const intervalMs = intervalSeconds(env, log3) * 1e3;
10464
+ scheduleTick(intervalMs, () => startTick(intervalMs));
10465
+ },
10466
+ async stopAndFlush(publish) {
10467
+ let result;
10468
+ const runningFlush = flushPromise;
10469
+ if (runningFlush) {
10470
+ result = await runningFlush;
10471
+ } else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
10472
+ result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
10473
+ } else {
10474
+ flushPasses++;
10475
+ const currentFlush = performFlush();
10476
+ flushPromise = currentFlush;
10477
+ try {
10478
+ result = await currentFlush;
10479
+ lastFlush = result;
10480
+ } finally {
10481
+ if (flushPromise === currentFlush) flushPromise = void 0;
10482
+ }
10483
+ }
10484
+ if (publish) writeMarker(markerPath, result.outcomes, log3);
10485
+ return result.outcomes;
10486
+ }
10487
+ };
9080
10488
  }
9081
10489
 
9082
10490
  // src/commands/run.ts
@@ -9116,7 +10524,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
9116
10524
  if (trimmed === "") {
9117
10525
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
9118
10526
  }
9119
- 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;
9120
10528
  if (!isAbsolute3(expanded)) {
9121
10529
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
9122
10530
  }
@@ -9140,6 +10548,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
9140
10548
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
9141
10549
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
9142
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
+ }
9143
10573
  function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
9144
10574
  const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
9145
10575
  let raw;
@@ -9206,7 +10636,7 @@ function log2(state, message, level = "info") {
9206
10636
  })
9207
10637
  );
9208
10638
  } else if (!state.interactive) {
9209
- 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");
9210
10640
  console.log(`${prefix} ${message}`);
9211
10641
  }
9212
10642
  }
@@ -9236,7 +10666,7 @@ function logActivity(state, entry) {
9236
10666
  }
9237
10667
  function reportSessionDbRecovery(state) {
9238
10668
  try {
9239
- const report = drainSessionDbRecoveryReport({ homeDir: homedir5(), env: process.env });
10669
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
9240
10670
  for (const record of report.records) {
9241
10671
  const activity = buildSessionDbRecoveryActivity(record);
9242
10672
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -9267,18 +10697,18 @@ function reportSessionDbRecoveryRecord(state, record) {
9267
10697
  function displayStatus(state) {
9268
10698
  if (!state.interactive) return;
9269
10699
  const attempt = state.connection?.reconnectAttempt ?? 0;
9270
- const tunnel = state.connected ? chalk6.green("tunnel: connected") : attempt > 0 ? chalk6.yellow(`tunnel: reconnecting (#${attempt})`) : chalk6.yellow("tunnel: connecting");
9271
- const opencode = state.opencodeConnected ? chalk6.green(`opencode: :${state.port}`) : chalk6.red(`opencode: :${state.port} (down)`);
9272
- const messages = state.messageCount > 0 ? chalk6.dim(` \xB7 ${state.messageCount} processed`) : "";
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`) : "";
9273
10703
  const last = state.activityLog[state.activityLog.length - 1];
9274
- 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 ?? ""}`) : "";
9275
10705
  const agent = state.agentName ?? state.agentId;
9276
10706
  console.log(
9277
- `${chalk6.bold("Evident")} ${chalk6.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
10707
+ `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
9278
10708
  );
9279
10709
  }
9280
10710
  async function promptForLogin(promptMessage, successMessage) {
9281
- const action = await select3({
10711
+ const action = await select4({
9282
10712
  message: promptMessage,
9283
10713
  choices: [
9284
10714
  {
@@ -9294,7 +10724,7 @@ async function promptForLogin(promptMessage, successMessage) {
9294
10724
  ]
9295
10725
  });
9296
10726
  if (action === "exit") {
9297
- console.log(chalk6.dim(`
10727
+ console.log(chalk7.dim(`
9298
10728
  You can log in later by running: ${getCliName()} login`));
9299
10729
  process.exit(0);
9300
10730
  }
@@ -9305,7 +10735,7 @@ You can log in later by running: ${getCliName()} login`));
9305
10735
  process.exit(1);
9306
10736
  }
9307
10737
  blank();
9308
- console.log(chalk6.green(successMessage));
10738
+ console.log(chalk7.green(successMessage));
9309
10739
  blank();
9310
10740
  return { token: credentials2.token, authType: "bearer", user: credentials2.user };
9311
10741
  }
@@ -9318,12 +10748,12 @@ async function handleAuthError(state, error2) {
9318
10748
  if (state.interactive) displayStatus(state);
9319
10749
  if (!state.interactive) {
9320
10750
  blank();
9321
- console.log(chalk6.red("Authentication expired"));
9322
- 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."));
9323
10753
  blank();
9324
- console.log(chalk6.dim("To fix this:"));
9325
- console.log(chalk6.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
9326
- console.log(chalk6.dim(" 2. Restart this command"));
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"));
9327
10757
  blank();
9328
10758
  await cleanup(state);
9329
10759
  await shutdownTelemetry();
@@ -9331,7 +10761,7 @@ async function handleAuthError(state, error2) {
9331
10761
  return { success: false };
9332
10762
  }
9333
10763
  blank();
9334
- console.log(chalk6.yellow("Your authentication has expired."));
10764
+ console.log(chalk7.yellow("Your authentication has expired."));
9335
10765
  blank();
9336
10766
  try {
9337
10767
  const credentials2 = await promptForLogin(
@@ -9376,6 +10806,10 @@ async function driveChannels(state, driver) {
9376
10806
  consecutiveDrainFailures = 0;
9377
10807
  unreachableMs = 0;
9378
10808
  state.messageCount += processed;
10809
+ if (driver.recycleRequested) {
10810
+ await beginGracefulShutdown(state, "recycle");
10811
+ return;
10812
+ }
9379
10813
  const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
9380
10814
  lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
9381
10815
  const fileActivitySnapshot = driver.fileSyncActivity();
@@ -9391,6 +10825,14 @@ async function driveChannels(state, driver) {
9391
10825
  const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
9392
10826
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
9393
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
+ }
9394
10836
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
9395
10837
  idlePolls = 0;
9396
10838
  idleMs = 0;
@@ -9418,8 +10860,8 @@ async function driveChannels(state, driver) {
9418
10860
  state.running = false;
9419
10861
  break;
9420
10862
  }
9421
- const errorMessage = error2 instanceof Error ? error2.message : String(error2);
9422
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage}` });
10863
+ const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
10864
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
9423
10865
  if (state.interactive) displayStatus(state);
9424
10866
  if (driver.hasInFlightWatchers()) {
9425
10867
  consecutiveDrainFailures = 0;
@@ -9457,9 +10899,18 @@ async function driveChannels(state, driver) {
9457
10899
  }
9458
10900
  }
9459
10901
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
9460
- 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
+ }
9461
10912
  function sessionDbPath() {
9462
- return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
10913
+ return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
9463
10914
  }
9464
10915
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
9465
10916
  const record = {
@@ -9555,7 +11006,7 @@ async function runSweep(state, driver, config) {
9555
11006
  } else {
9556
11007
  logActivity(state, {
9557
11008
  type: "info",
9558
- 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}` : "")
9559
11010
  });
9560
11011
  }
9561
11012
  } catch (error2) {
@@ -9578,13 +11029,20 @@ function scheduleSessionCleanup(state, driver, options) {
9578
11029
  for (const warning2 of config.warnings) {
9579
11030
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
9580
11031
  }
9581
- const dbBytes = statSessionDbBytes(homedir5());
11032
+ const dbBytes = statSessionDbBytes(homedir6());
9582
11033
  void (async () => {
9583
- 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
+ }
9584
11042
  const sizeWarning = buildSessionStoreSizeWarning({
9585
11043
  dbBytes,
9586
11044
  cleanupEnabled: config.enabled,
9587
- reclaimSkipReason
11045
+ reclaimSkipReason: reclaimAvailability?.reason ?? null
9588
11046
  });
9589
11047
  if (sizeWarning !== null) {
9590
11048
  logActivity(state, { type: "info", level: "warn", message: sizeWarning });
@@ -9751,14 +11209,11 @@ function scheduleClaudeUsageReporting(state, options) {
9751
11209
  report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
9752
11210
  isLocalCredentialProblem,
9753
11211
  forcedOnHint: "run `claude` to sign in",
9754
- firstDelayMs: () => FIRST_REPORT_DELAY_MS,
9755
- nextDelayMs: nextReportDelayMs,
9756
- failureLogLevel: claudeUsageFailureLogLevel
11212
+ firstDelayMs: firstReportDelayMs,
11213
+ nextDelayMs: usageReportDelayMs,
11214
+ failureLogLevel: usageReportFailureLogLevel
9757
11215
  });
9758
11216
  }
9759
- var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
9760
- var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
9761
- var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
9762
11217
  function scheduleResourceUsageReporting(state, options) {
9763
11218
  const { enabled, warnings } = resolveResourceUsageReportingEnabled(
9764
11219
  options.resourceUsageReporting,
@@ -9779,7 +11234,8 @@ function scheduleResourceUsageReporting(state, options) {
9779
11234
  });
9780
11235
  return;
9781
11236
  }
9782
- const collect = createResourceUsageCollector(homedir5());
11237
+ const { collect, stop } = createResourceUsageCollector(homedir6());
11238
+ state.stopResourceUsageSampling = stop;
9783
11239
  let consecutiveFailures = 0;
9784
11240
  const tick = async () => {
9785
11241
  try {
@@ -9810,10 +11266,7 @@ function scheduleResourceUsageReporting(state, options) {
9810
11266
  consecutiveFailures++;
9811
11267
  logActivity(state, {
9812
11268
  type: "info",
9813
- level: reportFailureLogLevel(
9814
- consecutiveFailures,
9815
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
9816
- ),
11269
+ level: usageReportFailureLogLevel(consecutiveFailures),
9817
11270
  message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
9818
11271
  });
9819
11272
  }
@@ -9822,20 +11275,11 @@ function scheduleResourceUsageReporting(state, options) {
9822
11275
  const message = error2 instanceof Error ? error2.message : String(error2);
9823
11276
  logActivity(state, {
9824
11277
  type: "info",
9825
- level: reportFailureLogLevel(
9826
- consecutiveFailures,
9827
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
9828
- ),
11278
+ level: usageReportFailureLogLevel(consecutiveFailures),
9829
11279
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
9830
11280
  });
9831
11281
  } finally {
9832
- state.resourceUsageTimer = setTimeout(
9833
- () => void tick(),
9834
- jitteredDelayMs(
9835
- RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
9836
- RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
9837
- )
9838
- );
11282
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
9839
11283
  }
9840
11284
  };
9841
11285
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -9875,6 +11319,8 @@ async function cleanup(state, opts = {}) {
9875
11319
  clearTimeout(timer);
9876
11320
  }
9877
11321
  state.sessionCleanupTimers = [];
11322
+ state.stopOpenCodeLogTail?.();
11323
+ state.stopOpenCodeLogTail = null;
9878
11324
  if (state.claudeUsageTimer) {
9879
11325
  clearTimeout(state.claudeUsageTimer);
9880
11326
  state.claudeUsageTimer = null;
@@ -9889,21 +11335,41 @@ async function cleanup(state, opts = {}) {
9889
11335
  clearTimeout(state.resourceUsageTimer);
9890
11336
  state.resourceUsageTimer = null;
9891
11337
  }
11338
+ state.stopResourceUsageSampling?.();
11339
+ state.stopResourceUsageSampling = null;
11340
+ const credentialSync = state.credentialSync;
11341
+ const flushCredentials = credentialSync ? async (phase, publish) => {
11342
+ await timeShutdownPhase(state, durations, phase, async () => {
11343
+ const outcomes = await credentialSync.stopAndFlush(publish);
11344
+ const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
11345
+ log2(
11346
+ state,
11347
+ `Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
11348
+ level
11349
+ );
11350
+ });
11351
+ } : void 0;
11352
+ let drainSettled = true;
9892
11353
  if (opts.graceful && state.channelDriver) {
9893
11354
  state.channelDriver.stop();
11355
+ }
11356
+ if (flushCredentials) {
11357
+ await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
11358
+ }
11359
+ if (opts.graceful && state.channelDriver) {
9894
11360
  log2(state, "Draining in-flight channel work before shutdown...");
9895
11361
  if (state.interactive) {
9896
11362
  logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
9897
11363
  displayStatus(state);
9898
11364
  }
9899
11365
  const driver = state.channelDriver;
9900
- const settled = await timeShutdownPhase(
11366
+ drainSettled = await timeShutdownPhase(
9901
11367
  state,
9902
11368
  durations,
9903
11369
  "drain",
9904
11370
  () => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
9905
11371
  );
9906
- if (!settled) {
11372
+ if (!drainSettled) {
9907
11373
  logActivity(state, {
9908
11374
  type: "info",
9909
11375
  message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
@@ -9911,6 +11377,9 @@ async function cleanup(state, opts = {}) {
9911
11377
  if (state.interactive) displayStatus(state);
9912
11378
  }
9913
11379
  }
11380
+ if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
11381
+ await flushCredentials("credential_flush_final", true);
11382
+ }
9914
11383
  await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
9915
11384
  if (state.connection) {
9916
11385
  const connection = state.connection;
@@ -9946,13 +11415,51 @@ async function cleanup(state, opts = {}) {
9946
11415
  }
9947
11416
  return durations;
9948
11417
  }
11418
+ async function beginGracefulShutdown(state, trigger) {
11419
+ if (state.shuttingDown) return;
11420
+ state.shuttingDown = true;
11421
+ const shutdownStartedAt = Date.now();
11422
+ const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
11423
+ if (state.interactive) {
11424
+ logActivity(state, { type: "info", message: shutdownMessage });
11425
+ displayStatus(state);
11426
+ } else {
11427
+ log2(state, shutdownMessage);
11428
+ }
11429
+ const durations = await cleanup(state, { graceful: true });
11430
+ const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
11431
+ await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
11432
+ let timer;
11433
+ const flushed = shutdownTelemetry().then(
11434
+ () => true,
11435
+ (error2) => {
11436
+ log2(
11437
+ state,
11438
+ `Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
11439
+ "warn"
11440
+ );
11441
+ return true;
11442
+ }
11443
+ );
11444
+ const timedOut = new Promise((resolve4) => {
11445
+ timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
11446
+ });
11447
+ if (!await Promise.race([flushed, timedOut])) {
11448
+ log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
11449
+ }
11450
+ clearTimeout(timer);
11451
+ });
11452
+ const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
11453
+ log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
11454
+ process.exit(0);
11455
+ }
9949
11456
  async function run(options) {
9950
11457
  const interactive = isInteractive(options.json);
9951
11458
  let logLevel;
9952
11459
  let fileSyncDirectories;
9953
11460
  try {
9954
11461
  logLevel = resolveLogLevel(options);
9955
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir5());
11462
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
9956
11463
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
9957
11464
  throw new Error(
9958
11465
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -9983,6 +11490,7 @@ async function run(options) {
9983
11490
  opencodeVersion: null,
9984
11491
  sessionDbProvenanceAnomaly: false,
9985
11492
  opencodeProcess: null,
11493
+ stopOpenCodeLogTail: null,
9986
11494
  litestreamProcess: null,
9987
11495
  connection: null,
9988
11496
  channelDriver: null,
@@ -9997,9 +11505,24 @@ async function run(options) {
9997
11505
  openaiUsageTimer: null,
9998
11506
  openaiUsageRearm: null,
9999
11507
  resourceUsageTimer: null,
11508
+ stopResourceUsageSampling: null,
11509
+ credentialSync: null,
10000
11510
  authHeader: ""
10001
11511
  };
10002
11512
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
11513
+ if (options.credentialSyncMarker) {
11514
+ state.credentialSync = createCredentialSync({
11515
+ markerPath: options.credentialSyncMarker,
11516
+ env: process.env,
11517
+ log: (message, level = "info") => {
11518
+ if (level === "error") {
11519
+ logActivity(state, { type: "error", error: message });
11520
+ } else {
11521
+ logActivity(state, { type: "info", level, message });
11522
+ }
11523
+ }
11524
+ });
11525
+ }
10003
11526
  if (fileSyncDirectories.length > 0) {
10004
11527
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
10005
11528
  } else {
@@ -10025,43 +11548,7 @@ async function run(options) {
10025
11548
  "warn"
10026
11549
  );
10027
11550
  }
10028
- const handleSignal = async () => {
10029
- if (state.shuttingDown) return;
10030
- state.shuttingDown = true;
10031
- const shutdownStartedAt = Date.now();
10032
- if (state.interactive) {
10033
- logActivity(state, { type: "info", message: "Shutting down..." });
10034
- displayStatus(state);
10035
- } else {
10036
- log2(state, "Shutting down...");
10037
- }
10038
- const durations = await cleanup(state, { graceful: true });
10039
- const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
10040
- await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
10041
- let timer;
10042
- const flushed = shutdownTelemetry().then(
10043
- () => true,
10044
- (error2) => {
10045
- log2(
10046
- state,
10047
- `Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
10048
- "warn"
10049
- );
10050
- return true;
10051
- }
10052
- );
10053
- const timedOut = new Promise((resolve4) => {
10054
- timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
10055
- });
10056
- if (!await Promise.race([flushed, timedOut])) {
10057
- log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
10058
- }
10059
- clearTimeout(timer);
10060
- });
10061
- const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
10062
- log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
10063
- process.exit(0);
10064
- };
11551
+ const handleSignal = () => beginGracefulShutdown(state, "signal");
10065
11552
  process.on("SIGINT", handleSignal);
10066
11553
  process.on("SIGTERM", handleSignal);
10067
11554
  try {
@@ -10071,15 +11558,15 @@ async function run(options) {
10071
11558
  printError("Authentication required");
10072
11559
  blank();
10073
11560
  console.log(
10074
- 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")
10075
11562
  );
10076
- console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
11563
+ console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
10077
11564
  blank();
10078
11565
  process.exit(1);
10079
11566
  return;
10080
11567
  }
10081
11568
  blank();
10082
- console.log(chalk6.yellow("You are not logged in to Evident."));
11569
+ console.log(chalk7.yellow("You are not logged in to Evident."));
10083
11570
  blank();
10084
11571
  credentials2 = await promptForLogin(
10085
11572
  "Would you like to log in now?",
@@ -10129,7 +11616,7 @@ async function run(options) {
10129
11616
  );
10130
11617
  blank();
10131
11618
  console.log(
10132
- chalk6.dim(
11619
+ chalk7.dim(
10133
11620
  "Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
10134
11621
  )
10135
11622
  );
@@ -10152,15 +11639,15 @@ async function run(options) {
10152
11639
  );
10153
11640
  if (interactive && !state.json) {
10154
11641
  blank();
10155
- console.log(chalk6.bold("Evident Run"));
10156
- console.log(chalk6.dim("-".repeat(40)));
11642
+ console.log(chalk7.bold("Evident Run"));
11643
+ console.log(chalk7.dim("-".repeat(40)));
10157
11644
  }
10158
11645
  const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
10159
11646
  let validation = await getAgentInfo(state.agentId, state.authHeader);
10160
11647
  if (!validation.valid && validation.authFailed && interactive) {
10161
11648
  spinner?.fail("Authentication failed");
10162
11649
  blank();
10163
- console.log(chalk6.yellow("Your authentication token is invalid or expired."));
11650
+ console.log(chalk7.yellow("Your authentication token is invalid or expired."));
10164
11651
  blank();
10165
11652
  credentials2 = await promptForLogin(
10166
11653
  "Would you like to log in again?",
@@ -10207,6 +11694,14 @@ async function run(options) {
10207
11694
  await restoreCredentialStores(credentialContext);
10208
11695
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
10209
11696
  }
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;
10210
11705
  let sessionDbVerifyFatal = false;
10211
11706
  if (!options.restoreSessionDb) {
10212
11707
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -10256,6 +11751,13 @@ async function run(options) {
10256
11751
  for (const warning2 of opencodeStartTimeoutWarnings) {
10257
11752
  logActivity(state, { type: "info", level: "warn", message: warning2 });
10258
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
+ }
10259
11761
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
10260
11762
  for (const warning2 of maxActiveSessionsWarnings) {
10261
11763
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -10263,7 +11765,14 @@ async function run(options) {
10263
11765
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
10264
11766
  const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
10265
11767
  try {
10266
- 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({
10267
11776
  port: state.port,
10268
11777
  interactive: state.interactive,
10269
11778
  agentId: state.agentId,
@@ -10276,7 +11785,7 @@ async function run(options) {
10276
11785
  state.opencodeVersion = oc.version;
10277
11786
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
10278
11787
  try {
10279
- writeFileSync5(options.opencodePidFile, `${oc.process.pid}
11788
+ writeFileSync6(options.opencodePidFile, `${oc.process.pid}
10280
11789
  `, { mode: 384 });
10281
11790
  chmodSync3(options.opencodePidFile, 384);
10282
11791
  } catch (error2) {
@@ -10290,7 +11799,7 @@ async function run(options) {
10290
11799
  const provenance = checkSessionDbProvenance({
10291
11800
  dbPath: sessionDbPath(),
10292
11801
  currentVersion: state.opencodeVersion,
10293
- homeDir: homedir5(),
11802
+ homeDir: homedir6(),
10294
11803
  env: process.env
10295
11804
  });
10296
11805
  if (provenance.anomaly) {
@@ -10317,6 +11826,7 @@ async function run(options) {
10317
11826
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
10318
11827
  }
10319
11828
  }
11829
+ await reloadProviderCache(state.port);
10320
11830
  const noProviderWarning = buildNoProviderWarning(
10321
11831
  await hasAnyConfiguredProvider(state.port)
10322
11832
  );
@@ -10325,10 +11835,10 @@ async function run(options) {
10325
11835
  if (state.interactive && !state.json) {
10326
11836
  logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
10327
11837
  blank();
10328
- console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
11838
+ console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
10329
11839
  console.log(
10330
- chalk6.dim(
10331
- `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.`
10332
11842
  )
10333
11843
  );
10334
11844
  blank();
@@ -10392,7 +11902,7 @@ async function run(options) {
10392
11902
  });
10393
11903
  try {
10394
11904
  if (litestreamProcess.pid !== void 0) {
10395
- writeFileSync5(options.litestreamPidFile, `${litestreamProcess.pid}
11905
+ writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
10396
11906
  `, {
10397
11907
  mode: 384
10398
11908
  });
@@ -10452,7 +11962,7 @@ async function run(options) {
10452
11962
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
10453
11963
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
10454
11964
  fileSyncDirectories,
10455
- homeDir: homedir5(),
11965
+ homeDir: homedir6(),
10456
11966
  maxActiveSessions,
10457
11967
  log: (entry) => (
10458
11968
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -10578,6 +12088,18 @@ async function run(options) {
10578
12088
  if (state.interactive) displayStatus(state);
10579
12089
  });
10580
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
+ },
10581
12103
  onInfo: (message) => logActivity(state, { type: "info", message })
10582
12104
  }
10583
12105
  });
@@ -10598,7 +12120,17 @@ async function run(options) {
10598
12120
  setTimer: (timer) => {
10599
12121
  state.openaiUsageTimer = timer;
10600
12122
  },
10601
- fetchUsage: () => getOpenAiUsage(state.port),
12123
+ fetchUsage: async () => {
12124
+ const usage = await getOpenAiUsage(state.port);
12125
+ if (usage.subscription === null) {
12126
+ logActivity(state, {
12127
+ type: "info",
12128
+ level: "debug",
12129
+ message: "OpenAI usage subscription could not be identified from the local credential"
12130
+ });
12131
+ }
12132
+ return usage;
12133
+ },
10602
12134
  report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
10603
12135
  isLocalCredentialProblem: isLocalCredentialProblem2,
10604
12136
  forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
@@ -10672,6 +12204,9 @@ program.command("run").description("Connect to Evident and process messages").op
10672
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(
10673
12205
  "--opencode-start-timeout <seconds>",
10674
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"
10675
12210
  ).option("--json", "Output in JSON format").option(
10676
12211
  "--session-cleanup-max-age <duration>",
10677
12212
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -10722,6 +12257,9 @@ program.command("run").description("Connect to Evident and process messages").op
10722
12257
  ).option(
10723
12258
  "--opencode-config-overlay <path>",
10724
12259
  "Apply this runner-provided OpenCode config before starting OpenCode."
12260
+ ).option(
12261
+ "--credential-sync-marker <path>",
12262
+ "Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
10725
12263
  ).action(
10726
12264
  (options) => {
10727
12265
  run({
@@ -10737,6 +12275,7 @@ program.command("run").description("Connect to Evident and process messages").op
10737
12275
  // Raw string — validation/precedence is single-sourced in run.ts's
10738
12276
  // resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
10739
12277
  opencodeStartTimeout: options.opencodeStartTimeout,
12278
+ opencodeVersion: options.opencodeVersion,
10740
12279
  json: options.json,
10741
12280
  // Raw strings — the resolver in run.ts single-sources parsing (M1).
10742
12281
  sessionCleanupMaxAge: options.sessionCleanupMaxAge,
@@ -10760,7 +12299,8 @@ program.command("run").description("Connect to Evident and process messages").op
10760
12299
  sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
10761
12300
  restoreSessionDb: options.restoreSessionDb,
10762
12301
  restoreRunnerCredentials: options.restoreRunnerCredentials,
10763
- opencodeConfigOverlay: options.opencodeConfigOverlay
12302
+ opencodeConfigOverlay: options.opencodeConfigOverlay,
12303
+ credentialSyncMarker: options.credentialSyncMarker
10764
12304
  });
10765
12305
  }
10766
12306
  );