@evident-ai/cli 3.4.1-dev.d2c12e9 → 3.4.1-dev.d74adb9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,8 +11,8 @@ import chalk2 from "chalk";
11
11
 
12
12
  // src/lib/config.ts
13
13
  import Conf from "conf";
14
- import { chmodSync, existsSync, statSync } from "fs";
15
- import { dirname } from "path";
14
+ import { chmodSync, existsSync, statSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
16
  var PRODUCTION_API_URL = "https://api.production.evident.run/v1";
17
17
  var PRODUCTION_TUNNEL_URL = "wss://tunnel.production.evident.run";
18
18
  var defaults = {
@@ -623,6 +623,19 @@ function isInteractive(jsonOutput) {
623
623
  return true;
624
624
  }
625
625
 
626
+ // src/lib/subscription-usage-report.ts
627
+ function toReportedSubscription(collected) {
628
+ if (!collected) return null;
629
+ if (collected.ownerEmail === null && collected.planType === null && collected.organizationName === null) {
630
+ return null;
631
+ }
632
+ return {
633
+ owner_email: collected.ownerEmail,
634
+ plan_type: collected.planType,
635
+ organization_name: collected.organizationName
636
+ };
637
+ }
638
+
626
639
  // src/commands/agent-lookup.ts
627
640
  async function readErrorMessage(response) {
628
641
  const text = await response.text().catch(() => "");
@@ -670,12 +683,15 @@ async function resolveAgentIdFromKey(authHeader) {
670
683
  }
671
684
  }
672
685
  var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
673
- async function notifyAgentDisconnected(agentId, authHeader) {
674
- const apiUrl = getApiUrlConfig();
686
+ async function postBestEffort(path, authHeader, body) {
675
687
  try {
676
- const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
688
+ const apiUrl = getApiUrlConfig();
689
+ const headers = { Authorization: authHeader };
690
+ if (body !== void 0) headers["Content-Type"] = "application/json";
691
+ const response = await fetch(`${apiUrl}${path}`, {
677
692
  method: "POST",
678
- headers: { Authorization: authHeader },
693
+ headers,
694
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
679
695
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
680
696
  });
681
697
  if (!response.ok) {
@@ -687,73 +703,32 @@ async function notifyAgentDisconnected(agentId, authHeader) {
687
703
  }
688
704
  return { ok: true };
689
705
  } catch (error2) {
690
- return { ok: false, error: describeBestEffortError(error2) };
706
+ return { ok: false, error: describeTimeoutError(error2, BEST_EFFORT_NOTIFY_TIMEOUT_MS) };
691
707
  }
692
708
  }
693
- function describeBestEffortError(error2) {
709
+ function describeTimeoutError(error2, timeoutMs) {
694
710
  const name = error2?.name;
695
711
  if (name === "TimeoutError" || name === "AbortError") {
696
- return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;
712
+ return `timed out after ${timeoutMs}ms`;
697
713
  }
698
714
  return error2 instanceof Error ? error2.message : String(error2);
699
715
  }
716
+ async function notifyAgentDisconnected(agentId, authHeader) {
717
+ return postBestEffort(`/runners/${agentId}/disconnect`, authHeader);
718
+ }
700
719
  async function reportMicrovmId(agentId, authHeader, microvmId) {
701
- try {
702
- const apiUrl = getApiUrlConfig();
703
- const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
704
- method: "POST",
705
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
706
- body: JSON.stringify({ microvm_id: microvmId }),
707
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
708
- });
709
- if (!response.ok) {
710
- const serverMessage = await readErrorMessage(response);
711
- return {
712
- ok: false,
713
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
714
- };
715
- }
716
- return { ok: true };
717
- } catch (error2) {
718
- return { ok: false, error: describeBestEffortError(error2) };
719
- }
720
+ return postBestEffort(`/runners/${agentId}/microvm`, authHeader, { microvm_id: microvmId });
720
721
  }
721
722
  function toReportedWindow(window) {
722
723
  if (!window) return null;
723
724
  return { utilization: window.utilization, resets_at: window.resetsAt };
724
725
  }
725
- function toReportedOwner(snapshot) {
726
- if (!snapshot.owner) return null;
727
- return {
728
- email: snapshot.owner.email,
729
- organization_name: snapshot.owner.organizationName,
730
- rate_limit_tier: snapshot.owner.rateLimitTier
731
- };
732
- }
733
726
  async function reportClaudeUsage(agentId, authHeader, snapshot) {
734
- try {
735
- const apiUrl = getApiUrlConfig();
736
- const response = await fetch(`${apiUrl}/runners/${agentId}/claude-usage`, {
737
- method: "POST",
738
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
739
- body: JSON.stringify({
740
- five_hour: toReportedWindow(snapshot.fiveHour),
741
- seven_day: toReportedWindow(snapshot.sevenDay),
742
- owner: toReportedOwner(snapshot)
743
- }),
744
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
745
- });
746
- if (!response.ok) {
747
- const serverMessage = await readErrorMessage(response);
748
- return {
749
- ok: false,
750
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
751
- };
752
- }
753
- return { ok: true };
754
- } catch (error2) {
755
- return { ok: false, error: describeBestEffortError(error2) };
756
- }
727
+ return postBestEffort(`/runners/${agentId}/claude-usage`, authHeader, {
728
+ five_hour: toReportedWindow(snapshot.fiveHour),
729
+ seven_day: toReportedWindow(snapshot.sevenDay),
730
+ subscription: toReportedSubscription(snapshot.subscription)
731
+ });
757
732
  }
758
733
  function toReportedOpenAiWindow(window) {
759
734
  if (!window) return null;
@@ -763,69 +738,26 @@ function toReportedOpenAiWindow(window) {
763
738
  resets_at: window.resetsAt
764
739
  };
765
740
  }
766
- function toReportedOpenAiSubscription(snapshot) {
767
- if (!snapshot.subscription) return null;
768
- return {
769
- owner_email: snapshot.subscription.ownerEmail,
770
- plan_type: snapshot.subscription.planType
771
- };
772
- }
773
741
  async function reportOpenAiUsage(agentId, authHeader, snapshot) {
774
- try {
775
- const apiUrl = getApiUrlConfig();
776
- const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
777
- method: "POST",
778
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
779
- body: JSON.stringify({
780
- primary: toReportedOpenAiWindow(snapshot.primary),
781
- secondary: toReportedOpenAiWindow(snapshot.secondary),
782
- has_credits: snapshot.hasCredits,
783
- credits_unlimited: snapshot.creditsUnlimited,
784
- subscription: toReportedOpenAiSubscription(snapshot)
785
- }),
786
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
787
- });
788
- if (!response.ok) {
789
- const serverMessage = await readErrorMessage(response);
790
- return {
791
- ok: false,
792
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
793
- };
794
- }
795
- return { ok: true };
796
- } catch (error2) {
797
- return { ok: false, error: describeBestEffortError(error2) };
798
- }
742
+ return postBestEffort(`/runners/${agentId}/openai-usage`, authHeader, {
743
+ primary: toReportedOpenAiWindow(snapshot.primary),
744
+ secondary: toReportedOpenAiWindow(snapshot.secondary),
745
+ has_credits: snapshot.hasCredits,
746
+ credits_unlimited: snapshot.creditsUnlimited,
747
+ subscription: toReportedSubscription(snapshot.subscription)
748
+ });
799
749
  }
800
750
  async function reportResourceUsage(agentId, authHeader, usage) {
801
- try {
802
- const apiUrl = getApiUrlConfig();
803
- const response = await fetch(`${apiUrl}/runners/${agentId}/resource-usage`, {
804
- method: "POST",
805
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
806
- body: JSON.stringify({
807
- cpu_percent: usage.cpuPercent,
808
- cpu_peak_percent: usage.cpuPeakPercent,
809
- cpu_count: usage.cpuCount,
810
- memory_total_bytes: usage.memoryTotalBytes,
811
- memory_available_bytes: usage.memoryAvailableBytes,
812
- disk_total_bytes: usage.diskTotalBytes,
813
- disk_free_bytes: usage.diskFreeBytes,
814
- opencode_db_bytes: usage.opencodeDbBytes
815
- }),
816
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
817
- });
818
- if (!response.ok) {
819
- const serverMessage = await readErrorMessage(response);
820
- return {
821
- ok: false,
822
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
823
- };
824
- }
825
- return { ok: true };
826
- } catch (error2) {
827
- return { ok: false, error: describeBestEffortError(error2) };
828
- }
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
+ });
829
761
  }
830
762
  async function getAgentInfo(agentId, authHeader) {
831
763
  const apiUrl = getApiUrlConfig();
@@ -877,13 +809,6 @@ function authLabelFor(credentials2) {
877
809
  }
878
810
  return "user token";
879
811
  }
880
- function describeFetchError(error2) {
881
- const name = error2?.name;
882
- if (name === "TimeoutError" || name === "AbortError") {
883
- return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
884
- }
885
- return error2 instanceof Error ? error2.message : String(error2);
886
- }
887
812
  async function checkStatus(jsonMode) {
888
813
  const apiUrl = getApiUrlConfig();
889
814
  const credentials2 = await getAuthCredentials();
@@ -911,7 +836,7 @@ async function checkStatus(jsonMode) {
911
836
  endpoint: apiUrl,
912
837
  authLabel: authLabelFor(credentials2),
913
838
  reason: "unreachable",
914
- 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.`,
915
840
  exitCode: 75
916
841
  };
917
842
  }
@@ -1009,10 +934,10 @@ async function status(options = {}) {
1009
934
  }
1010
935
 
1011
936
  // src/lib/claude-usage.ts
1012
- import { execFileSync } from "child_process";
1013
- import { readFileSync } from "fs";
1014
- import { homedir } from "os";
1015
- import { join } from "path";
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";
1016
941
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1017
942
  var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
1018
943
  var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
@@ -1097,7 +1022,7 @@ function ownerLookupFailure(error2) {
1097
1022
  }
1098
1023
  async function getClaudeUsageOwner(accessToken) {
1099
1024
  if (cachedOwner?.accessToken === accessToken) {
1100
- return { owner: cachedOwner.owner, ownerLookupError: null };
1025
+ return { subscription: cachedOwner.owner, ownerLookupError: null };
1101
1026
  }
1102
1027
  try {
1103
1028
  const response = await fetch(CLAUDE_PROFILE_URL, {
@@ -1109,27 +1034,27 @@ async function getClaudeUsageOwner(accessToken) {
1109
1034
  signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1110
1035
  });
1111
1036
  if (!response.ok) {
1112
- return { owner: null, ownerLookupError: `HTTP ${response.status}` };
1037
+ return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
1113
1038
  }
1114
1039
  let body;
1115
1040
  try {
1116
1041
  body = await response.json();
1117
1042
  } catch (error2) {
1118
- return { owner: null, ownerLookupError: "malformed response" };
1043
+ return { subscription: null, ownerLookupError: "malformed response" };
1119
1044
  }
1120
1045
  const profile = body;
1121
1046
  if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
1122
- return { owner: null, ownerLookupError: "malformed response" };
1047
+ return { subscription: null, ownerLookupError: "malformed response" };
1123
1048
  }
1124
- const owner = {
1125
- email: profile.account.email,
1049
+ const subscription = {
1050
+ ownerEmail: profile.account.email,
1126
1051
  organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
1127
- rateLimitTier: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1052
+ planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1128
1053
  };
1129
- cachedOwner = { accessToken, owner };
1130
- return { owner, ownerLookupError: null };
1054
+ cachedOwner = { accessToken, owner: subscription };
1055
+ return { subscription, ownerLookupError: null };
1131
1056
  } catch (error2) {
1132
- return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
1057
+ return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
1133
1058
  }
1134
1059
  }
1135
1060
  async function getClaudeUsage() {
@@ -1158,11 +1083,11 @@ async function getClaudeUsage() {
1158
1083
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1159
1084
  }
1160
1085
  const body = await res.json();
1161
- const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1086
+ const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1162
1087
  return {
1163
1088
  fiveHour: toWindow(body.five_hour),
1164
1089
  sevenDay: toWindow(body.seven_day),
1165
- owner,
1090
+ subscription,
1166
1091
  ownerLookupError
1167
1092
  };
1168
1093
  }
@@ -1192,10 +1117,10 @@ async function claudeUsage() {
1192
1117
  }
1193
1118
 
1194
1119
  // src/commands/run.ts
1195
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
1196
- import { homedir as homedir5 } from "os";
1197
- import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
1198
- import chalk6 from "chalk";
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";
1199
1124
 
1200
1125
  // ../../packages/types/src/agents/index.ts
1201
1126
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
@@ -1253,11 +1178,11 @@ function stripQuery(url) {
1253
1178
  }
1254
1179
 
1255
1180
  // src/commands/run.ts
1256
- import ora3 from "ora";
1257
- import { select as select3 } from "@inquirer/prompts";
1181
+ import ora4 from "ora";
1182
+ import { select as select4 } from "@inquirer/prompts";
1258
1183
 
1259
1184
  // src/lib/telemetry.ts
1260
- var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
1185
+ var CLI_VERSION = (true ? "3.4.1-dev.d74adb9" : void 0) ?? process.env.npm_package_version ?? "unknown";
1261
1186
  function getCliVersion() {
1262
1187
  return CLI_VERSION;
1263
1188
  }
@@ -1427,12 +1352,50 @@ var SEVERITY_BY_LEVEL = {
1427
1352
  warn: "warning",
1428
1353
  error: "error"
1429
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
+ }
1430
1393
  var MAX_MESSAGE_LENGTH = 500;
1431
1394
  var MAX_METADATA_VALUE_LENGTH = 200;
1432
1395
  var MAX_METADATA_ENTRIES = 20;
1433
1396
  var TRUNCATION_MARKER = "\u2026";
1434
1397
  function redact(message) {
1435
- return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
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>");
1436
1399
  }
1437
1400
  function truncate(message) {
1438
1401
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
@@ -1458,43 +1421,47 @@ function sanitiseMetadata(metadata) {
1458
1421
  }
1459
1422
  var RATE_LIMIT_WINDOW_MS = 6e4;
1460
1423
  var RATE_LIMIT_MAX_EVENTS = 30;
1461
- var windowStartedAt = 0;
1462
- var windowCount = 0;
1463
- var windowDroppedCount = 0;
1464
- function admitUnderRateLimit(now) {
1465
- if (now - windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1466
- if (windowDroppedCount > 0) {
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) {
1467
1433
  console.error(
1468
- `[runner-activity-telemetry] rate cap reached: dropped ${windowDroppedCount} ${windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min)`
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}"`
1469
1435
  );
1470
1436
  }
1471
- windowStartedAt = now;
1472
- windowCount = 0;
1473
- windowDroppedCount = 0;
1437
+ window.windowStartedAt = now;
1438
+ window.windowCount = 0;
1439
+ window.windowDroppedCount = 0;
1474
1440
  }
1475
- if (windowCount >= RATE_LIMIT_MAX_EVENTS) {
1476
- windowDroppedCount++;
1477
- if (windowDroppedCount === 1) {
1441
+ if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1442
+ window.windowDroppedCount++;
1443
+ if (window.windowDroppedCount === 1) {
1478
1444
  console.error(
1479
- `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
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}"`
1480
1446
  );
1481
1447
  }
1482
1448
  return false;
1483
1449
  }
1484
- windowCount++;
1450
+ window.windowCount++;
1485
1451
  return true;
1486
1452
  }
1487
1453
  function forwardRunnerActivity(entry, context) {
1488
1454
  try {
1489
1455
  if (!FORWARDED_LEVELS.has(entry.level)) return;
1490
1456
  if (!context.agentId || !context.authHeader) return;
1491
- if (!admitUnderRateLimit(Date.now())) return;
1457
+ const source = entry.source ?? "cli.run";
1458
+ if (!admitUnderRateLimit(source, Date.now())) return;
1492
1459
  const rawMessage = entry.error ?? entry.message ?? "";
1493
1460
  const message = truncate(redact(rawMessage));
1494
1461
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1495
1462
  severity: SEVERITY_BY_LEVEL[entry.level],
1496
1463
  message,
1497
- metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
1464
+ metadata: { ...sanitiseMetadata(entry.metadata), source },
1498
1465
  agentId: context.agentId
1499
1466
  });
1500
1467
  } catch (err) {
@@ -1505,8 +1472,8 @@ function forwardRunnerActivity(entry, context) {
1505
1472
  }
1506
1473
 
1507
1474
  // src/lib/opencode/session-db-recovery-report.ts
1508
- import { readFileSync as readFileSync2, unlinkSync } from "fs";
1509
- import { join as join2 } from "path";
1475
+ import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
1476
+ import { join as join2 } from "node:path";
1510
1477
  function sessionDbRecoveryReportPath(homeDir, env) {
1511
1478
  const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1512
1479
  return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
@@ -1689,6 +1656,11 @@ function isSessionDbRecoveryRecord(value) {
1689
1656
  );
1690
1657
  }
1691
1658
 
1659
+ // src/lib/opencode/auth.ts
1660
+ function buildOpenCodeBasicAuthHeader(password) {
1661
+ return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
1662
+ }
1663
+
1692
1664
  // src/lib/opencode/health.ts
1693
1665
  async function checkOpenCodeHealth(port) {
1694
1666
  try {
@@ -1706,6 +1678,27 @@ async function checkOpenCodeHealth(port) {
1706
1678
  return { healthy: false, error: message };
1707
1679
  }
1708
1680
  }
1681
+ async function checkOpenCode2Health(port, password) {
1682
+ try {
1683
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
1684
+ headers: {
1685
+ Authorization: buildOpenCodeBasicAuthHeader(password)
1686
+ },
1687
+ signal: AbortSignal.timeout(2e3)
1688
+ });
1689
+ if (response.status === 401) {
1690
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
1691
+ }
1692
+ if (!response.ok) {
1693
+ return { healthy: false, error: `HTTP ${response.status}` };
1694
+ }
1695
+ const data = await response.json().catch(() => ({}));
1696
+ return { healthy: true, version: data.version };
1697
+ } catch (error2) {
1698
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1699
+ return { healthy: false, error: message };
1700
+ }
1701
+ }
1709
1702
  async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1710
1703
  const startTime = Date.now();
1711
1704
  while (Date.now() - startTime < timeoutMs) {
@@ -1717,15 +1710,70 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1717
1710
  }
1718
1711
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1719
1712
  }
1713
+ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1714
+ const startTime = Date.now();
1715
+ while (Date.now() - startTime < timeoutMs) {
1716
+ const health = await checkOpenCode2Health(port, password);
1717
+ if (health.healthy || health.authFailed) {
1718
+ return health;
1719
+ }
1720
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
1721
+ }
1722
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1723
+ }
1724
+
1725
+ // src/lib/http-timeout.ts
1726
+ var REQUEST_TIMEOUT_MS = 6e4;
1727
+ function withRequestTimeout(fetchImpl, timeoutMs) {
1728
+ return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
1729
+ }
1730
+
1731
+ // src/lib/opencode/client.ts
1732
+ function redactPassword(message, password) {
1733
+ return message.replaceAll(password, "[redacted]");
1734
+ }
1735
+ function createOpenCodeClient(options) {
1736
+ const password = options.password ?? null;
1737
+ const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
1738
+ const baseUrl = `http://127.0.0.1:${options.port}`;
1739
+ return {
1740
+ port: options.port,
1741
+ version: options.version,
1742
+ password,
1743
+ async request(path, init, requestOptions) {
1744
+ const requestInit = options.version === "v2" && password !== null ? (() => {
1745
+ const headers = new Headers(init?.headers);
1746
+ headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
1747
+ return { ...init, headers };
1748
+ })() : init;
1749
+ try {
1750
+ const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
1751
+ if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
1752
+ const body = await response.text();
1753
+ throw new Error(
1754
+ `OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
1755
+ );
1756
+ }
1757
+ return response;
1758
+ } catch (error2) {
1759
+ if (options.version === "v2" && password !== null) {
1760
+ const message = error2 instanceof Error ? error2.message : String(error2);
1761
+ throw new Error(redactPassword(message, password));
1762
+ }
1763
+ throw error2;
1764
+ }
1765
+ }
1766
+ };
1767
+ }
1720
1768
 
1721
1769
  // src/lib/opencode/session-db-boot.ts
1722
- import { spawn as spawn2 } from "child_process";
1723
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1724
- import { homedir as homedir2 } from "os";
1725
- import { dirname as dirname2, resolve as resolvePath } from "path";
1770
+ import { spawn as spawn2 } from "node:child_process";
1771
+ import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
1772
+ import { homedir as homedir2 } from "node:os";
1773
+ import { dirname as dirname2, resolve as resolvePath } from "node:path";
1726
1774
 
1727
1775
  // src/lib/runner-synchroniser.ts
1728
- import { spawn } from "child_process";
1776
+ import { spawn } from "node:child_process";
1729
1777
  function appendError(stderr, error2) {
1730
1778
  const message = error2 instanceof Error ? error2.message : String(error2);
1731
1779
  return stderr === "" ? message : `${stderr}
@@ -2250,9 +2298,9 @@ async function restoreAndVerifySessionDb(options) {
2250
2298
  }
2251
2299
 
2252
2300
  // src/lib/opencode/session-db-provenance.ts
2253
- import { createRequire } from "module";
2254
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2255
- import { dirname as dirname3, join as join3 } from "path";
2301
+ import { createRequire } from "node:module";
2302
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2303
+ import { dirname as dirname3, join as join3 } from "node:path";
2256
2304
  var require2 = createRequire(import.meta.url);
2257
2305
  function readSessionDbMigrationIds(dbPath) {
2258
2306
  let db;
@@ -2380,15 +2428,23 @@ function isQueueValidatedVersion(version2) {
2380
2428
  if (!version2) return false;
2381
2429
  return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
2382
2430
  }
2383
- function buildOpenCodeVersionWarning(version2) {
2384
- if (isQueueValidatedVersion(version2)) return null;
2385
- const detected = version2 ? `v${version2}` : "unknown";
2431
+ function buildOpenCodeVersionWarning(version2, major) {
2432
+ if (major === "v2") return null;
2386
2433
  const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
2387
- return `Warning: opencode ${detected} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
2434
+ if (!version2) {
2435
+ return `Warning: the running opencode's version could not be determined from its health response, so queue validation could not be checked (validated: ${validated}). Compare against \`opencode --version\`; continuing anyway.`;
2436
+ }
2437
+ if (isQueueValidatedVersion(version2)) return null;
2438
+ return `Warning: opencode v${version2} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
2439
+ }
2440
+ function reportedOpenCodeVersion(input) {
2441
+ if (!input.connected) return null;
2442
+ return input.version || `${input.major}-unknown`;
2388
2443
  }
2389
2444
 
2390
2445
  // src/lib/opencode/process.ts
2391
2446
  import { execSync, spawn as spawn3 } from "child_process";
2447
+ import { randomBytes } from "node:crypto";
2392
2448
 
2393
2449
  // src/lib/process-stop.ts
2394
2450
  async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
@@ -2446,6 +2502,38 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2446
2502
 
2447
2503
  // src/lib/opencode/process.ts
2448
2504
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2505
+ var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
2506
+ var VALID_OPENCODE2_LOG_LEVELS = /* @__PURE__ */ new Set([
2507
+ "all",
2508
+ "trace",
2509
+ "debug",
2510
+ "info",
2511
+ "warn",
2512
+ "warning",
2513
+ "error",
2514
+ "fatal",
2515
+ "none"
2516
+ ]);
2517
+ function resolveOpenCodeLogLevel(env) {
2518
+ const raw = env.OPENCODE_LOG_LEVEL;
2519
+ if (!raw) return "INFO";
2520
+ const upper = raw.toUpperCase();
2521
+ if (VALID_OPENCODE_LOG_LEVELS.has(upper)) return upper;
2522
+ console.warn(
2523
+ `startOpenCode: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected DEBUG|INFO|WARN|ERROR) \u2014 using INFO`
2524
+ );
2525
+ return "INFO";
2526
+ }
2527
+ function resolveOpenCode2LogLevel(env) {
2528
+ const raw = env.OPENCODE_LOG_LEVEL;
2529
+ if (!raw) return "info";
2530
+ const lower = raw.toLowerCase();
2531
+ if (VALID_OPENCODE2_LOG_LEVELS.has(lower)) return lower;
2532
+ console.warn(
2533
+ `startOpenCode2: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected all|trace|debug|info|warn|warning|error|fatal|none) \u2014 using info`
2534
+ );
2535
+ return "info";
2536
+ }
2449
2537
  function getProcessCwd(pid) {
2450
2538
  const platform = process.platform;
2451
2539
  try {
@@ -2494,14 +2582,14 @@ function findAvailablePort(startPort, maxAttempts = 10) {
2494
2582
  }
2495
2583
  return null;
2496
2584
  }
2497
- function findOpenCodeProcesses() {
2585
+ function findProcessesByPattern(pgrepPattern, psPattern) {
2498
2586
  const instances = [];
2499
2587
  try {
2500
2588
  const platform = process.platform;
2501
2589
  if (platform === "darwin" || platform === "linux") {
2502
2590
  let pids = [];
2503
2591
  try {
2504
- const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
2592
+ const pgrepOutput = execSync(`pgrep -f "${pgrepPattern}"`, {
2505
2593
  encoding: "utf-8",
2506
2594
  stdio: ["pipe", "pipe", "pipe"]
2507
2595
  }).trim();
@@ -2510,7 +2598,7 @@ function findOpenCodeProcesses() {
2510
2598
  }
2511
2599
  } catch {
2512
2600
  try {
2513
- const psOutput = execSync('ps aux | grep -E "opencode (serve|--port)" | grep -v grep', {
2601
+ const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
2514
2602
  encoding: "utf-8",
2515
2603
  stdio: ["pipe", "pipe", "pipe"]
2516
2604
  }).trim();
@@ -2556,6 +2644,9 @@ function findOpenCodeProcesses() {
2556
2644
  }
2557
2645
  return instances;
2558
2646
  }
2647
+ function findOpenCodeProcesses() {
2648
+ return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
2649
+ }
2559
2650
  async function scanPortsForOpenCode() {
2560
2651
  const instances = [];
2561
2652
  const checks = OPENCODE_PORT_RANGE.map(async (port) => {
@@ -2602,7 +2693,7 @@ async function findHealthyOpenCodeInstances() {
2602
2693
  }
2603
2694
  async function startOpenCode(port, options = {}) {
2604
2695
  let command = "opencode";
2605
- const printLogs = options.inheritStdio ? ["--print-logs"] : [];
2696
+ const printLogs = options.inheritStdio ? ["--print-logs", "--log-level", resolveOpenCodeLogLevel(process.env)] : [];
2606
2697
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...printLogs];
2607
2698
  try {
2608
2699
  execSync("which opencode", { stdio: "ignore" });
@@ -2625,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
2625
2716
  });
2626
2717
  return child;
2627
2718
  }
2719
+ async function startOpenCode2(port, options = {}) {
2720
+ const password = randomBytes(24).toString("hex");
2721
+ let command = "opencode2";
2722
+ const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
2723
+ let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
2724
+ try {
2725
+ execSync("which opencode2", { stdio: "ignore" });
2726
+ } catch {
2727
+ command = "npx";
2728
+ args = [
2729
+ "-y",
2730
+ "-p",
2731
+ "@opencode-ai/cli@beta",
2732
+ "--",
2733
+ "opencode2",
2734
+ "serve",
2735
+ "--port",
2736
+ port.toString(),
2737
+ "--hostname",
2738
+ "127.0.0.1",
2739
+ ...logLevel
2740
+ ];
2741
+ }
2742
+ const child = spawn3(command, args, {
2743
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
2744
+ detached: true,
2745
+ stdio: options.inheritStdio ? "inherit" : "ignore",
2746
+ cwd: process.cwd()
2747
+ });
2748
+ return { child, password };
2749
+ }
2628
2750
  function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
2629
2751
  const sendSignal = (signal) => {
2630
2752
  if (process.platform === "win32") {
@@ -2659,6 +2781,19 @@ function isOpenCodeInstalled() {
2659
2781
  return false;
2660
2782
  }
2661
2783
  }
2784
+ function isOpenCode2Installed() {
2785
+ try {
2786
+ const platform = process.platform;
2787
+ if (platform === "win32") {
2788
+ execSync2("where opencode2", { stdio: "ignore" });
2789
+ } else {
2790
+ execSync2("which opencode2", { stdio: "ignore" });
2791
+ }
2792
+ return true;
2793
+ } catch {
2794
+ return false;
2795
+ }
2796
+ }
2662
2797
  async function promptOpenCodeInstall(interactive) {
2663
2798
  if (!interactive) {
2664
2799
  console.log(
@@ -2668,7 +2803,11 @@ async function promptOpenCodeInstall(interactive) {
2668
2803
  install_url: OPENCODE_INSTALL_URL,
2669
2804
  install_commands: {
2670
2805
  npm: "npm install -g opencode-ai",
2671
- curl: "curl -fsSL https://opencode.ai/install.sh | sh"
2806
+ curl: "curl -fsSL https://opencode.ai/install.sh | sh",
2807
+ v2: {
2808
+ npm: "npm install -g @opencode-ai/cli@beta",
2809
+ curl: "curl -fsSL https://opencode.ai/v2/install | bash"
2810
+ }
2672
2811
  }
2673
2812
  })
2674
2813
  );
@@ -2755,61 +2894,534 @@ function buildNoProviderWarning(hasProvider) {
2755
2894
  return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
2756
2895
  }
2757
2896
 
2758
- // src/lib/http-timeout.ts
2759
- var REQUEST_TIMEOUT_MS = 6e4;
2760
- function withRequestTimeout(fetchImpl, timeoutMs) {
2761
- return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
2897
+ // src/lib/opencode/session-v2.ts
2898
+ function isRecord(value) {
2899
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2762
2900
  }
2763
-
2764
- // src/lib/opencode/session.ts
2765
- function timedFetch(input, init) {
2766
- return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
2901
+ function finiteNumber(value) {
2902
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2767
2903
  }
2768
- function opencodeBase(port) {
2769
- return `http://127.0.0.1:${port}`;
2904
+ function adaptTime(value) {
2905
+ if (!isRecord(value)) return void 0;
2906
+ const created = finiteNumber(value.created);
2907
+ const completed = finiteNumber(value.completed);
2908
+ if (created === void 0 && completed === void 0) return void 0;
2909
+ return {
2910
+ ...created !== void 0 ? { created } : {},
2911
+ ...completed !== void 0 ? { completed } : {}
2912
+ };
2770
2913
  }
2771
- async function getOpenCodeDirectory(port) {
2772
- try {
2773
- const res = await timedFetch(`${opencodeBase(port)}/path`);
2774
- if (!res.ok) return null;
2775
- const body = await res.json();
2776
- const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
2777
- return dir && dir.trim() ? dir.trim() : null;
2778
- } catch {
2779
- return null;
2914
+ function adaptTokens(value) {
2915
+ if (!isRecord(value)) return void 0;
2916
+ const input = finiteNumber(value.input);
2917
+ const output = finiteNumber(value.output);
2918
+ const reasoning = finiteNumber(value.reasoning);
2919
+ const cache = isRecord(value.cache) ? {
2920
+ ...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
2921
+ ...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
2922
+ } : void 0;
2923
+ if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
2924
+ return void 0;
2780
2925
  }
2926
+ return {
2927
+ ...input !== void 0 ? { input } : {},
2928
+ ...output !== void 0 ? { output } : {},
2929
+ ...reasoning !== void 0 ? { reasoning } : {},
2930
+ ...cache ? { cache } : {}
2931
+ };
2781
2932
  }
2782
- function roleOf(m) {
2783
- if (!m || typeof m !== "object") return void 0;
2784
- if (typeof m.role === "string") return m.role;
2785
- const infoRole = m.info?.role;
2786
- return typeof infoRole === "string" ? infoRole : void 0;
2933
+ function adaptMessageInfo(value, role) {
2934
+ const info = {
2935
+ id: value.id,
2936
+ role
2937
+ };
2938
+ const time = adaptTime(value.time);
2939
+ if (time) info.time = time;
2940
+ if (typeof value.finish === "string") info.finish = value.finish;
2941
+ if ("error" in value) info.error = value.error;
2942
+ if (typeof value.agent === "string") info.agent = value.agent;
2943
+ if (isRecord(value.model)) {
2944
+ if (typeof value.model.id === "string") info.modelID = value.model.id;
2945
+ if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
2946
+ }
2947
+ if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
2948
+ const tokens = adaptTokens(value.tokens);
2949
+ if (tokens) info.tokens = tokens;
2950
+ return info;
2951
+ }
2952
+ function adaptV2Message(value) {
2953
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
2954
+ return null;
2955
+ }
2956
+ if (value.type === "user") {
2957
+ if (typeof value.text !== "string") return null;
2958
+ return {
2959
+ info: adaptMessageInfo(value, "user"),
2960
+ parts: [{ type: "text", text: value.text }]
2961
+ };
2962
+ }
2963
+ if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
2964
+ const parts = [];
2965
+ for (const content of value.content) {
2966
+ if (!isRecord(content) || typeof content.type !== "string") return null;
2967
+ if (content.type === "text") {
2968
+ if (typeof content.text !== "string") return null;
2969
+ parts.push({ type: "text", text: content.text });
2970
+ } else {
2971
+ parts.push({ type: content.type });
2972
+ }
2973
+ }
2974
+ return {
2975
+ info: adaptMessageInfo(value, "assistant"),
2976
+ parts
2977
+ };
2787
2978
  }
2788
- function completedOf(m) {
2789
- if (!m || typeof m !== "object") return void 0;
2790
- return m.info?.time?.completed ?? m.time?.completed;
2979
+ function adaptFormTool(value) {
2980
+ if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
2981
+ return void 0;
2982
+ }
2983
+ return { messageID: value.messageID, callID: value.id };
2791
2984
  }
2792
- function createdOf(m) {
2793
- if (!m || typeof m !== "object") return void 0;
2794
- return m.info?.time?.created ?? m.time?.created;
2985
+ function adaptV2FormWire(value) {
2986
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
2987
+ return null;
2988
+ }
2989
+ return value;
2795
2990
  }
2796
- function idOf(m) {
2797
- if (!m || typeof m !== "object") return void 0;
2798
- if (typeof m.id === "string") return m.id;
2799
- const infoId = m.info?.id;
2800
- return typeof infoId === "string" ? infoId : void 0;
2991
+ function adaptV2FormField(value, header) {
2992
+ if (!isRecord(value)) return null;
2993
+ const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
2994
+ if (!question) return null;
2995
+ const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
2996
+ if (!isRecord(option)) return [];
2997
+ const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
2998
+ if (!label) return [];
2999
+ return [
3000
+ {
3001
+ label,
3002
+ description: typeof option.description === "string" ? option.description : ""
3003
+ }
3004
+ ];
3005
+ }) : [];
3006
+ return { question, header, options };
3007
+ }
3008
+ function adaptV2Form(value) {
3009
+ const form = adaptV2FormWire(value);
3010
+ if (!form || !Array.isArray(form.fields)) return null;
3011
+ const header = typeof form.title === "string" ? form.title : "";
3012
+ const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
3013
+ if (questions.length === 0) return null;
3014
+ const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
3015
+ return {
3016
+ id: form.id,
3017
+ sessionID: form.sessionID,
3018
+ questions,
3019
+ ...tool ? { tool } : {},
3020
+ raw: form
3021
+ };
2801
3022
  }
2802
- function parentIdOf(m) {
2803
- if (!m || typeof m !== "object") return void 0;
2804
- if (typeof m.parentID === "string") return m.parentID;
2805
- const infoParent = m.info?.parentID;
2806
- return typeof infoParent === "string" ? infoParent : void 0;
3023
+ function adaptV2FormList(value) {
3024
+ if (!isRecord(value) || !Array.isArray(value.data)) return null;
3025
+ return value.data.map(adaptV2Form).filter((form) => form !== null);
2807
3026
  }
2808
- function finishOf(m) {
2809
- if (!m || typeof m !== "object") return void 0;
2810
- if (typeof m.finish === "string") return m.finish;
2811
- const infoFinish = m.info?.finish;
2812
- return typeof infoFinish === "string" ? infoFinish : void 0;
3027
+ function adaptPattern(value) {
3028
+ if (typeof value === "string" && value.length > 0) return value;
3029
+ if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
3030
+ return value;
3031
+ }
3032
+ return void 0;
3033
+ }
3034
+ function adaptV2PermissionWire(value) {
3035
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
3036
+ return null;
3037
+ }
3038
+ if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
3039
+ return value;
3040
+ }
3041
+ function adaptV2Permission(value) {
3042
+ const permission = adaptV2PermissionWire(value);
3043
+ if (!permission) return null;
3044
+ const type = permission.permission ?? permission.action;
3045
+ if (!type) return null;
3046
+ const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
3047
+ const time = isRecord(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
3048
+ return {
3049
+ id: permission.id,
3050
+ type,
3051
+ sessionID: permission.sessionID,
3052
+ metadata: isRecord(permission.metadata) ? permission.metadata : {},
3053
+ raw: permission,
3054
+ ...pattern !== void 0 ? { pattern } : {},
3055
+ ...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
3056
+ ...typeof permission.callID === "string" ? { callID: permission.callID } : {},
3057
+ ...typeof permission.title === "string" ? { title: permission.title } : {},
3058
+ ...time ? { time } : {}
3059
+ };
3060
+ }
3061
+ function adaptV2PermissionList(value) {
3062
+ if (!isRecord(value) || !Array.isArray(value.data)) return null;
3063
+ return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
3064
+ }
3065
+ function adaptV2Session(value) {
3066
+ if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
3067
+ const time = isRecord(value.time) ? {
3068
+ ...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
3069
+ ...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
3070
+ } : void 0;
3071
+ return {
3072
+ id: value.id,
3073
+ ...typeof value.title === "string" ? { title: value.title } : {},
3074
+ ...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
3075
+ ...time && Object.keys(time).length > 0 ? { time } : {}
3076
+ };
3077
+ }
3078
+ function adaptV2SessionList(value) {
3079
+ if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
3080
+ return {
3081
+ data: value.data.map(adaptV2Session).filter((session) => session !== null),
3082
+ cursor: value.cursor
3083
+ };
3084
+ }
3085
+ function adaptV2Location(value) {
3086
+ const candidates = [
3087
+ value,
3088
+ isRecord(value) ? value.data : void 0,
3089
+ isRecord(value) ? value.location : void 0
3090
+ ];
3091
+ for (const candidate of candidates) {
3092
+ if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
3093
+ const directory = candidate.directory.trim();
3094
+ if (directory) return directory;
3095
+ }
3096
+ return null;
3097
+ }
3098
+ function adaptV2Messages(value) {
3099
+ if (!isRecord(value) || !Array.isArray(value.data)) return [];
3100
+ return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
3101
+ }
3102
+ async function readJson(response) {
3103
+ try {
3104
+ return await response.json();
3105
+ } catch (error2) {
3106
+ throw new Error(
3107
+ `OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
3108
+ );
3109
+ }
3110
+ }
3111
+ async function readData(client, path, init) {
3112
+ const response = await client.request(path, init);
3113
+ const body = await readJson(response);
3114
+ if (!isRecord(body) || !("data" in body)) {
3115
+ throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
3116
+ }
3117
+ return body.data;
3118
+ }
3119
+ var OpenCodeV2PromptAckError = class extends Error {
3120
+ constructor(message) {
3121
+ super(message);
3122
+ this.name = "OpenCodeV2PromptAckError";
3123
+ }
3124
+ };
3125
+ async function getOpenCodeDirectoryV2(client) {
3126
+ try {
3127
+ return adaptV2Location(await readJson(await client.request("/api/location")));
3128
+ } catch (error2) {
3129
+ console.error(
3130
+ `[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3131
+ );
3132
+ return null;
3133
+ }
3134
+ }
3135
+ async function createV2Session(client, directory) {
3136
+ const data = await readData(client, "/api/session", {
3137
+ method: "POST",
3138
+ headers: { "Content-Type": "application/json" },
3139
+ body: JSON.stringify({ location: { directory } })
3140
+ });
3141
+ if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
3142
+ throw new Error("OpenCode V2 create session response was missing data.id");
3143
+ }
3144
+ return data.id;
3145
+ }
3146
+ async function getV2Session(client, sessionId) {
3147
+ const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
3148
+ const session = adaptV2Session(data);
3149
+ if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
3150
+ return session;
3151
+ }
3152
+ async function listV2SessionPage(client, cursor) {
3153
+ const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
3154
+ try {
3155
+ return adaptV2SessionList(await readJson(await client.request(path)));
3156
+ } catch (error2) {
3157
+ console.error(
3158
+ `[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3159
+ );
3160
+ return null;
3161
+ }
3162
+ }
3163
+ async function listV2Sessions(client) {
3164
+ const sessions = [];
3165
+ const seenCursors = /* @__PURE__ */ new Set();
3166
+ let cursor;
3167
+ let hasNextPage = true;
3168
+ try {
3169
+ while (hasNextPage) {
3170
+ const page = await listV2SessionPage(client, cursor);
3171
+ if (!page) return null;
3172
+ sessions.push(...page.data);
3173
+ const next = page.cursor.next;
3174
+ if (next === void 0 || next === null) {
3175
+ hasNextPage = false;
3176
+ continue;
3177
+ }
3178
+ if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
3179
+ throw new Error("OpenCode V2 session list contained an invalid next cursor");
3180
+ }
3181
+ seenCursors.add(next);
3182
+ cursor = next;
3183
+ }
3184
+ return sessions;
3185
+ } catch (error2) {
3186
+ console.error(
3187
+ `[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3188
+ );
3189
+ return null;
3190
+ }
3191
+ }
3192
+ async function deleteV2Session(client, sessionId) {
3193
+ try {
3194
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3195
+ return true;
3196
+ } catch (error2) {
3197
+ console.error(
3198
+ `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3199
+ );
3200
+ return false;
3201
+ }
3202
+ }
3203
+ async function v2SessionExists(client, sessionId) {
3204
+ try {
3205
+ const response = await client.request(
3206
+ `/api/session/${encodeURIComponent(sessionId)}`,
3207
+ void 0,
3208
+ { allowStatuses: [404] }
3209
+ );
3210
+ return response.status === 404 ? false : true;
3211
+ } catch (error2) {
3212
+ console.error(
3213
+ `[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3214
+ );
3215
+ return null;
3216
+ }
3217
+ }
3218
+ async function sendV2Prompt(client, sessionId, text) {
3219
+ const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
3220
+ const response = await client.request(path, {
3221
+ method: "POST",
3222
+ headers: { "Content-Type": "application/json" },
3223
+ body: JSON.stringify({ text, delivery: "queue" })
3224
+ });
3225
+ let body;
3226
+ try {
3227
+ body = await readJson(response);
3228
+ } catch (error2) {
3229
+ throw new OpenCodeV2PromptAckError(
3230
+ `OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
3231
+ );
3232
+ }
3233
+ const data = isRecord(body) && "data" in body ? body.data : void 0;
3234
+ if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
3235
+ throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
3236
+ }
3237
+ return data.id;
3238
+ }
3239
+ async function getV2SessionMessages(client, sessionId) {
3240
+ const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
3241
+ try {
3242
+ const body = await readJson(await client.request(path));
3243
+ if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
3244
+ return adaptV2Messages(body);
3245
+ } catch (error2) {
3246
+ console.error(
3247
+ `[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3248
+ );
3249
+ return null;
3250
+ }
3251
+ }
3252
+ async function listV2Forms(client, sessionId) {
3253
+ const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
3254
+ try {
3255
+ return adaptV2FormList(await readJson(await client.request(path)));
3256
+ } catch (error2) {
3257
+ console.error(
3258
+ `[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3259
+ );
3260
+ return null;
3261
+ }
3262
+ }
3263
+ async function listV2Permissions(client, sessionId) {
3264
+ const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
3265
+ try {
3266
+ return adaptV2PermissionList(await readJson(await client.request(path)));
3267
+ } catch (error2) {
3268
+ console.error(
3269
+ `[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3270
+ );
3271
+ return null;
3272
+ }
3273
+ }
3274
+ async function getV2ActiveSessions(client) {
3275
+ try {
3276
+ const body = await readJson(await client.request("/api/session/active"));
3277
+ if (!isRecord(body) || !isRecord(body.data)) return null;
3278
+ return body.data;
3279
+ } catch (error2) {
3280
+ console.error(
3281
+ `[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3282
+ );
3283
+ return null;
3284
+ }
3285
+ }
3286
+ async function isV2SessionOngoing(client, sessionId) {
3287
+ const activeSessions = await getV2ActiveSessions(client);
3288
+ if (activeSessions === null) return null;
3289
+ return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
3290
+ }
3291
+ function sessionErrorReason(value) {
3292
+ if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
3293
+ if (isRecord(value)) {
3294
+ const data = isRecord(value.data) ? value.data : void 0;
3295
+ const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
3296
+ if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
3297
+ }
3298
+ return "OpenCode reported a session error with no details";
3299
+ }
3300
+ function adaptV2SessionErrorEvent(value) {
3301
+ let parsed = value;
3302
+ if (typeof value === "string") {
3303
+ try {
3304
+ parsed = JSON.parse(value);
3305
+ } catch (error2) {
3306
+ void error2;
3307
+ return null;
3308
+ }
3309
+ }
3310
+ if (!isRecord(parsed)) return null;
3311
+ try {
3312
+ const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
3313
+ if (establishedShape) return establishedShape;
3314
+ } catch (error2) {
3315
+ void error2;
3316
+ }
3317
+ const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
3318
+ for (const event of candidates) {
3319
+ if (event.type !== "session.error") continue;
3320
+ const properties = [event.properties, event.data, event].find(isRecord);
3321
+ if (!properties) continue;
3322
+ const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
3323
+ if (!sessionId) continue;
3324
+ return {
3325
+ sessionId,
3326
+ reason: sessionErrorReason(properties.error ?? properties)
3327
+ };
3328
+ }
3329
+ return null;
3330
+ }
3331
+ async function readV2SessionErrorStream(client, options) {
3332
+ let reader = null;
3333
+ try {
3334
+ const response = await client.request("/api/event", {
3335
+ headers: { accept: "text/event-stream" },
3336
+ signal: options.signal
3337
+ });
3338
+ if (!response.ok || !response.body) {
3339
+ return { reason: "unavailable", detail: `HTTP ${response.status}` };
3340
+ }
3341
+ reader = response.body.getReader();
3342
+ const decoder = new TextDecoder();
3343
+ let buffer = "";
3344
+ const processLine = (line) => {
3345
+ const trimmed = line.trimEnd();
3346
+ if (!trimmed.startsWith("data:")) return;
3347
+ const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
3348
+ if (event) options.onSessionError(event);
3349
+ };
3350
+ while (true) {
3351
+ const { done, value } = await reader.read();
3352
+ if (done) return { reason: "ended" };
3353
+ buffer += decoder.decode(value, { stream: true });
3354
+ const lines = buffer.split("\n");
3355
+ buffer = lines.pop() ?? "";
3356
+ for (const line of lines) processLine(line);
3357
+ }
3358
+ } catch (error2) {
3359
+ if (options.signal.aborted) return { reason: "aborted" };
3360
+ return {
3361
+ reason: "unavailable",
3362
+ detail: error2 instanceof Error ? error2.message : String(error2)
3363
+ };
3364
+ } finally {
3365
+ if (reader) void reader.cancel().catch(() => void 0);
3366
+ }
3367
+ }
3368
+
3369
+ // src/lib/opencode/session.ts
3370
+ var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
3371
+ function timedFetch(input, init) {
3372
+ return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
3373
+ }
3374
+ function requestWithClient(port, client, path, init, options) {
3375
+ return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
3376
+ }
3377
+ function opencodeBase(port) {
3378
+ return `http://127.0.0.1:${port}`;
3379
+ }
3380
+ async function getOpenCodeDirectory(port, client) {
3381
+ try {
3382
+ const res = await requestWithClient(port, client, "/path");
3383
+ if (!res.ok) return null;
3384
+ const body = await res.json();
3385
+ const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
3386
+ return dir && dir.trim() ? dir.trim() : null;
3387
+ } catch (error2) {
3388
+ console.error(
3389
+ `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3390
+ );
3391
+ return null;
3392
+ }
3393
+ }
3394
+ function roleOf(m) {
3395
+ if (!m || typeof m !== "object") return void 0;
3396
+ if (typeof m.role === "string") return m.role;
3397
+ const infoRole = m.info?.role;
3398
+ return typeof infoRole === "string" ? infoRole : void 0;
3399
+ }
3400
+ function completedOf(m) {
3401
+ if (!m || typeof m !== "object") return void 0;
3402
+ return m.info?.time?.completed ?? m.time?.completed;
3403
+ }
3404
+ function createdOf(m) {
3405
+ if (!m || typeof m !== "object") return void 0;
3406
+ return m.info?.time?.created ?? m.time?.created;
3407
+ }
3408
+ function idOf(m) {
3409
+ if (!m || typeof m !== "object") return void 0;
3410
+ if (typeof m.id === "string") return m.id;
3411
+ const infoId = m.info?.id;
3412
+ return typeof infoId === "string" ? infoId : void 0;
3413
+ }
3414
+ function parentIdOf(m) {
3415
+ if (!m || typeof m !== "object") return void 0;
3416
+ if (typeof m.parentID === "string") return m.parentID;
3417
+ const infoParent = m.info?.parentID;
3418
+ return typeof infoParent === "string" ? infoParent : void 0;
3419
+ }
3420
+ function finishOf(m) {
3421
+ if (!m || typeof m !== "object") return void 0;
3422
+ if (typeof m.finish === "string") return m.finish;
3423
+ const infoFinish = m.info?.finish;
3424
+ return typeof infoFinish === "string" ? infoFinish : void 0;
2813
3425
  }
2814
3426
  function errorOf(m) {
2815
3427
  if (!m || typeof m !== "object") return void 0;
@@ -2819,16 +3431,48 @@ function isAssistantInFlight(m) {
2819
3431
  if (completedOf(m) == null) return true;
2820
3432
  return finishOf(m) === "tool-calls";
2821
3433
  }
2822
- async function getSessionMessages(port, sessionId) {
3434
+ async function getSessionMessages(port, sessionId, client) {
2823
3435
  try {
2824
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/message`);
3436
+ const path = `/session/${sessionId}/message`;
3437
+ const res = await requestWithClient(port, client, path);
2825
3438
  if (!res.ok) return null;
2826
3439
  const body = await res.json();
2827
3440
  return Array.isArray(body) ? body : null;
2828
- } catch {
3441
+ } catch (error2) {
3442
+ console.error(
3443
+ `[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3444
+ );
2829
3445
  return null;
2830
3446
  }
2831
3447
  }
3448
+ async function fetchSessionMessages(port, sessionId, client) {
3449
+ const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
3450
+ if (!response.ok) return null;
3451
+ const body = await response.json();
3452
+ return Array.isArray(body) ? body : null;
3453
+ }
3454
+ async function pollSessionMessagesForRedrive(port, sessionId, client) {
3455
+ try {
3456
+ const response = await requestWithClient(
3457
+ port,
3458
+ client,
3459
+ `/session/${sessionId}/message`,
3460
+ void 0,
3461
+ { allowStatuses: ALL_HTTP_STATUSES }
3462
+ );
3463
+ if (!response.ok) {
3464
+ return { ok: false, status: response.status, body: await response.text(), malformed: false };
3465
+ }
3466
+ const body = await response.json();
3467
+ if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
3468
+ return { ok: true, messages: body };
3469
+ } catch (error2) {
3470
+ console.error(
3471
+ `[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3472
+ );
3473
+ return { ok: false, status: null, body: "", malformed: false };
3474
+ }
3475
+ }
2832
3476
  function isSessionActivelyGenerating(messages) {
2833
3477
  if (!messages || messages.length === 0) return false;
2834
3478
  const last = messages[messages.length - 1];
@@ -2849,27 +3493,37 @@ function sessionLastActivityMs(session) {
2849
3493
  }
2850
3494
  return null;
2851
3495
  }
2852
- async function listSessions(port) {
3496
+ async function listSessions(port, client) {
3497
+ if (client?.version === "v2") return listV2Sessions(client);
2853
3498
  try {
2854
- const res = await timedFetch(`${opencodeBase(port)}/session`);
3499
+ const res = await requestWithClient(port, client, "/session");
2855
3500
  if (!res.ok) return null;
2856
3501
  const body = await res.json();
2857
3502
  return Array.isArray(body) ? body : null;
2858
- } catch {
3503
+ } catch (error2) {
3504
+ console.error(
3505
+ `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3506
+ );
2859
3507
  return null;
2860
3508
  }
2861
3509
  }
2862
- async function deleteSession(port, id) {
3510
+ async function deleteSession(port, id, client) {
3511
+ if (client?.version === "v2") return deleteV2Session(client, id);
2863
3512
  try {
2864
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`, { method: "DELETE" });
3513
+ const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2865
3514
  return res.status >= 200 && res.status < 300;
2866
- } catch {
3515
+ } catch (error2) {
3516
+ console.error(
3517
+ `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3518
+ );
2867
3519
  return false;
2868
3520
  }
2869
3521
  }
2870
- async function sessionExists(port, id) {
3522
+ async function sessionExists(port, id, client) {
2871
3523
  try {
2872
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`);
3524
+ const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
3525
+ allowStatuses: [404]
3526
+ });
2873
3527
  if (res.status >= 200 && res.status < 300) return true;
2874
3528
  if (res.status === 404) return false;
2875
3529
  return null;
@@ -2877,9 +3531,22 @@ async function sessionExists(port, id) {
2877
3531
  return null;
2878
3532
  }
2879
3533
  }
2880
- async function getSessionStatuses(port) {
3534
+ async function getOpenCodeSession(port, id, client) {
2881
3535
  try {
2882
- const res = await timedFetch(`${opencodeBase(port)}/session/status`);
3536
+ const response = await requestWithClient(port, client, `/session/${id}`);
3537
+ const body = await response.json();
3538
+ return body && typeof body === "object" && !Array.isArray(body) ? body : null;
3539
+ } catch (error2) {
3540
+ console.error(
3541
+ `[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3542
+ );
3543
+ return null;
3544
+ }
3545
+ }
3546
+ async function getSessionStatuses(port, client) {
3547
+ if (client?.version === "v2") return null;
3548
+ try {
3549
+ const res = await requestWithClient(port, client, "/session/status");
2883
3550
  if (!res.ok) {
2884
3551
  console.error(
2885
3552
  `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
@@ -2901,22 +3568,28 @@ async function getSessionStatuses(port) {
2901
3568
  return null;
2902
3569
  }
2903
3570
  }
2904
- async function isSessionOngoing(port, id) {
2905
- const map = await getSessionStatuses(port);
3571
+ async function isSessionOngoing(port, id, client) {
3572
+ if (client?.version === "v2") return isV2SessionOngoing(client, id);
3573
+ const map = await getSessionStatuses(port, client);
2906
3574
  if (map == null) return null;
2907
3575
  const entry = map[id];
2908
3576
  return entry != null && entry.type !== "idle";
2909
3577
  }
2910
- async function createOpenCodeSession(port, directory) {
2911
- const url = new URL(`${opencodeBase(port)}/session`);
2912
- if (directory && directory.trim()) {
2913
- url.searchParams.set("directory", directory.trim());
2914
- }
2915
- const response = await timedFetch(url, {
2916
- method: "POST",
2917
- headers: { "Content-Type": "application/json" },
2918
- body: JSON.stringify({})
2919
- });
3578
+ async function createOpenCodeSession(port, directory, client) {
3579
+ const path = new URL(`${opencodeBase(port)}/session`);
3580
+ if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
3581
+ const requestPath = `${path.pathname}${path.search}`;
3582
+ const response = await requestWithClient(
3583
+ port,
3584
+ client,
3585
+ requestPath,
3586
+ {
3587
+ method: "POST",
3588
+ headers: { "Content-Type": "application/json" },
3589
+ body: JSON.stringify({})
3590
+ },
3591
+ { allowStatuses: ALL_HTTP_STATUSES }
3592
+ );
2920
3593
  if (!response.ok) {
2921
3594
  const text = await response.text().catch(() => "");
2922
3595
  throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
@@ -2924,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
2924
3597
  const data = await response.json();
2925
3598
  return data.id;
2926
3599
  }
2927
- async function getModelAttachmentCapability(port, model) {
3600
+ async function getModelAttachmentCapability(port, model, client) {
2928
3601
  const { model: baseModel } = splitModelVariant(model);
3602
+ if (client?.version === "v2") {
3603
+ console.error(
3604
+ `[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
3605
+ );
3606
+ return null;
3607
+ }
2929
3608
  try {
2930
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
3609
+ const res = await requestWithClient(port, client, "/config/providers");
2931
3610
  if (!res.ok) {
2932
3611
  console.error(
2933
3612
  `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3047,19 +3726,43 @@ function applyModelOptions(body, options) {
3047
3726
  }
3048
3727
  if (variant) body.variant = variant;
3049
3728
  }
3729
+ async function listOpenCodeQuestions(port, client) {
3730
+ try {
3731
+ const response = await requestWithClient(port, client, "/question");
3732
+ const body = await response.json();
3733
+ return Array.isArray(body) ? body : null;
3734
+ } catch (error2) {
3735
+ console.error(
3736
+ `[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3737
+ );
3738
+ return null;
3739
+ }
3740
+ }
3741
+ async function listOpenCodePermissions(port, client) {
3742
+ try {
3743
+ const response = await requestWithClient(port, client, "/permission");
3744
+ const body = await response.json();
3745
+ return Array.isArray(body) ? body : null;
3746
+ } catch (error2) {
3747
+ console.error(
3748
+ `[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3749
+ );
3750
+ return null;
3751
+ }
3752
+ }
3050
3753
  function messageText(m) {
3051
3754
  if (!m || !Array.isArray(m.parts)) return "";
3052
3755
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
3053
3756
  }
3054
- async function sendPromptAsync(port, sessionId, content, options, attachments) {
3055
- const before = await getSessionMessages(port, sessionId);
3757
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3758
+ const before = await getSessionMessages(port, sessionId, client);
3056
3759
  const knownUserIds = new Set(
3057
3760
  (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3058
3761
  );
3059
3762
  const parts = [{ type: "text", text: content }];
3060
3763
  let pendingOutcomes = null;
3061
3764
  if (attachments && attachments.inputs.length > 0) {
3062
- const capable = await getModelAttachmentCapability(port, options?.model);
3765
+ const capable = await getModelAttachmentCapability(port, options?.model, client);
3063
3766
  const {
3064
3767
  parts: fileParts,
3065
3768
  outcomes,
@@ -3072,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3072
3775
  parts
3073
3776
  };
3074
3777
  applyModelOptions(body, options);
3075
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
3076
- method: "POST",
3077
- headers: { "Content-Type": "application/json" },
3078
- body: JSON.stringify(body)
3079
- });
3778
+ const res = await requestWithClient(
3779
+ port,
3780
+ client,
3781
+ `/session/${sessionId}/prompt_async`,
3782
+ {
3783
+ method: "POST",
3784
+ headers: { "Content-Type": "application/json" },
3785
+ body: JSON.stringify(body)
3786
+ },
3787
+ { allowStatuses: ALL_HTTP_STATUSES }
3788
+ );
3080
3789
  if (res.status < 200 || res.status >= 300) {
3081
3790
  const text = await res.text().catch(() => "");
3082
3791
  const { variant } = splitModelVariant(options?.model);
@@ -3087,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3087
3796
  const READ_BACK_ATTEMPTS = 5;
3088
3797
  const READ_BACK_DELAY_MS = 150;
3089
3798
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
3090
- const after = await getSessionMessages(port, sessionId);
3799
+ const after = await getSessionMessages(port, sessionId, client);
3091
3800
  if (after) {
3092
3801
  let best = null;
3093
3802
  for (const m of after) {
@@ -3190,21 +3899,74 @@ function collectSubagentSessions(messages, userMessageId) {
3190
3899
  }
3191
3900
  return refs;
3192
3901
  }
3193
- function messageUsage(messages, userMessageId) {
3194
- if (!messages || messages.length === 0) return null;
3195
- const byParentAll = messages.filter(
3196
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3902
+ function finiteNumber2(value) {
3903
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3904
+ }
3905
+ function taskCallModel(value) {
3906
+ if (!value || typeof value !== "object") return null;
3907
+ const model = value;
3908
+ const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
3909
+ const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
3910
+ return modelID || providerID ? { modelID, providerID } : null;
3911
+ }
3912
+ function collectTaskCalls(messages, userMessageId) {
3913
+ if (!messages || messages.length === 0) return [];
3914
+ const calls = [];
3915
+ for (const message of messages) {
3916
+ if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
3917
+ for (const part of message.parts ?? []) {
3918
+ if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
3919
+ continue;
3920
+ }
3921
+ const rawName = part.state.input?.subagent_type;
3922
+ const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
3923
+ const metadata = part.state.metadata;
3924
+ calls.push({
3925
+ callID: part.callID,
3926
+ subagentName,
3927
+ childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
3928
+ parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3929
+ model: taskCallModel(metadata?.model),
3930
+ status: part.state.status ?? "unknown",
3931
+ timeStart: finiteNumber2(part.state.time?.start),
3932
+ timeEnd: finiteNumber2(part.state.time?.end)
3933
+ });
3934
+ }
3935
+ }
3936
+ return calls;
3937
+ }
3938
+ function attributeTaskCallUsage(messages, windows) {
3939
+ const eligibleWindows = windows.filter(
3940
+ (window) => window.timeStart !== null && Number.isFinite(window.timeStart)
3197
3941
  );
3198
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3199
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3200
- let correlated;
3201
- if (byParent.length > 0) {
3202
- correlated = byParent;
3203
- } else {
3204
- const reply = findAssistantReplyAfter(messages, userMessageId);
3205
- correlated = reply ? [reply] : [];
3942
+ const assignments = /* @__PURE__ */ new Map();
3943
+ for (const window of eligibleWindows) assignments.set(window.callID, []);
3944
+ const unattributed = [];
3945
+ for (const message of messages ?? []) {
3946
+ if (roleOf(message) !== "assistant") continue;
3947
+ const created = finiteNumber2(createdOf(message));
3948
+ const matching = created === null ? [] : eligibleWindows.filter(
3949
+ (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3950
+ );
3951
+ if (matching.length === 0) {
3952
+ unattributed.push(message);
3953
+ continue;
3954
+ }
3955
+ matching.sort((a, b) => a.timeStart - b.timeStart);
3956
+ assignments.get(matching[0].callID)?.push(message);
3206
3957
  }
3207
- if (correlated.length === 0) return null;
3958
+ return {
3959
+ invocations: eligibleWindows.map((window) => {
3960
+ const assigned = assignments.get(window.callID) ?? [];
3961
+ return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
3962
+ }),
3963
+ unattributed
3964
+ };
3965
+ }
3966
+ function sumAssistantUsage(messages) {
3967
+ if (!messages || messages.length === 0) return null;
3968
+ const nonErrored = messages.filter((message) => errorOf(message) == null);
3969
+ const selected = nonErrored.length > 0 ? nonErrored : messages;
3208
3970
  let sawAnyUsage = false;
3209
3971
  let inputSum = 0;
3210
3972
  let outputSum = 0;
@@ -3215,7 +3977,7 @@ function messageUsage(messages, userMessageId) {
3215
3977
  let sawCost = false;
3216
3978
  let modelId = null;
3217
3979
  let providerId = null;
3218
- for (const m of correlated) {
3980
+ for (const m of selected) {
3219
3981
  const info = m.info;
3220
3982
  if (!info) continue;
3221
3983
  const tokens = info.tokens;
@@ -3250,12 +4012,28 @@ function messageUsage(messages, userMessageId) {
3250
4012
  usage_tokens_reasoning: reasoningSum,
3251
4013
  usage_tokens_cache_read: cacheReadSum,
3252
4014
  usage_tokens_cache_write: cacheWriteSum,
3253
- // NULL means "OpenCode never reported a cost" (never inferred from
3254
- // tokens) distinct from a genuine 0-cost turn, which would set
3255
- // `sawCost` true with `costSum === 0`.
4015
+ // NULL means OpenCode never reported a cost; it is distinct from a genuine
4016
+ // zero-cost message, which sets `sawCost` with `costSum === 0`.
3256
4017
  usage_cost_usd: sawCost ? costSum : null
3257
4018
  };
3258
4019
  }
4020
+ function messageUsage(messages, userMessageId) {
4021
+ if (!messages || messages.length === 0) return null;
4022
+ const byParentAll = messages.filter(
4023
+ (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
4024
+ );
4025
+ const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
4026
+ const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
4027
+ let correlated;
4028
+ if (byParent.length > 0) {
4029
+ correlated = byParent;
4030
+ } else {
4031
+ const reply = findAssistantReplyAfter(messages, userMessageId);
4032
+ correlated = reply ? [reply] : [];
4033
+ }
4034
+ if (correlated.length === 0) return null;
4035
+ return sumAssistantUsage(correlated);
4036
+ }
3259
4037
  function messageRunState(messages, userMessageId) {
3260
4038
  if (!messages || messages.length === 0) return "unknown";
3261
4039
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3385,9 +4163,73 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3385
4163
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3386
4164
  );
3387
4165
  }
3388
- async function hasAnyConfiguredProvider(port) {
4166
+ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
4167
+ if (!messages || messages.length === 0) return false;
4168
+ const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
4169
+ if (userIndex === -1) return false;
4170
+ let hasLaterUser = false;
4171
+ let hasStartedLaterUser = false;
4172
+ for (let i = userIndex + 1; i < messages.length; i++) {
4173
+ const message = messages[i];
4174
+ if (roleOf(message) !== "user") continue;
4175
+ hasLaterUser = true;
4176
+ const laterUserMessageId = idOf(message);
4177
+ if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
4178
+ return false;
4179
+ }
4180
+ if (messages.some(
4181
+ (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
4182
+ )) {
4183
+ hasStartedLaterUser = true;
4184
+ }
4185
+ }
4186
+ return hasLaterUser && hasStartedLaterUser;
4187
+ }
4188
+ async function hasAnyConfiguredProvider(port, client) {
4189
+ if (client?.version === "v2") {
4190
+ const directory = await getOpenCodeDirectoryV2(client);
4191
+ if (!directory) {
4192
+ console.error(
4193
+ `[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
4194
+ );
4195
+ return null;
4196
+ }
4197
+ const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4198
+ try {
4199
+ const res = await client.request(path);
4200
+ if (!res.ok) {
4201
+ console.error(
4202
+ `[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
4203
+ );
4204
+ return null;
4205
+ }
4206
+ const body = await res.json();
4207
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
4208
+ console.error(
4209
+ `[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
4210
+ );
4211
+ return null;
4212
+ }
4213
+ for (const integration of body.data) {
4214
+ if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
4215
+ console.error(
4216
+ `[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
4217
+ );
4218
+ return null;
4219
+ }
4220
+ }
4221
+ return body.data.some(
4222
+ (integration) => Array.isArray(integration.connections) && integration.connections.length > 0
4223
+ );
4224
+ } catch (error2) {
4225
+ console.error(
4226
+ `[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
4227
+ );
4228
+ return null;
4229
+ }
4230
+ }
3389
4231
  try {
3390
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
4232
+ const res = await requestWithClient(port, client, "/config/providers");
3391
4233
  if (!res.ok) {
3392
4234
  console.error(
3393
4235
  `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3416,6 +4258,99 @@ async function hasAnyConfiguredProvider(port) {
3416
4258
  return null;
3417
4259
  }
3418
4260
  }
4261
+ function sessionErrorReason2(error2) {
4262
+ const record = typeof error2 === "object" && error2 !== null ? error2 : null;
4263
+ const data = record?.data;
4264
+ const dataRecord = typeof data === "object" && data !== null ? data : null;
4265
+ 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";
4266
+ const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
4267
+ return reason || "OpenCode reported a session error with no details";
4268
+ }
4269
+ function parseSessionErrorFrame(data) {
4270
+ let parsed;
4271
+ try {
4272
+ parsed = JSON.parse(data);
4273
+ } catch (error2) {
4274
+ void error2;
4275
+ return null;
4276
+ }
4277
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
4278
+ const parsedRecord = parsed;
4279
+ const payload = parsedRecord.payload;
4280
+ const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
4281
+ if (event.type !== "session.error") return null;
4282
+ const properties = event.properties;
4283
+ if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
4284
+ return null;
4285
+ }
4286
+ const propertiesRecord = properties;
4287
+ const sessionId = propertiesRecord.sessionID;
4288
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
4289
+ return {
4290
+ sessionId,
4291
+ reason: sessionErrorReason2(propertiesRecord.error)
4292
+ };
4293
+ }
4294
+ async function readSessionErrorStream(port, options, client) {
4295
+ if (client?.version === "v2") return readV2SessionErrorStream(client, options);
4296
+ let reader = null;
4297
+ try {
4298
+ const response = await (client?.request("/event", {
4299
+ headers: { accept: "text/event-stream" },
4300
+ signal: options.signal
4301
+ }) ?? fetch(`${opencodeBase(port)}/event`, {
4302
+ headers: { accept: "text/event-stream" },
4303
+ signal: options.signal
4304
+ }));
4305
+ if (!response.ok || !response.body) {
4306
+ return { reason: "unavailable", detail: `HTTP ${response.status}` };
4307
+ }
4308
+ reader = response.body.getReader();
4309
+ const decoder = new TextDecoder();
4310
+ let buffer = "";
4311
+ const processLine = (line) => {
4312
+ const trimmed = line.trimEnd();
4313
+ if (!trimmed.startsWith("data:")) return;
4314
+ const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
4315
+ if (event) options.onSessionError(event);
4316
+ };
4317
+ while (true) {
4318
+ const { done, value } = await reader.read();
4319
+ if (done) return { reason: "ended" };
4320
+ buffer += decoder.decode(value, { stream: true });
4321
+ const lines = buffer.split("\n");
4322
+ buffer = lines.pop() ?? "";
4323
+ for (const line of lines) processLine(line);
4324
+ }
4325
+ } catch (err) {
4326
+ if (options.signal.aborted) return { reason: "aborted" };
4327
+ return {
4328
+ reason: "unavailable",
4329
+ detail: err instanceof Error ? err.message : String(err)
4330
+ };
4331
+ } finally {
4332
+ if (reader) void reader.cancel().catch(() => void 0);
4333
+ }
4334
+ }
4335
+ async function reloadProviderCache(port, client) {
4336
+ if (client?.version === "v2") return;
4337
+ try {
4338
+ const res = await requestWithClient(port, client, "/config", {
4339
+ method: "PATCH",
4340
+ headers: { "Content-Type": "application/json" },
4341
+ body: JSON.stringify({})
4342
+ });
4343
+ if (!res.ok) {
4344
+ console.error(
4345
+ `[reloadProviderCache] PATCH /config returned HTTP ${res.status} (port ${port})`
4346
+ );
4347
+ }
4348
+ } catch (err) {
4349
+ console.error(
4350
+ `[reloadProviderCache] PATCH /config failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
4351
+ );
4352
+ }
4353
+ }
3419
4354
 
3420
4355
  // src/lib/opencode/session-cleanup.ts
3421
4356
  var DURATION_UNIT_MS = {
@@ -3522,8 +4457,8 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
3522
4457
  }
3523
4458
 
3524
4459
  // src/lib/opencode/session-db-size.ts
3525
- import { statSync as statSync3 } from "fs";
3526
- import { join as join4 } from "path";
4460
+ import { statSync as statSync3 } from "node:fs";
4461
+ import { join as join4 } from "node:path";
3527
4462
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
3528
4463
  function statSessionDbBytes(homeDir) {
3529
4464
  const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
@@ -3553,9 +4488,96 @@ function buildSessionStoreSizeWarning(input) {
3553
4488
  return null;
3554
4489
  }
3555
4490
 
4491
+ // src/lib/opencode/log-tail.ts
4492
+ import { statSync as statSync4 } from "node:fs";
4493
+ import { homedir as homedir3 } from "node:os";
4494
+ import { join as join5 } from "node:path";
4495
+ import { open as open2, stat } from "node:fs/promises";
4496
+ var DEFAULT_POLL_INTERVAL_MS = 1e3;
4497
+ function resolveOpenCodeLogPath(homeDir = homedir3(), env = process.env) {
4498
+ const dataDir = env.XDG_DATA_HOME || join5(homeDir, ".local", "share");
4499
+ return join5(dataDir, "opencode", "log", "opencode.log");
4500
+ }
4501
+ function isEnoent(error2) {
4502
+ return error2?.code === "ENOENT";
4503
+ }
4504
+ function reportFailure(operation, logPath, error2) {
4505
+ console.error(
4506
+ `[opencode-log-tail] ${operation} failed for ${logPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
4507
+ );
4508
+ }
4509
+ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
4510
+ const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
4511
+ let offset = 0;
4512
+ let inode = null;
4513
+ let baselineReady = true;
4514
+ try {
4515
+ const initial = statSync4(logPath);
4516
+ offset = initial.size;
4517
+ inode = initial.ino;
4518
+ } catch (error2) {
4519
+ if (!isEnoent(error2)) {
4520
+ reportFailure("initial stat", logPath, error2);
4521
+ baselineReady = false;
4522
+ }
4523
+ }
4524
+ let polling = false;
4525
+ let stopped = false;
4526
+ const poll = async () => {
4527
+ if (polling || stopped) return;
4528
+ polling = true;
4529
+ try {
4530
+ let current;
4531
+ try {
4532
+ current = await stat(logPath);
4533
+ } catch (error2) {
4534
+ if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
4535
+ return;
4536
+ }
4537
+ if (!baselineReady) {
4538
+ offset = current.size;
4539
+ inode = current.ino;
4540
+ baselineReady = true;
4541
+ return;
4542
+ }
4543
+ if (inode !== null && current.ino !== inode || current.size < offset) {
4544
+ offset = 0;
4545
+ }
4546
+ inode = current.ino;
4547
+ if (current.size === offset) return;
4548
+ const length = current.size - offset;
4549
+ const fh = await open2(logPath, "r");
4550
+ try {
4551
+ const buf = Buffer.alloc(length);
4552
+ const { bytesRead } = await fh.read(buf, 0, length, offset);
4553
+ offset += bytesRead;
4554
+ if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
4555
+ } finally {
4556
+ await fh.close();
4557
+ }
4558
+ } catch (error2) {
4559
+ if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
4560
+ } finally {
4561
+ polling = false;
4562
+ }
4563
+ };
4564
+ const interval = setInterval(() => void poll(), pollIntervalMs);
4565
+ void poll();
4566
+ return {
4567
+ stop: () => {
4568
+ stopped = true;
4569
+ clearInterval(interval);
4570
+ }
4571
+ };
4572
+ }
4573
+
3556
4574
  // src/lib/opencode/session-db-reclaim.ts
3557
- import { statSync as statSync4, statfsSync } from "fs";
3558
- import { dirname as dirname4 } from "path";
4575
+ import { statSync as statSync5, statfsSync } from "node:fs";
4576
+ import { dirname as dirname4 } from "node:path";
4577
+ function errorMessage(error2) {
4578
+ if (!(error2 instanceof Error)) return String(error2);
4579
+ return error2.cause instanceof Error ? error2.cause.message : error2.message;
4580
+ }
3559
4581
  function insufficientSpaceReason(dbPath, requiredBytes) {
3560
4582
  try {
3561
4583
  const fsStats = statfsSync(dirname4(dbPath));
@@ -3581,17 +4603,17 @@ async function probeReclaimAvailability(input) {
3581
4603
  const { dbPath, requiredBytes } = input;
3582
4604
  let sqlite;
3583
4605
  try {
3584
- sqlite = await import("sqlite");
4606
+ sqlite = await import("node:sqlite");
3585
4607
  } catch (err) {
3586
- console.warn(
3587
- `[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3588
- );
3589
- return "sqlite-unavailable";
4608
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
4609
+ console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
4610
+ return { reason: "sqlite-unavailable", detail };
3590
4611
  }
3591
4612
  let autoVacuum = null;
3592
4613
  try {
3593
4614
  const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
3594
4615
  try {
4616
+ db.exec("PRAGMA busy_timeout=5000");
3595
4617
  autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3596
4618
  } finally {
3597
4619
  db.close();
@@ -3602,23 +4624,25 @@ async function probeReclaimAvailability(input) {
3602
4624
  );
3603
4625
  }
3604
4626
  if (autoVacuum !== 0) return null;
3605
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
4627
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
3606
4628
  }
3607
4629
  async function reclaimSessionDbSpace(input) {
3608
4630
  const { dbPath, maxPages, allowFullVacuum = true } = input;
3609
4631
  let sqlite;
3610
4632
  try {
3611
- sqlite = await import("sqlite");
4633
+ sqlite = await import("node:sqlite");
3612
4634
  } catch (err) {
4635
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
3613
4636
  console.warn(
3614
- `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
4637
+ `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
3615
4638
  );
3616
- return { ok: false, skipped: "sqlite-unavailable" };
4639
+ return { ok: false, skipped: "sqlite-unavailable", detail };
3617
4640
  }
3618
4641
  const { DatabaseSync } = sqlite;
3619
4642
  let db;
3620
4643
  try {
3621
4644
  db = new DatabaseSync(dbPath);
4645
+ db.exec("PRAGMA busy_timeout=5000");
3622
4646
  const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
3623
4647
  if (autoVacuum === 0) {
3624
4648
  if (!allowFullVacuum) {
@@ -3627,7 +4651,7 @@ async function reclaimSessionDbSpace(input) {
3627
4651
  );
3628
4652
  return { ok: false, skipped: "full-vacuum-blocked" };
3629
4653
  }
3630
- const fileBytesForGuard = statSync4(dbPath).size;
4654
+ const fileBytesForGuard = statSync5(dbPath).size;
3631
4655
  const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
3632
4656
  if (skipReason !== null) {
3633
4657
  console.warn(
@@ -3655,10 +4679,12 @@ async function reclaimSessionDbSpace(input) {
3655
4679
  );
3656
4680
  return { ok: false, skipped: "auto-vacuum-not-applicable" };
3657
4681
  } catch (err) {
3658
- console.error(
3659
- `[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
3660
- );
3661
- return { ok: false, skipped: "reclaim-error" };
4682
+ console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
4683
+ return {
4684
+ ok: false,
4685
+ skipped: "reclaim-error",
4686
+ detail: errorMessage(err)
4687
+ };
3662
4688
  } finally {
3663
4689
  db?.close();
3664
4690
  }
@@ -3687,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
3687
4713
  "content-length"
3688
4714
  ]);
3689
4715
  var StreamForwarder = class {
3690
- constructor(ws, port, callbacks = {}) {
4716
+ constructor(ws, port, callbacks = {}, options = {}) {
3691
4717
  this.ws = ws;
3692
4718
  this.port = port;
3693
4719
  this.callbacks = callbacks;
4720
+ this.options = options;
3694
4721
  }
3695
4722
  inflight = /* @__PURE__ */ new Map();
3696
4723
  /**
@@ -3774,7 +4801,15 @@ var StreamForwarder = class {
3774
4801
  }
3775
4802
  const fwdHeaders = {};
3776
4803
  for (const [k, v] of Object.entries(headers ?? {})) {
3777
- if (!STRIP_REQ.has(k.toLowerCase())) fwdHeaders[k] = v;
4804
+ const lower = k.toLowerCase();
4805
+ if (STRIP_REQ.has(lower)) continue;
4806
+ if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
4807
+ if (lower === "authorization") continue;
4808
+ }
4809
+ fwdHeaders[k] = v;
4810
+ }
4811
+ if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
4812
+ fwdHeaders.Authorization = buildOpenCodeBasicAuthHeader(this.options.openCodePassword);
3778
4813
  }
3779
4814
  this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
3780
4815
  const body = bodyPromise ? await bodyPromise : void 0;
@@ -3887,6 +4922,7 @@ function connectTunnel(options) {
3887
4922
  agentId,
3888
4923
  authHeader,
3889
4924
  port,
4925
+ openCodePassword,
3890
4926
  onConnected,
3891
4927
  onDisconnected,
3892
4928
  onError,
@@ -3904,11 +4940,16 @@ function connectTunnel(options) {
3904
4940
  Authorization: authHeader
3905
4941
  }
3906
4942
  });
3907
- const forwarder = new StreamForwarder(ws, port, {
3908
- onHead: () => onResponse?.(),
3909
- onDrainPing: () => onDrainPing?.(),
3910
- onUsageRearmPing: () => onUsageRearmPing?.()
3911
- });
4943
+ const forwarder = new StreamForwarder(
4944
+ ws,
4945
+ port,
4946
+ {
4947
+ onHead: () => onResponse?.(),
4948
+ onDrainPing: () => onDrainPing?.(),
4949
+ onUsageRearmPing: () => onUsageRearmPing?.()
4950
+ },
4951
+ { openCodePassword }
4952
+ );
3912
4953
  const connectionTimeout = setTimeout(() => {
3913
4954
  ws.close();
3914
4955
  reject(new Error("Connection timeout"));
@@ -3950,8 +4991,8 @@ function connectTunnel(options) {
3950
4991
  try {
3951
4992
  message = JSON.parse(data.toString());
3952
4993
  } catch (error2) {
3953
- const errorMessage2 = error2 instanceof Error ? error2.message : "Unknown error";
3954
- onError?.(`Failed to handle message: ${errorMessage2}`);
4994
+ const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
4995
+ onError?.(`Failed to handle message: ${errorMessage3}`);
3955
4996
  return;
3956
4997
  }
3957
4998
  if (isStreamFrame(message)) {
@@ -4051,6 +5092,7 @@ var RunnerConnection = class {
4051
5092
  agentId: this.resolvedAgentId,
4052
5093
  authHeader: this.opts.getAuthHeader(),
4053
5094
  port: this.opts.port,
5095
+ openCodePassword: this.opts.openCodePassword,
4054
5096
  onConnected: (agentId) => {
4055
5097
  this.reconnectAttempt = 0;
4056
5098
  this.reconnecting = false;
@@ -4095,7 +5137,7 @@ var RunnerConnection = class {
4095
5137
  };
4096
5138
 
4097
5139
  // src/lib/tunnel/ready-marker.ts
4098
- import { writeFileSync as writeFileSync3 } from "fs";
5140
+ import { writeFileSync as writeFileSync3 } from "node:fs";
4099
5141
  function writeTunnelReadyMarker(path, agentId) {
4100
5142
  try {
4101
5143
  writeFileSync3(path, `${agentId}
@@ -4107,7 +5149,7 @@ function writeTunnelReadyMarker(path, agentId) {
4107
5149
  }
4108
5150
 
4109
5151
  // src/lib/replication.ts
4110
- import { spawn as spawn4 } from "child_process";
5152
+ import { spawn as spawn4 } from "node:child_process";
4111
5153
  function startSessionDbReplication(configPath) {
4112
5154
  return spawn4("litestream", ["replicate", "-config", configPath], {
4113
5155
  stdio: "inherit"
@@ -4123,7 +5165,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
4123
5165
  }
4124
5166
 
4125
5167
  // src/lib/process-liveness.ts
4126
- import { readFileSync as readFileSync4 } from "fs";
5168
+ import { readFileSync as readFileSync4 } from "node:fs";
4127
5169
  function isProcessAlive(pid) {
4128
5170
  try {
4129
5171
  process.kill(pid, 0);
@@ -4149,9 +5191,9 @@ function isProcessAlive(pid) {
4149
5191
  }
4150
5192
 
4151
5193
  // src/lib/openai-usage.ts
4152
- import { readFileSync as readFileSync5 } from "fs";
4153
- import { homedir as homedir3 } from "os";
4154
- import { join as join5 } from "path";
5194
+ import { readFileSync as readFileSync5 } from "node:fs";
5195
+ import { homedir as homedir4 } from "node:os";
5196
+ import { join as join6 } from "node:path";
4155
5197
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4156
5198
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4157
5199
  var OpenAiUsageError = class extends Error {
@@ -4165,7 +5207,7 @@ function isLocalCredentialProblem2(err) {
4165
5207
  }
4166
5208
  function readOpenCodeChatGptCredentials() {
4167
5209
  try {
4168
- const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
5210
+ const raw = readFileSync5(join6(homedir4(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4169
5211
  let parsed;
4170
5212
  try {
4171
5213
  parsed = JSON.parse(raw);
@@ -4202,7 +5244,7 @@ function parseChatGptIdentity(accessToken) {
4202
5244
  const auth = payload["https://api.openai.com/auth"];
4203
5245
  const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4204
5246
  const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4205
- return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
5247
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
4206
5248
  }
4207
5249
  function toWindow2(headers, name) {
4208
5250
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
@@ -4231,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
4231
5273
  function normalizeProbeModel(model) {
4232
5274
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4233
5275
  }
4234
- async function resolveProbeModels(port) {
5276
+ function isRecord2(value) {
5277
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5278
+ }
5279
+ function unsupportedProbeModels(reason, port) {
5280
+ console.error(`[resolveProbeModels] ${reason} (port ${port})`);
5281
+ return { status: "unsupported", reason };
5282
+ }
5283
+ async function resolveV1ProbeModels(client, port) {
4235
5284
  try {
4236
- const res = await withRequestTimeout(
4237
- fetch,
4238
- REQUEST_TIMEOUT_MS
4239
- )(`${opencodeBase(port)}/config/providers`);
4240
- if (!res.ok) {
4241
- console.error(
4242
- `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
4243
- );
4244
- return [];
5285
+ const response = await client.request("/config/providers");
5286
+ const body = await response.json();
5287
+ if (!isRecord2(body) || !Array.isArray(body.providers)) {
5288
+ return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
4245
5289
  }
4246
- const body = await res.json();
4247
- const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
4248
- if (!provider || !provider.models || typeof provider.models !== "object") return [];
5290
+ const provider = body.providers.find(
5291
+ (candidate) => isRecord2(candidate) && candidate.id === "openai"
5292
+ );
5293
+ if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
5294
+ const defaults2 = isRecord2(body.default) ? body.default : void 0;
4249
5295
  const candidates = [
4250
- ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
5296
+ ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4251
5297
  ...Object.keys(provider.models)
4252
5298
  ].map(normalizeProbeModel);
4253
- return [...new Set(candidates)].slice(0, 4);
5299
+ return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
4254
5300
  } catch (err) {
4255
- console.error(
4256
- `[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
5301
+ return unsupportedProbeModels(
5302
+ `V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
5303
+ port
5304
+ );
5305
+ }
5306
+ }
5307
+ async function resolveV2ProbeModels(client, port) {
5308
+ const directory = await getOpenCodeDirectoryV2(client);
5309
+ if (!directory) {
5310
+ return unsupportedProbeModels("V2 working directory could not be verified", port);
5311
+ }
5312
+ const path = `/api/provider?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
5313
+ try {
5314
+ const response = await client.request(path);
5315
+ const body = await response.json();
5316
+ if (!isRecord2(body) || !Array.isArray(body.data)) {
5317
+ return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
5318
+ }
5319
+ const provider = body.data.find(
5320
+ (candidate) => isRecord2(candidate) && candidate.id === "openai"
5321
+ );
5322
+ if (!provider) return { status: "supported", models: [] };
5323
+ if (!isRecord2(provider.models)) {
5324
+ return unsupportedProbeModels(
5325
+ "V2 provider response has no safe OpenAI model catalogue",
5326
+ port
5327
+ );
5328
+ }
5329
+ return {
5330
+ status: "supported",
5331
+ models: [...new Set(Object.keys(provider.models).map(normalizeProbeModel))].slice(0, 4)
5332
+ };
5333
+ } catch (err) {
5334
+ return unsupportedProbeModels(
5335
+ `V2 GET ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
5336
+ port
4257
5337
  );
4258
- return [];
4259
5338
  }
4260
5339
  }
5340
+ async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
5341
+ return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
5342
+ }
4261
5343
  function hasPrimaryHeaders(headers) {
4262
5344
  return [
4263
5345
  "x-codex-primary-used-percent",
@@ -4265,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
4265
5347
  "x-codex-primary-reset-at"
4266
5348
  ].some((name) => headers.has(name));
4267
5349
  }
4268
- async function getOpenAiUsage(port) {
5350
+ async function getOpenAiUsage(port, client) {
4269
5351
  const credentials2 = readOpenCodeChatGptCredentials();
4270
5352
  if (!credentials2) {
4271
5353
  throw new OpenAiUsageError(
@@ -4280,12 +5362,16 @@ async function getOpenAiUsage(port) {
4280
5362
  );
4281
5363
  }
4282
5364
  const subscription = parseChatGptIdentity(credentials2.accessToken);
4283
- const models = await resolveProbeModels(port);
4284
- if (models.length === 0) {
4285
- throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
5365
+ const lookup = await resolveProbeModels(port, client);
5366
+ if (lookup.status === "unsupported" || lookup.models.length === 0) {
5367
+ const detail = lookup.status === "unsupported" ? ` ${lookup.reason}.` : "";
5368
+ throw new OpenAiUsageError(
5369
+ `No supported OpenAI probe model is available.${detail}`,
5370
+ "no_probe_model"
5371
+ );
4286
5372
  }
4287
5373
  let lastStatus;
4288
- for (const model of models) {
5374
+ for (const model of lookup.models) {
4289
5375
  let res;
4290
5376
  try {
4291
5377
  res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
@@ -4384,13 +5470,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
4384
5470
  envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
4385
5471
  });
4386
5472
  }
4387
- function nextReportDelayMs(random = Math.random) {
4388
- return usageReportDelayMs(random);
4389
- }
4390
- var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
4391
- function claudeUsageFailureLogLevel(consecutiveFailures) {
4392
- return usageReportFailureLogLevel(consecutiveFailures);
4393
- }
4394
5473
 
4395
5474
  // src/lib/openai-usage-reporting.ts
4396
5475
  function resolveOpenAiUsageReportingMode(flagValue, env) {
@@ -4427,8 +5506,8 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
4427
5506
  }
4428
5507
 
4429
5508
  // src/lib/resource-usage.ts
4430
- import { cpus, totalmem, freemem } from "os";
4431
- import { statfsSync as statfsSync2 } from "fs";
5509
+ import { cpus, totalmem, freemem } from "node:os";
5510
+ import { statfsSync as statfsSync2 } from "node:fs";
4432
5511
 
4433
5512
  // src/lib/ecs-task-metadata.ts
4434
5513
  var ECS_METADATA_TIMEOUT_MS = 2e3;
@@ -4595,15 +5674,15 @@ function createResourceUsageCollector(homeDir) {
4595
5674
  }
4596
5675
 
4597
5676
  // src/lib/channels/driver.ts
4598
- import { homedir as homedir4 } from "os";
5677
+ import { homedir as homedir5 } from "node:os";
4599
5678
 
4600
5679
  // src/lib/runner-file-sync.ts
4601
- import { join as join7 } from "path";
5680
+ import { join as join8 } from "node:path";
4602
5681
 
4603
5682
  // src/lib/file-push.ts
4604
- import { randomUUID } from "crypto";
4605
- import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
4606
- import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
5683
+ import { randomUUID } from "node:crypto";
5684
+ import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
5685
+ import { basename, dirname as dirname5, isAbsolute, join as join7, relative, resolve as resolve2, sep } from "node:path";
4607
5686
  var FILE_MODE = 384;
4608
5687
  var DIRECTORY_MODE = 448;
4609
5688
  async function writePushedFile(request) {
@@ -4636,7 +5715,7 @@ async function writePushedFile(request) {
4636
5715
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
4637
5716
  dirname5(candidate)
4638
5717
  );
4639
- const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
5718
+ const realTarget = join7(existingAncestor, ...missingSegments, basename(candidate));
4640
5719
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
4641
5720
  if (allowedDirectory === null) {
4642
5721
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -4672,7 +5751,7 @@ function expandAndValidate(requestedPath, homeDir) {
4672
5751
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
4673
5752
  return null;
4674
5753
  }
4675
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
5754
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4676
5755
  if (expanded.split(/[/\\]/).includes("..")) {
4677
5756
  return null;
4678
5757
  }
@@ -4745,16 +5824,16 @@ function contains(realDirectory, realTarget) {
4745
5824
  async function createMissingDirectories(existingAncestor, missingSegments) {
4746
5825
  let current = existingAncestor;
4747
5826
  for (const segment of missingSegments) {
4748
- current = join6(current, segment);
5827
+ current = join7(current, segment);
4749
5828
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
4750
5829
  await chmod(current, DIRECTORY_MODE);
4751
5830
  }
4752
5831
  }
4753
5832
  async function writeAtomically(realTarget, content) {
4754
- const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
5833
+ const temporaryPath = join7(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
4755
5834
  let handle;
4756
5835
  try {
4757
- handle = await open2(temporaryPath, "wx", FILE_MODE);
5836
+ handle = await open3(temporaryPath, "wx", FILE_MODE);
4758
5837
  await handle.writeFile(content);
4759
5838
  await handle.chmod(FILE_MODE);
4760
5839
  await handle.close();
@@ -4881,12 +5960,12 @@ var NOT_APPLIED = {
4881
5960
  opencodeAuthApplied: false
4882
5961
  };
4883
5962
  function isClaudeCredentialPath(requestedPath, homeDir) {
4884
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4885
- return expanded === join7(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
5963
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5964
+ return expanded === join8(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4886
5965
  }
4887
5966
  function isOpenCodeAuthPath(requestedPath, homeDir) {
4888
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4889
- return expanded === join7(homeDir, ...OPENCODE_AUTH_SEGMENTS);
5967
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
5968
+ return expanded === join8(homeDir, ...OPENCODE_AUTH_SEGMENTS);
4890
5969
  }
4891
5970
  async function applyOne(options, file) {
4892
5971
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -5046,6 +6125,10 @@ var DEFAULT_RETRY_POLICY = {
5046
6125
  baseDelayMs: 500,
5047
6126
  maxDelayMs: 3e4
5048
6127
  };
6128
+ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
6129
+ var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
6130
+ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
6131
+ var MAX_BUFFERED_SESSION_ERRORS = 256;
5049
6132
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
5050
6133
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
5051
6134
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5107,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
5107
6190
  maxActiveSessions;
5108
6191
  watcherStallMs;
5109
6192
  wedgeWarningIntervalMs;
6193
+ openCodeClient;
5110
6194
  /** Cache of conversationId → opencode sessionId. */
5111
6195
  sessions = /* @__PURE__ */ new Map();
5112
6196
  /**
@@ -5186,6 +6270,17 @@ var ChannelDriver = class _ChannelDriver {
5186
6270
  * message; it is removed once its in-flight set empties.
5187
6271
  */
5188
6272
  watchers = /* @__PURE__ */ new Map();
6273
+ sessionErrorStream = null;
6274
+ /**
6275
+ * Session-error failures currently being reported; entries are empty at rest
6276
+ * because each handoff deletes its id in `finally`.
6277
+ */
6278
+ sessionErrorHandled = /* @__PURE__ */ new Set();
6279
+ /**
6280
+ * Session errors that arrived before their dispatch was registered. Bounded FIFO
6281
+ * with a short TTL so an unmatched session cannot retain an event indefinitely.
6282
+ */
6283
+ bufferedSessionErrors = /* @__PURE__ */ new Map();
5189
6284
  /**
5190
6285
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5191
6286
  * dispatched and are still in-flight. A message in this set is never
@@ -5214,10 +6309,15 @@ var ChannelDriver = class _ChannelDriver {
5214
6309
  * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
5215
6310
  * in opencode must still be delivered via `markDone` on the next drain — so
5216
6311
  * `readoptOne` computes `state` FIRST and this set is checked only on the
5217
- * non-done path. It is cleared once the row leaves the processing list (cron
5218
- * reset → it drains normally as `pending`), so it can never leak.
6312
+ * non-done path. It is cleared once the row leaves both processing and pending
6313
+ * lists, so it can never leak. A V2 prompt
6314
+ * acknowledgement with no usable id also uses this fence: OpenCode accepted the
6315
+ * turn, but there is no safe id to watch, so a failed `markFailed` report must not
6316
+ * allow the pending row to post the prompt again.
5219
6317
  */
5220
6318
  dontRedispatch = /* @__PURE__ */ new Set();
6319
+ /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6320
+ pendingMessageIds = /* @__PURE__ */ new Set();
5221
6321
  /**
5222
6322
  * "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
5223
6323
  * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
@@ -5340,7 +6440,8 @@ var ChannelDriver = class _ChannelDriver {
5340
6440
  */
5341
6441
  attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
5342
6442
  /**
5343
- * Cache of the opencode root directory (from `GET /path`). Resolved lazily on
6443
+ * Cache of the opencode root directory from the selected client's location lookup.
6444
+ * Resolved lazily on
5344
6445
  * first session creation so drain-created sessions are rooted at the project
5345
6446
  * directory and thus visible in `opencode web`'s session list. `undefined` =
5346
6447
  * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
@@ -5369,6 +6470,13 @@ var ChannelDriver = class _ChannelDriver {
5369
6470
  * no watcher) can resolve the title.
5370
6471
  */
5371
6472
  sessionTitles = /* @__PURE__ */ new Map();
6473
+ /** One best-effort terminal subagent collection per Evident message id. */
6474
+ subagentInvocationCollections = /* @__PURE__ */ new Map();
6475
+ /**
6476
+ * Early snapshots are only liveness hints; they must not become the terminal
6477
+ * collection when the task parts or child transcript have advanced.
6478
+ */
6479
+ subagentInvocationPrefetches = /* @__PURE__ */ new Map();
5372
6480
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5373
6481
  draining = false;
5374
6482
  /**
@@ -5427,20 +6535,55 @@ var ChannelDriver = class _ChannelDriver {
5427
6535
  config.fetchImpl ?? fetch,
5428
6536
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
5429
6537
  );
6538
+ this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
6539
+ port: config.port,
6540
+ version: "v1",
6541
+ fetchImpl: config.fetchImpl
6542
+ });
5430
6543
  this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5431
6544
  this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
5432
6545
  this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
5433
6546
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
5434
6547
  this.now = config.now ?? (() => Date.now());
5435
6548
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
5436
- this.homeDir = config.homeDir ?? homedir4();
6549
+ this.homeDir = config.homeDir ?? homedir5();
5437
6550
  this.maxActiveSessions = config.maxActiveSessions;
5438
6551
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5439
6552
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
5440
6553
  }
5441
- /** The IPv4-loopback base URL for the local `opencode serve`. */
5442
- get opencodeBase() {
5443
- return `http://127.0.0.1:${this.port}`;
6554
+ get isV2() {
6555
+ return this.openCodeClient.version === "v2";
6556
+ }
6557
+ async getSessionMessages(sessionId) {
6558
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6559
+ }
6560
+ async getSubagentSessionMessages(sessionId) {
6561
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
6562
+ }
6563
+ async getTelemetrySubagentSessionMessages(sessionId) {
6564
+ if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
6565
+ return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6566
+ }
6567
+ async listSessions() {
6568
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
6569
+ }
6570
+ async sessionExists(sessionId) {
6571
+ return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
6572
+ }
6573
+ async isSessionOngoing(sessionId) {
6574
+ return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
6575
+ }
6576
+ async getOpenCodeDirectory() {
6577
+ return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
6578
+ }
6579
+ async createOpenCodeSession(directory) {
6580
+ return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
6581
+ }
6582
+ async hasAnyConfiguredProvider() {
6583
+ return hasAnyConfiguredProvider(this.port, this.openCodeClient);
6584
+ }
6585
+ async readOpenCodeSessionErrorStream(options) {
6586
+ return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
5444
6587
  }
5445
6588
  /**
5446
6589
  * Drain all pending channel conversations once: poll → dispatch → register.
@@ -5518,6 +6661,7 @@ var ChannelDriver = class _ChannelDriver {
5518
6661
  async runDrain() {
5519
6662
  let dispatched = 0;
5520
6663
  try {
6664
+ this.pendingMessageIds.clear();
5521
6665
  const conversations = await this.getPendingConversations();
5522
6666
  if (this.recycleRequestedFlag) {
5523
6667
  this.stop();
@@ -5583,6 +6727,21 @@ var ChannelDriver = class _ChannelDriver {
5583
6727
  }
5584
6728
  return ids;
5585
6729
  }
6730
+ /**
6731
+ * OpenCode user-message ids tracked for other Evident messages in a session.
6732
+ * Excluding this message makes an unattributed later row fail safe; a missing
6733
+ * watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
6734
+ */
6735
+ siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
6736
+ const ids = /* @__PURE__ */ new Set();
6737
+ if (!watcher) return ids;
6738
+ for (const inFlight of watcher.inFlight.values()) {
6739
+ if (inFlight.evidentMessageId !== ownEvidentMessageId) {
6740
+ ids.add(inFlight.opencodeMessageId);
6741
+ }
6742
+ }
6743
+ return ids;
6744
+ }
5586
6745
  /**
5587
6746
  * File-pull work, for `run.ts`'s idle accounting (#559).
5588
6747
  *
@@ -5649,6 +6808,8 @@ var ChannelDriver = class _ChannelDriver {
5649
6808
  */
5650
6809
  stop() {
5651
6810
  this.stopped = true;
6811
+ this.sessionErrorStream?.abort.abort();
6812
+ this.sessionErrorStream = null;
5652
6813
  }
5653
6814
  /**
5654
6815
  * The server clears this request when a new MicroVM identity is recorded, so a
@@ -5723,7 +6884,9 @@ var ChannelDriver = class _ChannelDriver {
5723
6884
  */
5724
6885
  async processConversation(conv) {
5725
6886
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6887
+ this.ensureSessionErrorStream();
5726
6888
  const messages = await this.getPendingMessages(conv.id);
6889
+ for (const message of messages) this.pendingMessageIds.add(message.id);
5727
6890
  let dispatched = 0;
5728
6891
  let skippedAlreadyDispatched = 0;
5729
6892
  if (refusedSessionId && messages.length > 0) {
@@ -5737,6 +6900,15 @@ var ChannelDriver = class _ChannelDriver {
5737
6900
  skippedAlreadyDispatched += 1;
5738
6901
  continue;
5739
6902
  }
6903
+ if (this.dontRedispatch.has(message.id)) {
6904
+ this.log({
6905
+ level: "warn",
6906
+ message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
6907
+ conversation_id: conv.id,
6908
+ message_id: message.id
6909
+ });
6910
+ break;
6911
+ }
5740
6912
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
5741
6913
  if (effectiveOpencodeMessageId) {
5742
6914
  const outcome = await this.resolveRedrive(
@@ -5765,15 +6937,55 @@ var ChannelDriver = class _ChannelDriver {
5765
6937
  conversation_id: conv.id,
5766
6938
  message_id: message.id
5767
6939
  });
5768
- const sendAttachments = this.buildSendAttachments(conv, message);
5769
- opencodeMessageId = await this.dispatchLocked(
6940
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
6941
+ if (this.isV2 && message.attachments && message.attachments.length > 0) {
6942
+ this.signalAttachmentsSkipped(
6943
+ conv.id,
6944
+ message.id,
6945
+ message.attachments.map((attachment, index) => ({
6946
+ index,
6947
+ mime: attachment.mime,
6948
+ ...attachment.filename ? { filename: attachment.filename } : {},
6949
+ status: "skipped"
6950
+ })),
6951
+ false
6952
+ );
6953
+ }
6954
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
5770
6955
  sessionId,
5771
- () => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
6956
+ () => sendPromptAsync(
6957
+ this.port,
6958
+ sessionId,
6959
+ message.content,
6960
+ options,
6961
+ sendAttachments,
6962
+ this.openCodeClient
6963
+ )
5772
6964
  );
5773
6965
  } catch (err) {
5774
6966
  if (err instanceof ChannelAuthError) throw err;
6967
+ if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
6968
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
6969
+ this.dontRedispatch.add(message.id);
6970
+ this.log({
6971
+ level: "error",
6972
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
6973
+ conversation_id: conv.id,
6974
+ message_id: message.id
6975
+ });
6976
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
6977
+ this.log({
6978
+ level: "warn",
6979
+ message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
6980
+ conversation_id: conv.id,
6981
+ message_id: message.id
6982
+ });
6983
+ void this.postSignal(conv.id, message.id, "ack_untrackable");
6984
+ });
6985
+ break;
6986
+ }
5775
6987
  this.dispatched.delete(message.id);
5776
- const exists = await sessionExists(this.port, sessionId);
6988
+ const exists = await this.sessionExists(sessionId);
5777
6989
  if (exists === false) {
5778
6990
  this.sessions.delete(conv.id);
5779
6991
  this.log({
@@ -5795,7 +7007,7 @@ var ChannelDriver = class _ChannelDriver {
5795
7007
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
5796
7008
  break;
5797
7009
  }
5798
- const errorMessage2 = err instanceof Error ? err.message : String(err);
7010
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
5799
7011
  this.sessions.delete(conv.id);
5800
7012
  this.supersede(conv.id, sessionId);
5801
7013
  this.log({
@@ -5804,7 +7016,7 @@ var ChannelDriver = class _ChannelDriver {
5804
7016
  conversation_id: conv.id,
5805
7017
  message_id: message.id
5806
7018
  });
5807
- await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
7019
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5808
7020
  this.log({
5809
7021
  level: "warn",
5810
7022
  message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -5815,13 +7027,16 @@ var ChannelDriver = class _ChannelDriver {
5815
7027
  });
5816
7028
  this.log({
5817
7029
  level: "error",
5818
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage2}`,
7030
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
5819
7031
  conversation_id: conv.id,
5820
7032
  message_id: message.id
5821
7033
  });
5822
7034
  break;
5823
7035
  }
5824
7036
  if (opencodeMessageId === null) {
7037
+ if (this.isV2) {
7038
+ throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7039
+ }
5825
7040
  const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
5826
7041
  if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
5827
7042
  this.log({
@@ -5836,14 +7051,14 @@ var ChannelDriver = class _ChannelDriver {
5836
7051
  this.unconfirmedDispatchFailures.delete(message.id);
5837
7052
  this.sessions.delete(conv.id);
5838
7053
  this.supersede(conv.id, sessionId);
5839
- const errorMessage2 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
7054
+ const errorMessage3 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
5840
7055
  this.log({
5841
7056
  level: "error",
5842
- message: errorMessage2,
7057
+ message: errorMessage3,
5843
7058
  conversation_id: conv.id,
5844
7059
  message_id: message.id
5845
7060
  });
5846
- await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
7061
+ await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
5847
7062
  this.log({
5848
7063
  level: "warn",
5849
7064
  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)}`,
@@ -5969,29 +7184,38 @@ var ChannelDriver = class _ChannelDriver {
5969
7184
  */
5970
7185
  async pollSessionMessagesForRedrive(conv, message, sessionId) {
5971
7186
  try {
5972
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
5973
- if (!res.ok) {
5974
- const rawBody = await res.text();
5975
- const normalized = normalizeRedrivePollFailureBody(rawBody);
5976
- this.log({
5977
- level: "warn",
5978
- message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned HTTP ${res.status}${normalized ? `: ${normalized}` : ""} \u2014 treating as unreadable this tick`,
5979
- conversation_id: conv.id,
5980
- message_id: message.id
5981
- });
5982
- return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ""}` };
7187
+ if (this.isV2) {
7188
+ const messages = await this.getSessionMessages(sessionId);
7189
+ if (messages === null) {
7190
+ this.log({
7191
+ level: "warn",
7192
+ message: `Re-drive: failed to poll V2 session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} \u2014 treating as unreadable this tick`,
7193
+ conversation_id: conv.id,
7194
+ message_id: message.id
7195
+ });
7196
+ return { ok: false, signature: null };
7197
+ }
7198
+ return { ok: true, messages };
5983
7199
  }
5984
- const body = await res.json();
5985
- if (!Array.isArray(body)) {
7200
+ const polledV1 = await pollSessionMessagesForRedrive(
7201
+ this.port,
7202
+ sessionId,
7203
+ this.openCodeClient
7204
+ );
7205
+ if (!polledV1.ok) {
7206
+ const normalized = normalizeRedrivePollFailureBody(polledV1.body);
5986
7207
  this.log({
5987
7208
  level: "warn",
5988
- message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned a non-array message body \u2014 treating as unreadable this tick`,
7209
+ message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned ${polledV1.malformed ? "a non-array message body" : `HTTP ${polledV1.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 treating as unreadable this tick`,
5989
7210
  conversation_id: conv.id,
5990
7211
  message_id: message.id
5991
7212
  });
5992
- return { ok: false, signature: "non-array message body" };
7213
+ return {
7214
+ ok: false,
7215
+ signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
7216
+ };
5993
7217
  }
5994
- return { ok: true, messages: body };
7218
+ return { ok: true, messages: polledV1.messages };
5995
7219
  } catch (err) {
5996
7220
  this.log({
5997
7221
  level: "warn",
@@ -6050,11 +7274,11 @@ var ChannelDriver = class _ChannelDriver {
6050
7274
  }
6051
7275
  const state = messageRunState(messages, ocId ?? "");
6052
7276
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
6053
- const ongoing = await isSessionOngoing(this.port, sessionId);
7277
+ const ongoing = await this.isSessionOngoing(sessionId);
6054
7278
  if (ongoing === false) {
6055
7279
  this.log({
6056
7280
  level: "info",
6057
- message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
7281
+ message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
6058
7282
  conversation_id: conv.id,
6059
7283
  message_id: message.id
6060
7284
  });
@@ -6067,8 +7291,25 @@ var ChannelDriver = class _ChannelDriver {
6067
7291
  return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
6068
7292
  }
6069
7293
  if (state === "running" || state === "queued") {
6070
- const ongoing = await isSessionOngoing(this.port, sessionId);
7294
+ const ongoing = await this.isSessionOngoing(sessionId);
6071
7295
  if (ongoing === true) {
7296
+ if (state === "queued") {
7297
+ const siblingOcIds = this.siblingOpencodeMessageIds(
7298
+ this.watchers.get(sessionId),
7299
+ message.id
7300
+ );
7301
+ if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
7302
+ this.log({
7303
+ level: "warn",
7304
+ 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`,
7305
+ conversation_id: conv.id,
7306
+ message_id: message.id
7307
+ });
7308
+ this.clearRedriveUnresolved(message.id);
7309
+ void this.postSignal(conv.id, message.id, "redrive_redispatched");
7310
+ return "dispatch";
7311
+ }
7312
+ }
6072
7313
  return this.reattachRedrive(conv, sessionId, message, ocId);
6073
7314
  }
6074
7315
  if (ongoing === false) {
@@ -6158,16 +7399,37 @@ var ChannelDriver = class _ChannelDriver {
6158
7399
  if (state === "done") {
6159
7400
  const title = await this.resolveSessionTitle(sessionId, conv.id);
6160
7401
  const usage = messageUsage(messages, ocId ?? "");
7402
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
7403
+ const subagentInvocations = await this.resolveSubagentInvocations(
7404
+ messages,
7405
+ ocId ?? "",
7406
+ message.id
7407
+ );
6161
7408
  this.log({
6162
7409
  level: "info",
6163
7410
  message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending \u2014 marking done instead of re-dispatching`,
6164
7411
  conversation_id: conv.id,
6165
7412
  message_id: message.id
6166
7413
  });
6167
- await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);
7414
+ await this.markDone(
7415
+ conv.id,
7416
+ message.id,
7417
+ sessionId,
7418
+ ocId,
7419
+ title,
7420
+ usage,
7421
+ usageAgentName,
7422
+ subagentInvocations
7423
+ );
6168
7424
  } else {
6169
7425
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
6170
7426
  const usage = messageUsage(messages, ocId ?? "");
7427
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
7428
+ const subagentInvocations = await this.resolveSubagentInvocations(
7429
+ messages,
7430
+ ocId ?? "",
7431
+ message.id
7432
+ );
6171
7433
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
6172
7434
  this.log({
6173
7435
  level: "error",
@@ -6175,7 +7437,16 @@ var ChannelDriver = class _ChannelDriver {
6175
7437
  conversation_id: conv.id,
6176
7438
  message_id: message.id
6177
7439
  });
6178
- await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
7440
+ await this.markFailed(
7441
+ conv.id,
7442
+ message.id,
7443
+ sessionId,
7444
+ error2,
7445
+ usage,
7446
+ failure,
7447
+ usageAgentName,
7448
+ subagentInvocations
7449
+ );
6179
7450
  }
6180
7451
  if (ocId !== null) {
6181
7452
  await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
@@ -6480,7 +7751,7 @@ var ChannelDriver = class _ChannelDriver {
6480
7751
  };
6481
7752
  }
6482
7753
  if (bound) {
6483
- const exists = await sessionExists(this.port, bound);
7754
+ const exists = await this.sessionExists(bound);
6484
7755
  if (exists === false) {
6485
7756
  this.log({
6486
7757
  level: "debug",
@@ -6513,7 +7784,7 @@ var ChannelDriver = class _ChannelDriver {
6513
7784
  */
6514
7785
  async createAndBindSession(conversationId) {
6515
7786
  const directory = await this.resolveOpenCodeDirectory();
6516
- const sessionId = await createOpenCodeSession(this.port, directory);
7787
+ const sessionId = await this.createOpenCodeSession(directory);
6517
7788
  this.sessions.set(conversationId, sessionId);
6518
7789
  await this.persistSession(conversationId, sessionId).catch((err) => {
6519
7790
  this.log({
@@ -6525,17 +7796,18 @@ var ChannelDriver = class _ChannelDriver {
6525
7796
  return sessionId;
6526
7797
  }
6527
7798
  /**
6528
- * Lazily resolve (and cache) opencode's root directory via `GET /path`.
7799
+ * Lazily resolve (and cache) opencode's root directory via the selected client's
7800
+ * location lookup.
6529
7801
  * Resolved once per driver: `undefined` until first lookup, then the directory
6530
- * string or `null` if unavailable (we don't keep retrying a missing `/path`).
7802
+ * string or `null` if unavailable (we don't keep retrying a failed lookup).
6531
7803
  */
6532
7804
  async resolveOpenCodeDirectory() {
6533
7805
  if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
6534
- this.opencodeDirectory = await getOpenCodeDirectory(this.port);
7806
+ this.opencodeDirectory = await this.getOpenCodeDirectory();
6535
7807
  if (!this.opencodeDirectory) {
6536
7808
  this.log({
6537
7809
  level: "warn",
6538
- message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
7810
+ message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
6539
7811
  });
6540
7812
  }
6541
7813
  return this.opencodeDirectory;
@@ -6719,6 +7991,24 @@ var ChannelDriver = class _ChannelDriver {
6719
7991
  ambiguousPinnedSinceMs: 0,
6720
7992
  ambiguousResolved: false
6721
7993
  });
7994
+ const buffered = this.bufferedSessionErrors.get(sessionId);
7995
+ if (!buffered) return;
7996
+ this.bufferedSessionErrors.delete(sessionId);
7997
+ if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
7998
+ this.handleSessionError(buffered.event);
7999
+ }
8000
+ }
8001
+ bufferSessionError(event) {
8002
+ this.bufferedSessionErrors.delete(event.sessionId);
8003
+ this.bufferedSessionErrors.set(event.sessionId, {
8004
+ event,
8005
+ receivedAt: this.now()
8006
+ });
8007
+ while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
8008
+ const oldest = this.bufferedSessionErrors.keys().next().value;
8009
+ if (typeof oldest !== "string") break;
8010
+ this.bufferedSessionErrors.delete(oldest);
8011
+ }
6722
8012
  }
6723
8013
  /**
6724
8014
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
@@ -6923,6 +8213,7 @@ var ChannelDriver = class _ChannelDriver {
6923
8213
  ensureWatcherRunning(sessionId) {
6924
8214
  const watcher = this.watchers.get(sessionId);
6925
8215
  if (!watcher) return;
8216
+ this.ensureSessionErrorStream();
6926
8217
  if (watcher.loop) return;
6927
8218
  if (watcher.inFlight.size === 0) {
6928
8219
  this.watchers.delete(sessionId);
@@ -6938,6 +8229,154 @@ var ChannelDriver = class _ChannelDriver {
6938
8229
  });
6939
8230
  watcher.loop = loop;
6940
8231
  }
8232
+ ensureSessionErrorStream() {
8233
+ if (this.sessionErrorStream || this.stopped) return;
8234
+ const abort = new AbortController();
8235
+ const loop = this.runSessionErrorStream(abort.signal);
8236
+ this.sessionErrorStream = { abort, loop };
8237
+ }
8238
+ async runSessionErrorStream(signal) {
8239
+ let attempt = 0;
8240
+ let warned = false;
8241
+ while (!this.stopped && !signal.aborted) {
8242
+ const openedAt = this.now();
8243
+ try {
8244
+ const outcome = await this.readOpenCodeSessionErrorStream({
8245
+ signal,
8246
+ onSessionError: (event) => this.handleSessionError(event)
8247
+ });
8248
+ if (outcome.reason === "aborted" || signal.aborted) return;
8249
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
8250
+ if (outcome.reason === "unavailable" || outcome.reason === "ended") {
8251
+ if (!healthy) {
8252
+ const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
8253
+ this.log({
8254
+ level: warned ? "debug" : "warn",
8255
+ message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
8256
+ });
8257
+ warned = true;
8258
+ }
8259
+ }
8260
+ if (healthy) {
8261
+ if (warned) {
8262
+ this.log({
8263
+ level: "info",
8264
+ message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
8265
+ });
8266
+ warned = false;
8267
+ }
8268
+ attempt = 0;
8269
+ } else {
8270
+ attempt += 1;
8271
+ }
8272
+ if (this.stopped || signal.aborted) return;
8273
+ await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
8274
+ } catch (err) {
8275
+ if (this.stopped || signal.aborted) return;
8276
+ this.log({
8277
+ level: "error",
8278
+ message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
8279
+ });
8280
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
8281
+ const delayAttempt = healthy ? 0 : attempt;
8282
+ attempt = healthy ? 0 : attempt + 1;
8283
+ try {
8284
+ await this.sleep(backoffDelay(delayAttempt, this.retry));
8285
+ } catch (sleepErr) {
8286
+ this.log({
8287
+ level: "error",
8288
+ message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
8289
+ });
8290
+ }
8291
+ }
8292
+ }
8293
+ }
8294
+ handleSessionError(event) {
8295
+ try {
8296
+ const watcher = this.watchers.get(event.sessionId);
8297
+ if (!watcher) {
8298
+ this.bufferSessionError(event);
8299
+ this.log({
8300
+ level: "debug",
8301
+ message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
8302
+ });
8303
+ return;
8304
+ }
8305
+ if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
8306
+ this.log({
8307
+ level: "debug",
8308
+ message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
8309
+ conversation_id: watcher.conv.id
8310
+ });
8311
+ return;
8312
+ }
8313
+ const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
8314
+ if (!inFlight) {
8315
+ this.bufferSessionError(event);
8316
+ this.log({
8317
+ level: "debug",
8318
+ message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
8319
+ conversation_id: watcher.conv.id
8320
+ });
8321
+ return;
8322
+ }
8323
+ if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
8324
+ this.sessionErrorHandled.add(inFlight.evidentMessageId);
8325
+ void this.failFromSessionError(watcher, event, inFlight);
8326
+ } catch (err) {
8327
+ this.log({
8328
+ level: "error",
8329
+ message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
8330
+ });
8331
+ }
8332
+ }
8333
+ async failFromSessionError(watcher, event, inFlight) {
8334
+ try {
8335
+ const messages = await this.getSessionMessages(event.sessionId);
8336
+ const state = messageRunState(messages, inFlight.opencodeMessageId);
8337
+ if (state !== "queued") {
8338
+ this.log({
8339
+ level: "debug",
8340
+ message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
8341
+ conversation_id: watcher.conv.id,
8342
+ message_id: inFlight.evidentMessageId
8343
+ });
8344
+ return;
8345
+ }
8346
+ this.log({
8347
+ level: "error",
8348
+ message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
8349
+ conversation_id: watcher.conv.id,
8350
+ message_id: inFlight.evidentMessageId
8351
+ });
8352
+ await this.markFailed(
8353
+ watcher.conv.id,
8354
+ inFlight.evidentMessageId,
8355
+ event.sessionId,
8356
+ `OpenCode could not run this turn: ${event.reason}`
8357
+ );
8358
+ inFlight.done = true;
8359
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
8360
+ } catch (err) {
8361
+ if (err instanceof ChannelAuthError) {
8362
+ this.log({
8363
+ level: "warn",
8364
+ 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`,
8365
+ conversation_id: watcher.conv.id,
8366
+ message_id: inFlight.evidentMessageId
8367
+ });
8368
+ } else {
8369
+ this.log({
8370
+ level: "warn",
8371
+ 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`,
8372
+ conversation_id: watcher.conv.id,
8373
+ message_id: inFlight.evidentMessageId
8374
+ });
8375
+ }
8376
+ } finally {
8377
+ this.sessionErrorHandled.delete(inFlight.evidentMessageId);
8378
+ }
8379
+ }
6941
8380
  /**
6942
8381
  * The per-session polling loop (WI-3). Once per tick it:
6943
8382
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -6945,7 +8384,8 @@ var ChannelDriver = class _ChannelDriver {
6945
8384
  * markDone (done) exactly once per transition;
6946
8385
  * 2. applies the idle-path re-dispatch guard (a dispatched message that never
6947
8386
  * APPEARS → re-dispatch — D1 obligation 2);
6948
- * 3. polls `/question` + `/permission` (scoped to the session) and surfaces
8387
+ * 3. polls V1's global `/question` + `/permission`, or V2's
8388
+ * `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
6949
8389
  * NEW ones via `reportInteraction`, carrying the PAUSED message's own
6950
8390
  * `source_message_id`;
6951
8391
  * 4. drops messages that completed or timed out from the in-flight set.
@@ -6971,11 +8411,7 @@ var ChannelDriver = class _ChannelDriver {
6971
8411
  if (watcher.generation !== generation) return;
6972
8412
  let messages = null;
6973
8413
  try {
6974
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
6975
- if (res.ok) {
6976
- const body = await res.json();
6977
- messages = Array.isArray(body) ? body : null;
6978
- }
8414
+ messages = await this.getSessionMessages(sessionId);
6979
8415
  } catch {
6980
8416
  }
6981
8417
  if (messages != null && messages.length > 0) {
@@ -7050,6 +8486,21 @@ var ChannelDriver = class _ChannelDriver {
7050
8486
  const conv = watcher.conv;
7051
8487
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7052
8488
  const id = inFlight.evidentMessageId;
8489
+ if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
8490
+ void this.resolveSubagentInvocations(
8491
+ messages,
8492
+ inFlight.opencodeMessageId,
8493
+ id,
8494
+ "prefetch"
8495
+ ).catch((err) => {
8496
+ this.log({
8497
+ level: "warn",
8498
+ message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
8499
+ conversation_id: conv.id,
8500
+ message_id: id
8501
+ });
8502
+ });
8503
+ }
7053
8504
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
7054
8505
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
7055
8506
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -7103,6 +8554,12 @@ var ChannelDriver = class _ChannelDriver {
7103
8554
  message_id: inFlight.evidentMessageId
7104
8555
  });
7105
8556
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
8557
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
8558
+ const subagentInvocations = await this.resolveSubagentInvocations(
8559
+ messages,
8560
+ inFlight.opencodeMessageId,
8561
+ inFlight.evidentMessageId
8562
+ );
7106
8563
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
7107
8564
  try {
7108
8565
  await this.markFailed(
@@ -7111,7 +8568,9 @@ var ChannelDriver = class _ChannelDriver {
7111
8568
  sessionId,
7112
8569
  error2,
7113
8570
  usage,
7114
- failure
8571
+ failure,
8572
+ usageAgentName,
8573
+ subagentInvocations
7115
8574
  );
7116
8575
  } catch (err) {
7117
8576
  if (err instanceof ChannelAuthError) throw err;
@@ -7154,9 +8613,11 @@ var ChannelDriver = class _ChannelDriver {
7154
8613
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7155
8614
  return;
7156
8615
  }
8616
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
8617
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
7157
8618
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
7158
8619
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
7159
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
8620
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
7160
8621
  inFlight.stuckReported = true;
7161
8622
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
7162
8623
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7183,7 +8644,7 @@ var ChannelDriver = class _ChannelDriver {
7183
8644
  inFlight.b2LastDescendantCheckMs = this.now();
7184
8645
  const [descendantOngoing, rootOngoing] = await Promise.all([
7185
8646
  this.isAnyDescendantSessionOngoing(sessionId),
7186
- isSessionOngoing(this.port, sessionId)
8647
+ this.isSessionOngoing(sessionId)
7187
8648
  ]);
7188
8649
  if (isB2AbandonmentConfirmed({
7189
8650
  pinnedForMs,
@@ -7243,7 +8704,7 @@ var ChannelDriver = class _ChannelDriver {
7243
8704
  });
7244
8705
  }
7245
8706
  const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
7246
- const ongoing = await isSessionOngoing(this.port, sessionId);
8707
+ const ongoing = await this.isSessionOngoing(sessionId);
7247
8708
  if (isAmbiguousFinishResolved({
7248
8709
  pinnedForMs,
7249
8710
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
@@ -7317,7 +8778,7 @@ var ChannelDriver = class _ChannelDriver {
7317
8778
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7318
8779
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7319
8780
  );
7320
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
8781
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7321
8782
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7322
8783
  this.log({
7323
8784
  level: "debug",
@@ -7352,6 +8813,12 @@ var ChannelDriver = class _ChannelDriver {
7352
8813
  });
7353
8814
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7354
8815
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
8816
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
8817
+ const subagentInvocations = await this.resolveSubagentInvocations(
8818
+ messages,
8819
+ inFlight.opencodeMessageId,
8820
+ inFlight.evidentMessageId
8821
+ );
7355
8822
  try {
7356
8823
  await this.markDone(
7357
8824
  conv.id,
@@ -7359,7 +8826,9 @@ var ChannelDriver = class _ChannelDriver {
7359
8826
  sessionId,
7360
8827
  inFlight.opencodeMessageId,
7361
8828
  title,
7362
- usage
8829
+ usage,
8830
+ usageAgentName,
8831
+ subagentInvocations
7363
8832
  );
7364
8833
  } catch (err) {
7365
8834
  if (err instanceof ChannelAuthError) throw err;
@@ -7425,7 +8894,7 @@ var ChannelDriver = class _ChannelDriver {
7425
8894
  ...this.doneUndeliverable,
7426
8895
  ...this.readoptPollUnresolvedSignalled
7427
8896
  ]) {
7428
- if (!stillProcessing.has(id)) {
8897
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
7429
8898
  const cleared = this.dontRedispatch.delete(id);
7430
8899
  const clearedUndeliverable = this.doneUndeliverable.delete(id);
7431
8900
  this.readoptPollUnresolvedSignalled.delete(id);
@@ -7458,23 +8927,32 @@ var ChannelDriver = class _ChannelDriver {
7458
8927
  for (const [sessionId, sessionRows] of bySession) {
7459
8928
  let messages;
7460
8929
  try {
7461
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
7462
- if (!res.ok) {
7463
- this.log({
7464
- level: "warn",
7465
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
7466
- });
7467
- continue;
7468
- }
7469
- const body = await res.json();
7470
- if (!Array.isArray(body)) {
7471
- this.log({
7472
- level: "warn",
7473
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
7474
- });
7475
- continue;
8930
+ if (this.isV2) {
8931
+ const snapshot = await this.getSessionMessages(sessionId);
8932
+ if (snapshot === null) {
8933
+ this.log({
8934
+ level: "warn",
8935
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
8936
+ });
8937
+ continue;
8938
+ }
8939
+ messages = snapshot;
8940
+ } else {
8941
+ const polled = await pollSessionMessagesForRedrive(
8942
+ this.port,
8943
+ sessionId,
8944
+ this.openCodeClient
8945
+ );
8946
+ if (!polled.ok) {
8947
+ const normalized = normalizeRedrivePollFailureBody(polled.body);
8948
+ this.log({
8949
+ level: "warn",
8950
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned ${polled.malformed ? "a non-array message body" : `HTTP ${polled.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 skipping this session this tick`
8951
+ });
8952
+ continue;
8953
+ }
8954
+ messages = polled.messages;
7476
8955
  }
7477
- messages = body;
7478
8956
  } catch (err) {
7479
8957
  this.log({
7480
8958
  level: "warn",
@@ -7483,7 +8961,7 @@ var ChannelDriver = class _ChannelDriver {
7483
8961
  continue;
7484
8962
  }
7485
8963
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
7486
- const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
8964
+ const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
7487
8965
  for (const row of sessionRows) {
7488
8966
  await this.readoptOne(sessionId, row, messages, sessionOngoing);
7489
8967
  }
@@ -7530,7 +9008,7 @@ var ChannelDriver = class _ChannelDriver {
7530
9008
  if (restartAborted) {
7531
9009
  this.log({
7532
9010
  level: "info",
7533
- message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
9011
+ message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
7534
9012
  conversation_id: row.conversation_id,
7535
9013
  message_id: row.id
7536
9014
  });
@@ -7538,6 +9016,12 @@ var ChannelDriver = class _ChannelDriver {
7538
9016
  if (state === "failed" && !restartAborted) {
7539
9017
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7540
9018
  const usage = messageUsage(messages, ocId ?? "");
9019
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
9020
+ const subagentInvocations = await this.resolveSubagentInvocations(
9021
+ messages,
9022
+ ocId ?? "",
9023
+ row.id
9024
+ );
7541
9025
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7542
9026
  this.log({
7543
9027
  level: "error",
@@ -7546,7 +9030,16 @@ var ChannelDriver = class _ChannelDriver {
7546
9030
  message_id: row.id
7547
9031
  });
7548
9032
  try {
7549
- await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage, failure);
9033
+ await this.markFailed(
9034
+ row.conversation_id,
9035
+ row.id,
9036
+ sessionId,
9037
+ error2,
9038
+ usage,
9039
+ failure,
9040
+ usageAgentName,
9041
+ subagentInvocations
9042
+ );
7550
9043
  } catch (err) {
7551
9044
  if (err instanceof ChannelAuthError) throw err;
7552
9045
  if (err instanceof ChannelTerminalError) {
@@ -7593,7 +9086,7 @@ var ChannelDriver = class _ChannelDriver {
7593
9086
  const finish = reply?.info?.finish ?? reply?.finish;
7594
9087
  this.log({
7595
9088
  level: "info",
7596
- message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per GET /session/status \u2014 delivering the existing reply instead of re-dispatching`,
9089
+ message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per the active-session status check \u2014 delivering the existing reply instead of re-dispatching`,
7597
9090
  conversation_id: row.conversation_id,
7598
9091
  message_id: row.id
7599
9092
  });
@@ -7602,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
7602
9095
  }
7603
9096
  this.log({
7604
9097
  level: "info",
7605
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
9098
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
7606
9099
  conversation_id: row.conversation_id,
7607
9100
  message_id: row.id
7608
9101
  });
@@ -7612,7 +9105,7 @@ var ChannelDriver = class _ChannelDriver {
7612
9105
  if (ongoing === true) {
7613
9106
  this.log({
7614
9107
  level: "debug",
7615
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per GET /session/status (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
9108
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per the active-session status check (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
7616
9109
  conversation_id: row.conversation_id,
7617
9110
  message_id: row.id
7618
9111
  });
@@ -7620,7 +9113,7 @@ var ChannelDriver = class _ChannelDriver {
7620
9113
  if (shape === "b1") {
7621
9114
  this.log({
7622
9115
  level: "debug",
7623
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
9116
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
7624
9117
  conversation_id: row.conversation_id,
7625
9118
  message_id: row.id
7626
9119
  });
@@ -7632,7 +9125,7 @@ var ChannelDriver = class _ChannelDriver {
7632
9125
  }
7633
9126
  this.log({
7634
9127
  level: "debug",
7635
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
9128
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
7636
9129
  conversation_id: row.conversation_id,
7637
9130
  message_id: row.id
7638
9131
  });
@@ -7708,7 +9201,22 @@ var ChannelDriver = class _ChannelDriver {
7708
9201
  try {
7709
9202
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7710
9203
  const usage = messageUsage(messages, ocId ?? "");
7711
- await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
9204
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
9205
+ const subagentInvocations = await this.resolveSubagentInvocations(
9206
+ messages,
9207
+ ocId ?? "",
9208
+ row.id
9209
+ );
9210
+ await this.markDone(
9211
+ row.conversation_id,
9212
+ row.id,
9213
+ sessionId,
9214
+ ocId,
9215
+ title,
9216
+ usage,
9217
+ usageAgentName,
9218
+ subagentInvocations
9219
+ );
7712
9220
  } catch (err) {
7713
9221
  if (err instanceof ChannelAuthError) throw err;
7714
9222
  if (err instanceof ChannelTerminalError) {
@@ -7797,19 +9305,90 @@ var ChannelDriver = class _ChannelDriver {
7797
9305
  conversation_id: row.conversation_id,
7798
9306
  message_id: row.id
7799
9307
  });
7800
- this.awaitingReadopt.add(row.id);
9308
+ if (!this.isV2) this.awaitingReadopt.add(row.id);
7801
9309
  const readoptConv = this.convForRow(sessionId, row);
7802
9310
  const readoptMessage = this.queuedMessageForRow(row);
7803
- const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
9311
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
9312
+ if (this.isV2 && row.attachments && row.attachments.length > 0) {
9313
+ this.signalAttachmentsSkipped(
9314
+ row.conversation_id,
9315
+ row.id,
9316
+ row.attachments.map((attachment, index) => ({
9317
+ index,
9318
+ mime: attachment.mime,
9319
+ ...attachment.filename ? { filename: attachment.filename } : {},
9320
+ status: "skipped"
9321
+ })),
9322
+ false
9323
+ );
9324
+ }
7804
9325
  let ocId;
7805
9326
  try {
7806
- ocId = await this.dispatchLocked(
9327
+ ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
7807
9328
  sessionId,
7808
- () => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
9329
+ () => sendPromptAsync(
9330
+ this.port,
9331
+ sessionId,
9332
+ row.content,
9333
+ options,
9334
+ sendAttachments,
9335
+ this.openCodeClient
9336
+ )
7809
9337
  );
7810
9338
  } catch (err) {
7811
9339
  this.awaitingReadopt.delete(row.id);
7812
9340
  if (err instanceof ChannelAuthError) throw err;
9341
+ if (this.isV2) {
9342
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
9343
+ const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
9344
+ if (!invalidPromptAck) {
9345
+ const exists = await this.sessionExists(sessionId);
9346
+ if (exists === false) {
9347
+ this.log({
9348
+ level: "warn",
9349
+ message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage3}`,
9350
+ conversation_id: row.conversation_id,
9351
+ message_id: row.id
9352
+ });
9353
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9354
+ return;
9355
+ }
9356
+ if (exists === null) {
9357
+ this.log({
9358
+ level: "warn",
9359
+ message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage3}`,
9360
+ conversation_id: row.conversation_id,
9361
+ message_id: row.id
9362
+ });
9363
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9364
+ return;
9365
+ }
9366
+ } else {
9367
+ this.dontRedispatch.add(row.id);
9368
+ }
9369
+ this.log({
9370
+ level: "error",
9371
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
9372
+ conversation_id: row.conversation_id,
9373
+ message_id: row.id
9374
+ });
9375
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
9376
+ (markErr) => {
9377
+ this.log({
9378
+ level: "warn",
9379
+ message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9380
+ conversation_id: row.conversation_id,
9381
+ message_id: row.id
9382
+ });
9383
+ if (invalidPromptAck) {
9384
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9385
+ } else {
9386
+ this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
9387
+ }
9388
+ }
9389
+ );
9390
+ return;
9391
+ }
7813
9392
  this.log({
7814
9393
  level: "warn",
7815
9394
  message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
@@ -7820,20 +9399,31 @@ var ChannelDriver = class _ChannelDriver {
7820
9399
  return;
7821
9400
  }
7822
9401
  if (ocId === null) {
9402
+ if (this.isV2) {
9403
+ this.dontRedispatch.add(row.id);
9404
+ this.log({
9405
+ level: "error",
9406
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} completed without an acknowledged message id`,
9407
+ conversation_id: row.conversation_id,
9408
+ message_id: row.id
9409
+ });
9410
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9411
+ return;
9412
+ }
7823
9413
  this.awaitingReadopt.delete(row.id);
7824
9414
  const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
7825
9415
  if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
7826
9416
  this.unconfirmedDispatchFailures.delete(row.id);
7827
9417
  this.sessions.delete(readoptConv.id);
7828
9418
  this.supersede(readoptConv.id, sessionId);
7829
- const errorMessage2 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
9419
+ const errorMessage3 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
7830
9420
  this.log({
7831
9421
  level: "error",
7832
- message: errorMessage2,
9422
+ message: errorMessage3,
7833
9423
  conversation_id: row.conversation_id,
7834
9424
  message_id: row.id
7835
9425
  });
7836
- await this.markFailed(row.conversation_id, row.id, null, errorMessage2).catch((markErr) => {
9426
+ await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
7837
9427
  this.log({
7838
9428
  level: "warn",
7839
9429
  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)}`,
@@ -7941,12 +9531,13 @@ var ChannelDriver = class _ChannelDriver {
7941
9531
  this.dispatched.delete(evidentMessageId);
7942
9532
  }
7943
9533
  /**
7944
- * Poll `/question` + `/permission` (scoped to the session) and surface NEW ones
7945
- * via `reportInteraction` (Task 3.5), carrying the PAUSED message's own
7946
- * `source_message_id` so the server @mentions the correct person under
7947
- * concurrency. Dedups by interaction id across ticks (reused per-session sets).
9534
+ * Poll V1's global `/question` + `/permission`, or V2's watched-session form and
9535
+ * permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
9536
+ * carrying the PAUSED message's own `source_message_id` so the server @mentions
9537
+ * the correct person under concurrency. Dedups by interaction id across ticks
9538
+ * (reused per-session sets).
7948
9539
  *
7949
- * The interaction is attributed to the in-flight message it paused on. opencode
9540
+ * The interaction is attributed to the in-flight message it paused on. OpenCode
7950
9541
  * stamps a `messageID` on a permission (and `tool.messageID` on a question) =
7951
9542
  * the assistant message id, whose `parentID` is the user message id — but the
7952
9543
  * simplest robust attribution here is: the single in-flight message that is
@@ -7969,16 +9560,14 @@ var ChannelDriver = class _ChannelDriver {
7969
9560
  let permissionsPolledOk = true;
7970
9561
  let questions = [];
7971
9562
  try {
7972
- const res = await this.fetchImpl(`${this.opencodeBase}/question`);
7973
- if (res.ok) {
7974
- const body = await res.json();
7975
- if (Array.isArray(body)) {
7976
- questions = body;
7977
- } else {
7978
- questionsPolledOk = false;
7979
- }
9563
+ if (this.isV2) {
9564
+ const forms = await listV2Forms(this.openCodeClient, sessionId);
9565
+ if (forms === null) questionsPolledOk = false;
9566
+ else questions = forms;
7980
9567
  } else {
7981
- questionsPolledOk = false;
9568
+ const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
9569
+ if (listed === null) questionsPolledOk = false;
9570
+ else questions = listed;
7982
9571
  }
7983
9572
  } catch {
7984
9573
  questionsPolledOk = false;
@@ -7998,16 +9587,14 @@ var ChannelDriver = class _ChannelDriver {
7998
9587
  }
7999
9588
  let permissions = [];
8000
9589
  try {
8001
- const res = await this.fetchImpl(`${this.opencodeBase}/permission`);
8002
- if (res.ok) {
8003
- const body = await res.json();
8004
- if (Array.isArray(body)) {
8005
- permissions = body;
8006
- } else {
8007
- permissionsPolledOk = false;
8008
- }
9590
+ if (this.isV2) {
9591
+ const listed = await listV2Permissions(this.openCodeClient, sessionId);
9592
+ if (listed === null) permissionsPolledOk = false;
9593
+ else permissions = listed;
8009
9594
  } else {
8010
- permissionsPolledOk = false;
9595
+ const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
9596
+ if (listed === null) permissionsPolledOk = false;
9597
+ else permissions = listed;
8011
9598
  }
8012
9599
  } catch {
8013
9600
  permissionsPolledOk = false;
@@ -8101,10 +9688,14 @@ var ChannelDriver = class _ChannelDriver {
8101
9688
  if (cached !== void 0) return cached;
8102
9689
  let parent = void 0;
8103
9690
  try {
8104
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8105
- if (res.ok) {
8106
- const body = await res.json();
8107
- parent = body && typeof body.parentID === "string" ? body.parentID : null;
9691
+ if (this.isV2) {
9692
+ const session = await getV2Session(this.openCodeClient, sessionId);
9693
+ parent = null;
9694
+ const candidate = session.parentID;
9695
+ if (typeof candidate === "string") parent = candidate;
9696
+ } else {
9697
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9698
+ parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
8108
9699
  }
8109
9700
  } catch {
8110
9701
  parent = void 0;
@@ -8112,6 +9703,164 @@ var ChannelDriver = class _ChannelDriver {
8112
9703
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
8113
9704
  return parent;
8114
9705
  }
9706
+ usageAgentName(messages, userMessageId) {
9707
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
9708
+ const mode = reply?.info?.mode;
9709
+ if (typeof mode === "string" && mode.length > 0) return mode;
9710
+ const agent = reply?.info?.agent;
9711
+ return typeof agent === "string" && agent.length > 0 ? agent : null;
9712
+ }
9713
+ async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
9714
+ if (!messages) return void 0;
9715
+ const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
9716
+ const cached = cache.get(messageId);
9717
+ if (cached) return cached;
9718
+ const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
9719
+ (err) => {
9720
+ this.log({
9721
+ level: "warn",
9722
+ message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
9723
+ message_id: messageId
9724
+ });
9725
+ return void 0;
9726
+ }
9727
+ );
9728
+ cache.set(messageId, collection);
9729
+ const result = await collection;
9730
+ if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
9731
+ return result;
9732
+ }
9733
+ clearSubagentInvocationCaches(messageId) {
9734
+ this.subagentInvocationCollections.delete(messageId);
9735
+ this.subagentInvocationPrefetches.delete(messageId);
9736
+ }
9737
+ async buildSubagentInvocations(messages, userMessageId, messageId) {
9738
+ const rootCalls = collectTaskCalls(messages, userMessageId);
9739
+ if (rootCalls.length === 0) return void 0;
9740
+ const childMessages = /* @__PURE__ */ new Map();
9741
+ const seenCallIds = new Set(rootCalls.map((call) => call.callID));
9742
+ const work = rootCalls.map((call) => ({
9743
+ call,
9744
+ depth: 1
9745
+ }));
9746
+ const payload = [];
9747
+ const fetchChildMessages = (sessionId) => {
9748
+ const cached = childMessages.get(sessionId);
9749
+ if (cached) return cached;
9750
+ const pending = (async () => {
9751
+ try {
9752
+ const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
9753
+ if (messages2 === null) {
9754
+ this.log({
9755
+ level: "warn",
9756
+ message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was unreadable \u2014 omitting invocation telemetry`,
9757
+ message_id: messageId
9758
+ });
9759
+ return null;
9760
+ }
9761
+ return messages2;
9762
+ } catch (err) {
9763
+ this.log({
9764
+ level: "warn",
9765
+ 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)}`,
9766
+ message_id: messageId
9767
+ });
9768
+ return null;
9769
+ }
9770
+ })();
9771
+ childMessages.set(sessionId, pending);
9772
+ return pending;
9773
+ };
9774
+ const fetchChildWithoutBlocking = async (sessionId) => {
9775
+ const pending = fetchChildMessages(sessionId);
9776
+ let timer;
9777
+ const timeout = new Promise((resolve4) => {
9778
+ timer = setTimeout(() => {
9779
+ this.log({
9780
+ level: "warn",
9781
+ 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`,
9782
+ message_id: messageId
9783
+ });
9784
+ resolve4(null);
9785
+ }, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
9786
+ });
9787
+ try {
9788
+ return await Promise.race([pending, timeout]);
9789
+ } finally {
9790
+ if (timer !== void 0) clearTimeout(timer);
9791
+ }
9792
+ };
9793
+ while (work.length > 0) {
9794
+ const groups = /* @__PURE__ */ new Map();
9795
+ for (const item of work.splice(0)) {
9796
+ const group = groups.get(item.call.childSessionId) ?? [];
9797
+ group.push(item);
9798
+ groups.set(item.call.childSessionId, group);
9799
+ }
9800
+ const groupResults = await Promise.all(
9801
+ [...groups].map(async ([sessionId, items]) => ({
9802
+ sessionId,
9803
+ items,
9804
+ messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
9805
+ }))
9806
+ );
9807
+ for (const { sessionId, items, messages: child } of groupResults) {
9808
+ if (sessionId !== null && child === null) continue;
9809
+ const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
9810
+ child,
9811
+ items.map(({ call }) => ({
9812
+ callID: call.callID,
9813
+ timeStart: call.timeStart,
9814
+ timeEnd: call.timeEnd
9815
+ }))
9816
+ );
9817
+ if (sessionId !== null && attribution.unattributed.length > 0) {
9818
+ this.log({
9819
+ level: "warn",
9820
+ 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`,
9821
+ message_id: messageId
9822
+ });
9823
+ }
9824
+ const usageByCall = new Map(
9825
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
9826
+ );
9827
+ const messagesByCall = new Map(
9828
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
9829
+ );
9830
+ for (const { call, depth } of items) {
9831
+ const usage = usageByCall.get(call.callID) ?? null;
9832
+ payload.push({
9833
+ tool_call_id: call.callID,
9834
+ agent_name: call.subagentName,
9835
+ opencode_session_id: call.childSessionId,
9836
+ parent_opencode_session_id: call.parentSessionId,
9837
+ depth,
9838
+ status: call.status,
9839
+ started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
9840
+ ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
9841
+ usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
9842
+ usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
9843
+ usage_tokens_input: usage?.usage_tokens_input ?? null,
9844
+ usage_tokens_output: usage?.usage_tokens_output ?? null,
9845
+ usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
9846
+ usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
9847
+ usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
9848
+ usage_cost_usd: usage?.usage_cost_usd ?? null
9849
+ });
9850
+ for (const assigned of messagesByCall.get(call.callID) ?? []) {
9851
+ const parentId = assigned.info?.parentID ?? assigned.parentID;
9852
+ if (!parentId) continue;
9853
+ for (const nested of collectTaskCalls([assigned], parentId)) {
9854
+ if (seenCallIds.has(nested.callID)) continue;
9855
+ seenCallIds.add(nested.callID);
9856
+ work.push({ call: nested, depth: depth + 1 });
9857
+ }
9858
+ }
9859
+ }
9860
+ }
9861
+ }
9862
+ return payload.length > 0 ? payload : void 0;
9863
+ }
8115
9864
  /**
8116
9865
  * OpenCode's synchronous default session title (e.g.
8117
9866
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8150,21 +9899,18 @@ var ChannelDriver = class _ChannelDriver {
8150
9899
  const cached = this.sessionTitles.get(sessionId);
8151
9900
  if (cached != null) return cached;
8152
9901
  try {
8153
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8154
- if (res.ok) {
8155
- const body = await res.json();
8156
- const title = body && typeof body.title === "string" ? body.title.trim() : "";
8157
- if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
8158
- this.sessionTitles.set(sessionId, title);
8159
- return title;
8160
- }
8161
- return null;
9902
+ let title = "";
9903
+ if (this.isV2) {
9904
+ title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
9905
+ } else {
9906
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9907
+ title = typeof body?.title === "string" ? body.title.trim() : "";
8162
9908
  }
8163
- this.log({
8164
- level: "debug",
8165
- message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
8166
- conversation_id: conversationId
8167
- });
9909
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
9910
+ this.sessionTitles.set(sessionId, title);
9911
+ return title;
9912
+ }
9913
+ return null;
8168
9914
  } catch (err) {
8169
9915
  this.log({
8170
9916
  level: "debug",
@@ -8262,7 +10008,7 @@ var ChannelDriver = class _ChannelDriver {
8262
10008
  * `SessionStatus` only.
8263
10009
  */
8264
10010
  async isAnyDescendantSessionAlive(rootSessionId) {
8265
- const sessions = await listSessions(this.port);
10011
+ const sessions = await this.listSessions();
8266
10012
  if (!sessions) {
8267
10013
  this.log({
8268
10014
  level: "warn",
@@ -8273,7 +10019,7 @@ var ChannelDriver = class _ChannelDriver {
8273
10019
  for (const candidate of sessions) {
8274
10020
  if (!candidate?.id || candidate.id === rootSessionId) continue;
8275
10021
  if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
8276
- const childMsgs = await getSessionMessages(this.port, candidate.id);
10022
+ const childMsgs = await this.getSubagentSessionMessages(candidate.id);
8277
10023
  if (isSessionActivelyGenerating(childMsgs)) {
8278
10024
  return true;
8279
10025
  }
@@ -8335,7 +10081,7 @@ var ChannelDriver = class _ChannelDriver {
8335
10081
  * `isB2AbandonmentConfirmed`.
8336
10082
  */
8337
10083
  async isAnyDescendantSessionOngoing(rootSessionId) {
8338
- const sessions = await listSessions(this.port);
10084
+ const sessions = await this.listSessions();
8339
10085
  if (!sessions) {
8340
10086
  this.log({
8341
10087
  level: "warn",
@@ -8352,7 +10098,7 @@ var ChannelDriver = class _ChannelDriver {
8352
10098
  continue;
8353
10099
  }
8354
10100
  if (membership === false) continue;
8355
- const ongoing = await isSessionOngoing(this.port, candidate.id);
10101
+ const ongoing = await this.isSessionOngoing(candidate.id);
8356
10102
  if (ongoing === true) return true;
8357
10103
  if (ongoing === null) indeterminate = true;
8358
10104
  }
@@ -8595,7 +10341,7 @@ var ChannelDriver = class _ChannelDriver {
8595
10341
  * watcher retries next tick within the
8596
10342
  * deadline, Finding 4).
8597
10343
  */
8598
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
10344
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8599
10345
  const res = await this.fetchImpl(
8600
10346
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8601
10347
  {
@@ -8611,15 +10357,21 @@ var ChannelDriver = class _ChannelDriver {
8611
10357
  opencode_session_id: sessionId,
8612
10358
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8613
10359
  ...title ? { title } : {},
8614
- ...usage ? usage : {}
10360
+ ...usage ? usage : {},
10361
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
10362
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8615
10363
  })
8616
10364
  }
8617
10365
  );
8618
10366
  this.assertAuth(res, "marking message as done");
8619
- if (res.ok) return;
10367
+ if (res.ok) {
10368
+ this.clearSubagentInvocationCaches(messageId);
10369
+ return;
10370
+ }
8620
10371
  if (isRetryableStatus(res.status)) {
8621
10372
  throw new Error(`marking message as done: HTTP ${res.status}`);
8622
10373
  }
10374
+ this.clearSubagentInvocationCaches(messageId);
8623
10375
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8624
10376
  }
8625
10377
  /**
@@ -8634,7 +10386,7 @@ var ChannelDriver = class _ChannelDriver {
8634
10386
  * exists but is wedged, so the next attempt must get a fresh one
8635
10387
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8636
10388
  */
8637
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
10389
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8638
10390
  const body = { status: "failed" };
8639
10391
  if (sessionId === null) {
8640
10392
  body.opencode_session_id = null;
@@ -8643,23 +10395,33 @@ var ChannelDriver = class _ChannelDriver {
8643
10395
  }
8644
10396
  if (error2 !== void 0) body.error = error2;
8645
10397
  if (usage) Object.assign(body, usage);
10398
+ if (usageAgentName) body.usage_agent_name = usageAgentName;
10399
+ if (subagentInvocations && subagentInvocations.length > 0) {
10400
+ body.subagent_invocations = subagentInvocations;
10401
+ }
8646
10402
  if (failure) {
8647
10403
  body.failure_kind = failure.kind;
8648
10404
  body.failure_provider_id = failure.providerId;
8649
10405
  body.failure_model_id = failure.modelId;
8650
10406
  body.failure_reason = failure.reason;
8651
10407
  }
8652
- await this.callWithRetry(
8653
- "marking message as failed",
8654
- () => this.fetchImpl(
8655
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8656
- {
8657
- method: "PATCH",
8658
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8659
- body: JSON.stringify(body)
8660
- }
8661
- )
8662
- );
10408
+ try {
10409
+ await this.callWithRetry(
10410
+ "marking message as failed",
10411
+ () => this.fetchImpl(
10412
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
10413
+ {
10414
+ method: "PATCH",
10415
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
10416
+ body: JSON.stringify(body)
10417
+ }
10418
+ )
10419
+ );
10420
+ } catch (err) {
10421
+ if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
10422
+ throw err;
10423
+ }
10424
+ this.clearSubagentInvocationCaches(messageId);
8663
10425
  }
8664
10426
  /**
8665
10427
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -8676,7 +10438,7 @@ var ChannelDriver = class _ChannelDriver {
8676
10438
  const classified = messageFailure(messages, userMessageId);
8677
10439
  if (classified != null) return classified;
8678
10440
  const reply = findLastAssistantReplyFor(messages, userMessageId);
8679
- const hasProvider = await hasAnyConfiguredProvider(this.port);
10441
+ const hasProvider = await this.hasAnyConfiguredProvider();
8680
10442
  return applyZeroProviderFallback(
8681
10443
  classified,
8682
10444
  hasProvider,
@@ -8749,7 +10511,7 @@ var ChannelDriver = class _ChannelDriver {
8749
10511
  const succeededProviders = /* @__PURE__ */ new Set();
8750
10512
  for (const ref of refs) {
8751
10513
  try {
8752
- const childMessages = await getSessionMessages(this.port, ref.sessionId);
10514
+ const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
8753
10515
  if (childMessages === null) {
8754
10516
  this.log({
8755
10517
  level: "debug",
@@ -8942,6 +10704,13 @@ import chalk5 from "chalk";
8942
10704
  import ora2 from "ora";
8943
10705
  import { select as select2 } from "@inquirer/prompts";
8944
10706
  var INTERACTIVE_START_TIMEOUT_MS = 3e4;
10707
+ function checkNonInteractivePortConflict(port, isPortInUseFn) {
10708
+ if (isPortInUseFn(port)) {
10709
+ throw new Error(
10710
+ `Port ${port} is already in use by a non-OpenCode process. Free it or pass --port.`
10711
+ );
10712
+ }
10713
+ }
8945
10714
  async function ensureOpenCodeRunning(ctx) {
8946
10715
  const healthCheck = await checkOpenCodeHealth(ctx.port);
8947
10716
  if (healthCheck.healthy) {
@@ -8989,6 +10758,7 @@ async function ensureOpenCodeRunning(ctx) {
8989
10758
  }
8990
10759
  }
8991
10760
  if (!ctx.interactive) {
10761
+ checkNonInteractivePortConflict(ctx.port, isPortInUse);
8992
10762
  ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
8993
10763
  const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
8994
10764
  const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
@@ -9070,9 +10840,159 @@ Port ${port} is already in use.`));
9070
10840
  return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
9071
10841
  }
9072
10842
 
10843
+ // src/commands/ensure-opencode-v2.ts
10844
+ import chalk6 from "chalk";
10845
+ import ora3 from "ora";
10846
+ import { select as select3 } from "@inquirer/prompts";
10847
+ async function probeOpenCode2WithoutPassword(port) {
10848
+ try {
10849
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
10850
+ signal: AbortSignal.timeout(2e3)
10851
+ });
10852
+ if (response.status === 401) {
10853
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
10854
+ }
10855
+ if (!response.ok) {
10856
+ return { healthy: false, error: `HTTP ${response.status}` };
10857
+ }
10858
+ return { healthy: true };
10859
+ } catch (error2) {
10860
+ return {
10861
+ healthy: false,
10862
+ error: error2 instanceof Error ? error2.message : "Unknown error"
10863
+ };
10864
+ }
10865
+ }
10866
+ function unknownPasswordError(port) {
10867
+ return new Error(
10868
+ `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
10869
+ );
10870
+ }
10871
+ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
10872
+ async function ensureOpenCode2Running(ctx) {
10873
+ const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
10874
+ if (initialHealth.authFailed) {
10875
+ throw unknownPasswordError(ctx.port);
10876
+ }
10877
+ if (initialHealth.healthy) {
10878
+ return {
10879
+ port: ctx.port,
10880
+ process: null,
10881
+ version: null,
10882
+ notReadyReason: null,
10883
+ password: null
10884
+ };
10885
+ }
10886
+ if (!isOpenCode2Installed()) {
10887
+ throw new Error(
10888
+ "OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
10889
+ );
10890
+ }
10891
+ let port = ctx.port;
10892
+ if (!ctx.interactive) {
10893
+ checkNonInteractivePortConflict(port, isPortInUse);
10894
+ } else if (isPortInUse(port)) {
10895
+ console.log(chalk6.yellow(`
10896
+ Port ${port} is already in use.`));
10897
+ const alternativePort = findAvailablePort(port + 1);
10898
+ if (alternativePort) {
10899
+ const useAlternative = await select3({
10900
+ message: `Use port ${alternativePort} instead?`,
10901
+ choices: [
10902
+ { name: `Yes, use port ${alternativePort}`, value: "yes" },
10903
+ { name: "No, I will free the port manually", value: "no" }
10904
+ ]
10905
+ });
10906
+ if (useAlternative === "yes") {
10907
+ port = alternativePort;
10908
+ } else {
10909
+ throw new Error(`Port ${ctx.port} is in use`);
10910
+ }
10911
+ }
10912
+ }
10913
+ if (!ctx.interactive) {
10914
+ ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
10915
+ const { child: proc, password } = await startOpenCode2(port, {
10916
+ inheritStdio: ctx.inheritStdio
10917
+ });
10918
+ const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
10919
+ if (!health.healthy) {
10920
+ return {
10921
+ port,
10922
+ process: proc,
10923
+ version: null,
10924
+ notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
10925
+ password
10926
+ };
10927
+ }
10928
+ ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
10929
+ return {
10930
+ port,
10931
+ process: proc,
10932
+ version: health.version ?? null,
10933
+ notReadyReason: null,
10934
+ password
10935
+ };
10936
+ }
10937
+ const action = await select3({
10938
+ message: "OpenCode V2 is not running. What would you like to do?",
10939
+ choices: [
10940
+ {
10941
+ name: "Start OpenCode V2 for me",
10942
+ value: "start",
10943
+ description: `Run 'opencode2 serve --port ${port}'`
10944
+ },
10945
+ {
10946
+ name: "Show me the command",
10947
+ value: "manual",
10948
+ description: "Display the command to run manually"
10949
+ },
10950
+ {
10951
+ name: "Continue without OpenCode V2",
10952
+ value: "continue",
10953
+ description: "Requests will fail until OpenCode V2 starts"
10954
+ }
10955
+ ]
10956
+ });
10957
+ if (action === "manual") {
10958
+ blank();
10959
+ console.log(chalk6.bold("Run this command in another terminal:"));
10960
+ blank();
10961
+ console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
10962
+ blank();
10963
+ throw new Error("Please start OpenCode V2 manually");
10964
+ }
10965
+ if (action === "start") {
10966
+ const spinner = ora3("Starting OpenCode V2...").start();
10967
+ const { child: proc, password } = await startOpenCode2(port, {
10968
+ inheritStdio: ctx.inheritStdio
10969
+ });
10970
+ const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
10971
+ if (!health.healthy) {
10972
+ spinner.fail("Failed to start OpenCode V2");
10973
+ throw new Error("OpenCode V2 failed to start");
10974
+ }
10975
+ spinner.stop();
10976
+ return {
10977
+ port,
10978
+ process: proc,
10979
+ version: health.version ?? null,
10980
+ notReadyReason: null,
10981
+ password
10982
+ };
10983
+ }
10984
+ return {
10985
+ port,
10986
+ process: null,
10987
+ version: null,
10988
+ notReadyReason: "you chose to continue without OpenCode V2",
10989
+ password: null
10990
+ };
10991
+ }
10992
+
9073
10993
  // src/lib/runner-credentials.ts
9074
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
9075
- import { spawn as spawn5 } from "child_process";
10994
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
10995
+ import { spawn as spawn5 } from "node:child_process";
9076
10996
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
9077
10997
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
9078
10998
  var GITHUB_PROBE_TIMEOUT_MS = 1e4;
@@ -9344,11 +11264,11 @@ async function configureGitHubAccess({ env, log: log3 }) {
9344
11264
  }
9345
11265
 
9346
11266
  // src/lib/opencode/config-overlay.ts
9347
- import { execFileSync as execFileSync2 } from "child_process";
9348
- import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
9349
- import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
11267
+ import { execFileSync as execFileSync2 } from "node:child_process";
11268
+ import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
11269
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "node:path";
9350
11270
  function isFile(filePath) {
9351
- return existsSync2(filePath) && statSync5(filePath).isFile();
11271
+ return existsSync2(filePath) && statSync6(filePath).isFile();
9352
11272
  }
9353
11273
  function applyRunnerOpenCodeConfig({
9354
11274
  overlayPath,
@@ -9360,7 +11280,7 @@ function applyRunnerOpenCodeConfig({
9360
11280
  return;
9361
11281
  }
9362
11282
  const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
9363
- const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
11283
+ const target = isFile(join9(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
9364
11284
  if (!isFile(source)) {
9365
11285
  log3(
9366
11286
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -9368,7 +11288,7 @@ function applyRunnerOpenCodeConfig({
9368
11288
  );
9369
11289
  return;
9370
11290
  }
9371
- copyFileSync(source, join8(cwd, target));
11291
+ copyFileSync(source, join9(cwd, target));
9372
11292
  try {
9373
11293
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
9374
11294
  stdio: "ignore"
@@ -9377,11 +11297,11 @@ function applyRunnerOpenCodeConfig({
9377
11297
  const detail = error2 instanceof Error ? error2.message : String(error2);
9378
11298
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
9379
11299
  }
9380
- log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
11300
+ log3(`Applied runner OpenCode config ${source} to ${join9(cwd, target)}`);
9381
11301
  }
9382
11302
 
9383
11303
  // src/lib/credential-sync.ts
9384
- import { renameSync, writeFileSync as writeFileSync5 } from "fs";
11304
+ import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
9385
11305
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
9386
11306
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
9387
11307
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -9391,7 +11311,7 @@ var MAX_FLUSH_PASSES = 2;
9391
11311
  function outcomesWith(outcome) {
9392
11312
  return { claude: outcome, opencode: outcome };
9393
11313
  }
9394
- function errorMessage(error2) {
11314
+ function errorMessage2(error2) {
9395
11315
  return error2 instanceof Error ? error2.message : String(error2);
9396
11316
  }
9397
11317
  function waitForSettlement(promise, timeoutMs) {
@@ -9418,7 +11338,7 @@ function writeMarker(markerPath, outcomes, log3) {
9418
11338
  writeFileSync5(temporaryPath, body, { mode: 384 });
9419
11339
  renameSync(temporaryPath, markerPath);
9420
11340
  } catch (error2) {
9421
- log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage(error2)}`, "warn");
11341
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
9422
11342
  }
9423
11343
  }
9424
11344
  function intervalSeconds(env, log3) {
@@ -9450,7 +11370,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
9450
11370
  },
9451
11371
  (error2) => {
9452
11372
  failed = true;
9453
- log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage(error2)}`, "warn");
11373
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
9454
11374
  }
9455
11375
  );
9456
11376
  const abortTimer = setTimeout(() => controller.abort(), remainingMs);
@@ -9510,7 +11430,7 @@ function createCredentialSync({
9510
11430
  outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
9511
11431
  } catch (error2) {
9512
11432
  outcomes[store] = "failed";
9513
- log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage(error2)}`, "debug");
11433
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
9514
11434
  }
9515
11435
  }
9516
11436
  const failed = STORES.some((store) => outcomes[store] === "failed");
@@ -9652,7 +11572,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
9652
11572
  if (trimmed === "") {
9653
11573
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
9654
11574
  }
9655
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join9(homeDir, trimmed.slice(2)) : trimmed;
11575
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join10(homeDir, trimmed.slice(2)) : trimmed;
9656
11576
  if (!isAbsolute3(expanded)) {
9657
11577
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
9658
11578
  }
@@ -9676,6 +11596,28 @@ function resolveFileSyncDirectories(raw, homeDir) {
9676
11596
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
9677
11597
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
9678
11598
  var OPENCODE_START_TIMEOUT_ENV = "EVIDENT_OPENCODE_START_TIMEOUT";
11599
+ var OPENCODE_VERSION_ENV = "EVIDENT_OPENCODE_VERSION";
11600
+ function resolveOpenCodeVersion(options, env = process.env) {
11601
+ let raw;
11602
+ let source;
11603
+ if (options.opencodeVersion !== void 0) {
11604
+ raw = options.opencodeVersion;
11605
+ source = "--opencode-version";
11606
+ } else if (env[OPENCODE_VERSION_ENV] !== void 0 && env[OPENCODE_VERSION_ENV] !== "") {
11607
+ raw = env[OPENCODE_VERSION_ENV];
11608
+ source = OPENCODE_VERSION_ENV;
11609
+ } else {
11610
+ return { version: "v1", warnings: [] };
11611
+ }
11612
+ const normalized = raw.trim().toLowerCase();
11613
+ if (normalized !== "v1" && normalized !== "v2") {
11614
+ return {
11615
+ version: "v1",
11616
+ warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
11617
+ };
11618
+ }
11619
+ return { version: normalized, warnings: [] };
11620
+ }
9679
11621
  function resolveOpenCodeStartTimeoutMs(options, env = process.env) {
9680
11622
  const defaultMs = DEFAULT_OPENCODE_START_TIMEOUT_SECONDS * 1e3;
9681
11623
  let raw;
@@ -9742,7 +11684,7 @@ function log2(state, message, level = "info") {
9742
11684
  })
9743
11685
  );
9744
11686
  } else if (!state.interactive) {
9745
- const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
11687
+ const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
9746
11688
  console.log(`${prefix} ${message}`);
9747
11689
  }
9748
11690
  }
@@ -9772,7 +11714,7 @@ function logActivity(state, entry) {
9772
11714
  }
9773
11715
  function reportSessionDbRecovery(state) {
9774
11716
  try {
9775
- const report = drainSessionDbRecoveryReport({ homeDir: homedir5(), env: process.env });
11717
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
9776
11718
  for (const record of report.records) {
9777
11719
  const activity = buildSessionDbRecoveryActivity(record);
9778
11720
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -9803,18 +11745,18 @@ function reportSessionDbRecoveryRecord(state, record) {
9803
11745
  function displayStatus(state) {
9804
11746
  if (!state.interactive) return;
9805
11747
  const attempt = state.connection?.reconnectAttempt ?? 0;
9806
- const tunnel = state.connected ? chalk6.green("tunnel: connected") : attempt > 0 ? chalk6.yellow(`tunnel: reconnecting (#${attempt})`) : chalk6.yellow("tunnel: connecting");
9807
- const opencode = state.opencodeConnected ? chalk6.green(`opencode: :${state.port}`) : chalk6.red(`opencode: :${state.port} (down)`);
9808
- const messages = state.messageCount > 0 ? chalk6.dim(` \xB7 ${state.messageCount} processed`) : "";
11748
+ const tunnel = state.connected ? chalk7.green("tunnel: connected") : attempt > 0 ? chalk7.yellow(`tunnel: reconnecting (#${attempt})`) : chalk7.yellow("tunnel: connecting");
11749
+ const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
11750
+ const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
9809
11751
  const last = state.activityLog[state.activityLog.length - 1];
9810
- const detail = last ? chalk6.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
11752
+ const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
9811
11753
  const agent = state.agentName ?? state.agentId;
9812
11754
  console.log(
9813
- `${chalk6.bold("Evident")} ${chalk6.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
11755
+ `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
9814
11756
  );
9815
11757
  }
9816
11758
  async function promptForLogin(promptMessage, successMessage) {
9817
- const action = await select3({
11759
+ const action = await select4({
9818
11760
  message: promptMessage,
9819
11761
  choices: [
9820
11762
  {
@@ -9830,7 +11772,7 @@ async function promptForLogin(promptMessage, successMessage) {
9830
11772
  ]
9831
11773
  });
9832
11774
  if (action === "exit") {
9833
- console.log(chalk6.dim(`
11775
+ console.log(chalk7.dim(`
9834
11776
  You can log in later by running: ${getCliName()} login`));
9835
11777
  process.exit(0);
9836
11778
  }
@@ -9841,7 +11783,7 @@ You can log in later by running: ${getCliName()} login`));
9841
11783
  process.exit(1);
9842
11784
  }
9843
11785
  blank();
9844
- console.log(chalk6.green(successMessage));
11786
+ console.log(chalk7.green(successMessage));
9845
11787
  blank();
9846
11788
  return { token: credentials2.token, authType: "bearer", user: credentials2.user };
9847
11789
  }
@@ -9854,12 +11796,12 @@ async function handleAuthError(state, error2) {
9854
11796
  if (state.interactive) displayStatus(state);
9855
11797
  if (!state.interactive) {
9856
11798
  blank();
9857
- console.log(chalk6.red("Authentication expired"));
9858
- console.log(chalk6.dim("Your authentication token is no longer valid."));
11799
+ console.log(chalk7.red("Authentication expired"));
11800
+ console.log(chalk7.dim("Your authentication token is no longer valid."));
9859
11801
  blank();
9860
- console.log(chalk6.dim("To fix this:"));
9861
- console.log(chalk6.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
9862
- console.log(chalk6.dim(" 2. Restart this command"));
11802
+ console.log(chalk7.dim("To fix this:"));
11803
+ console.log(chalk7.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
11804
+ console.log(chalk7.dim(" 2. Restart this command"));
9863
11805
  blank();
9864
11806
  await cleanup(state);
9865
11807
  await shutdownTelemetry();
@@ -9867,7 +11809,7 @@ async function handleAuthError(state, error2) {
9867
11809
  return { success: false };
9868
11810
  }
9869
11811
  blank();
9870
- console.log(chalk6.yellow("Your authentication has expired."));
11812
+ console.log(chalk7.yellow("Your authentication has expired."));
9871
11813
  blank();
9872
11814
  try {
9873
11815
  const credentials2 = await promptForLogin(
@@ -9931,6 +11873,14 @@ async function driveChannels(state, driver) {
9931
11873
  const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
9932
11874
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
9933
11875
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
11876
+ if (claudeCredentialApplied || opencodeAuthApplied) {
11877
+ void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
11878
+ (error2) => logActivity(state, {
11879
+ type: "error",
11880
+ error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
11881
+ })
11882
+ );
11883
+ }
9934
11884
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
9935
11885
  idlePolls = 0;
9936
11886
  idleMs = 0;
@@ -9958,8 +11908,8 @@ async function driveChannels(state, driver) {
9958
11908
  state.running = false;
9959
11909
  break;
9960
11910
  }
9961
- const errorMessage2 = error2 instanceof Error ? error2.message : String(error2);
9962
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage2}` });
11911
+ const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
11912
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
9963
11913
  if (state.interactive) displayStatus(state);
9964
11914
  if (driver.hasInFlightWatchers()) {
9965
11915
  consecutiveDrainFailures = 0;
@@ -9997,9 +11947,18 @@ async function driveChannels(state, driver) {
9997
11947
  }
9998
11948
  }
9999
11949
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
10000
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
11950
+ var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
11951
+ function shouldWarnForReclaimSkip(reason) {
11952
+ if (reason !== "sqlite-unavailable") return false;
11953
+ const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
11954
+ if (!version2) return false;
11955
+ const major = Number(version2[1]);
11956
+ const minor = Number(version2[2]);
11957
+ const patch = Number(version2[3]);
11958
+ return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
11959
+ }
10001
11960
  function sessionDbPath() {
10002
- return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
11961
+ return join10(homedir6(), ".local", "share", "opencode", "opencode.db");
10003
11962
  }
10004
11963
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
10005
11964
  const record = {
@@ -10040,7 +11999,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
10040
11999
  async function runSweep(state, driver, config) {
10041
12000
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
10042
12001
  try {
10043
- const sessions = await listSessions(state.port);
12002
+ const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
10044
12003
  if (sessions === null) {
10045
12004
  logActivity(state, {
10046
12005
  type: "info",
@@ -10070,7 +12029,7 @@ async function runSweep(state, driver, config) {
10070
12029
  });
10071
12030
  continue;
10072
12031
  }
10073
- if (await deleteSession(state.port, id)) deleted++;
12032
+ if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
10074
12033
  else failed++;
10075
12034
  }
10076
12035
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
@@ -10095,7 +12054,7 @@ async function runSweep(state, driver, config) {
10095
12054
  } else {
10096
12055
  logActivity(state, {
10097
12056
  type: "info",
10098
- message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
12057
+ message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
10099
12058
  });
10100
12059
  }
10101
12060
  } catch (error2) {
@@ -10118,13 +12077,20 @@ function scheduleSessionCleanup(state, driver, options) {
10118
12077
  for (const warning2 of config.warnings) {
10119
12078
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
10120
12079
  }
10121
- const dbBytes = statSessionDbBytes(homedir5());
12080
+ const dbBytes = statSessionDbBytes(homedir6());
10122
12081
  void (async () => {
10123
- const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
12082
+ const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
12083
+ if (reclaimAvailability !== null) {
12084
+ logActivity(state, {
12085
+ type: "info",
12086
+ level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
12087
+ message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
12088
+ });
12089
+ }
10124
12090
  const sizeWarning = buildSessionStoreSizeWarning({
10125
12091
  dbBytes,
10126
12092
  cleanupEnabled: config.enabled,
10127
- reclaimSkipReason
12093
+ reclaimSkipReason: reclaimAvailability?.reason ?? null
10128
12094
  });
10129
12095
  if (sizeWarning !== null) {
10130
12096
  logActivity(state, { type: "info", level: "warn", message: sizeWarning });
@@ -10291,14 +12257,11 @@ function scheduleClaudeUsageReporting(state, options) {
10291
12257
  report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
10292
12258
  isLocalCredentialProblem,
10293
12259
  forcedOnHint: "run `claude` to sign in",
10294
- firstDelayMs: () => FIRST_REPORT_DELAY_MS,
10295
- nextDelayMs: nextReportDelayMs,
10296
- failureLogLevel: claudeUsageFailureLogLevel
12260
+ firstDelayMs: firstReportDelayMs,
12261
+ nextDelayMs: usageReportDelayMs,
12262
+ failureLogLevel: usageReportFailureLogLevel
10297
12263
  });
10298
12264
  }
10299
- var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
10300
- var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
10301
- var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
10302
12265
  function scheduleResourceUsageReporting(state, options) {
10303
12266
  const { enabled, warnings } = resolveResourceUsageReportingEnabled(
10304
12267
  options.resourceUsageReporting,
@@ -10319,7 +12282,7 @@ function scheduleResourceUsageReporting(state, options) {
10319
12282
  });
10320
12283
  return;
10321
12284
  }
10322
- const { collect, stop } = createResourceUsageCollector(homedir5());
12285
+ const { collect, stop } = createResourceUsageCollector(homedir6());
10323
12286
  state.stopResourceUsageSampling = stop;
10324
12287
  let consecutiveFailures = 0;
10325
12288
  const tick = async () => {
@@ -10351,10 +12314,7 @@ function scheduleResourceUsageReporting(state, options) {
10351
12314
  consecutiveFailures++;
10352
12315
  logActivity(state, {
10353
12316
  type: "info",
10354
- level: reportFailureLogLevel(
10355
- consecutiveFailures,
10356
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10357
- ),
12317
+ level: usageReportFailureLogLevel(consecutiveFailures),
10358
12318
  message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
10359
12319
  });
10360
12320
  }
@@ -10363,20 +12323,11 @@ function scheduleResourceUsageReporting(state, options) {
10363
12323
  const message = error2 instanceof Error ? error2.message : String(error2);
10364
12324
  logActivity(state, {
10365
12325
  type: "info",
10366
- level: reportFailureLogLevel(
10367
- consecutiveFailures,
10368
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10369
- ),
12326
+ level: usageReportFailureLogLevel(consecutiveFailures),
10370
12327
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
10371
12328
  });
10372
12329
  } finally {
10373
- state.resourceUsageTimer = setTimeout(
10374
- () => void tick(),
10375
- jitteredDelayMs(
10376
- RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
10377
- RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
10378
- )
10379
- );
12330
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
10380
12331
  }
10381
12332
  };
10382
12333
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -10416,6 +12367,8 @@ async function cleanup(state, opts = {}) {
10416
12367
  clearTimeout(timer);
10417
12368
  }
10418
12369
  state.sessionCleanupTimers = [];
12370
+ state.stopOpenCodeLogTail?.();
12371
+ state.stopOpenCodeLogTail = null;
10419
12372
  if (state.claudeUsageTimer) {
10420
12373
  clearTimeout(state.claudeUsageTimer);
10421
12374
  state.claudeUsageTimer = null;
@@ -10554,7 +12507,7 @@ async function run(options) {
10554
12507
  let fileSyncDirectories;
10555
12508
  try {
10556
12509
  logLevel = resolveLogLevel(options);
10557
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir5());
12510
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
10558
12511
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
10559
12512
  throw new Error(
10560
12513
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -10583,8 +12536,12 @@ async function run(options) {
10583
12536
  connected: false,
10584
12537
  opencodeConnected: false,
10585
12538
  opencodeVersion: null,
12539
+ opencodeApiVersion: "v1",
12540
+ opencodePassword: null,
12541
+ opencodeClient: null,
10586
12542
  sessionDbProvenanceAnomaly: false,
10587
12543
  opencodeProcess: null,
12544
+ stopOpenCodeLogTail: null,
10588
12545
  litestreamProcess: null,
10589
12546
  connection: null,
10590
12547
  channelDriver: null,
@@ -10652,15 +12609,15 @@ async function run(options) {
10652
12609
  printError("Authentication required");
10653
12610
  blank();
10654
12611
  console.log(
10655
- chalk6.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
12612
+ chalk7.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
10656
12613
  );
10657
- console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
12614
+ console.log(chalk7.dim("Or run `evident login` for interactive authentication"));
10658
12615
  blank();
10659
12616
  process.exit(1);
10660
12617
  return;
10661
12618
  }
10662
12619
  blank();
10663
- console.log(chalk6.yellow("You are not logged in to Evident."));
12620
+ console.log(chalk7.yellow("You are not logged in to Evident."));
10664
12621
  blank();
10665
12622
  credentials2 = await promptForLogin(
10666
12623
  "Would you like to log in now?",
@@ -10710,7 +12667,7 @@ async function run(options) {
10710
12667
  );
10711
12668
  blank();
10712
12669
  console.log(
10713
- chalk6.dim(
12670
+ chalk7.dim(
10714
12671
  "Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
10715
12672
  )
10716
12673
  );
@@ -10733,15 +12690,15 @@ async function run(options) {
10733
12690
  );
10734
12691
  if (interactive && !state.json) {
10735
12692
  blank();
10736
- console.log(chalk6.bold("Evident Run"));
10737
- console.log(chalk6.dim("-".repeat(40)));
12693
+ console.log(chalk7.bold("Evident Run"));
12694
+ console.log(chalk7.dim("-".repeat(40)));
10738
12695
  }
10739
- const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
12696
+ const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
10740
12697
  let validation = await getAgentInfo(state.agentId, state.authHeader);
10741
12698
  if (!validation.valid && validation.authFailed && interactive) {
10742
12699
  spinner?.fail("Authentication failed");
10743
12700
  blank();
10744
- console.log(chalk6.yellow("Your authentication token is invalid or expired."));
12701
+ console.log(chalk7.yellow("Your authentication token is invalid or expired."));
10745
12702
  blank();
10746
12703
  credentials2 = await promptForLogin(
10747
12704
  "Would you like to log in again?",
@@ -10789,6 +12746,13 @@ async function run(options) {
10789
12746
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
10790
12747
  }
10791
12748
  state.credentialSync?.arm();
12749
+ state.stopOpenCodeLogTail = tailOpenCodeLogFile(
12750
+ resolveOpenCodeLogPath(homedir6(), process.env),
12751
+ createOpenCodeActivityForwarder(() => ({
12752
+ agentId: state.agentId,
12753
+ authHeader: state.authHeader
12754
+ }))
12755
+ ).stop;
10792
12756
  let sessionDbVerifyFatal = false;
10793
12757
  if (!options.restoreSessionDb) {
10794
12758
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -10838,14 +12802,28 @@ async function run(options) {
10838
12802
  for (const warning2 of opencodeStartTimeoutWarnings) {
10839
12803
  logActivity(state, { type: "info", level: "warn", message: warning2 });
10840
12804
  }
12805
+ const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
12806
+ options,
12807
+ process.env
12808
+ );
12809
+ for (const warning2 of opencodeVersionWarnings) {
12810
+ logActivity(state, { type: "info", level: "warn", message: warning2 });
12811
+ }
10841
12812
  const { value: maxActiveSessions, warnings: maxActiveSessionsWarnings } = resolveMaxActiveSessions(options, process.env);
10842
12813
  for (const warning2 of maxActiveSessionsWarnings) {
10843
12814
  logActivity(state, { type: "info", level: "warn", message: warning2 });
10844
12815
  }
10845
12816
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
10846
- const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
12817
+ const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
10847
12818
  try {
10848
- const oc = await ensureOpenCodeRunning({
12819
+ const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
12820
+ port: state.port,
12821
+ interactive: state.interactive,
12822
+ agentId: state.agentId,
12823
+ log: (message) => log2(state, message),
12824
+ startTimeoutMs: opencodeStartTimeoutMs,
12825
+ inheritStdio: Boolean(options.opencodePidFile)
12826
+ }) : await ensureOpenCodeRunning({
10849
12827
  port: state.port,
10850
12828
  interactive: state.interactive,
10851
12829
  agentId: state.agentId,
@@ -10856,6 +12834,19 @@ async function run(options) {
10856
12834
  state.port = oc.port;
10857
12835
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
10858
12836
  state.opencodeVersion = oc.version;
12837
+ state.opencodeApiVersion = opencodeVersion;
12838
+ let opencodePassword = null;
12839
+ if (opencodeVersion === "v2" && "password" in oc) {
12840
+ const value = oc.password;
12841
+ if (typeof value === "string" || value === null) opencodePassword = value;
12842
+ }
12843
+ state.opencodePassword = opencodePassword;
12844
+ const openCodeClient = createOpenCodeClient({
12845
+ port: state.port,
12846
+ version: state.opencodeApiVersion,
12847
+ password: state.opencodePassword
12848
+ });
12849
+ state.opencodeClient = openCodeClient;
10859
12850
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
10860
12851
  try {
10861
12852
  writeFileSync6(options.opencodePidFile, `${oc.process.pid}
@@ -10872,7 +12863,7 @@ async function run(options) {
10872
12863
  const provenance = checkSessionDbProvenance({
10873
12864
  dbPath: sessionDbPath(),
10874
12865
  currentVersion: state.opencodeVersion,
10875
- homeDir: homedir5(),
12866
+ homeDir: homedir6(),
10876
12867
  env: process.env
10877
12868
  });
10878
12869
  if (provenance.anomaly) {
@@ -10892,25 +12883,29 @@ async function run(options) {
10892
12883
  const message = `OpenCode is not ready on port ${state.port}: ${oc.notReadyReason}. The runner will still come online, but messages will fail until opencode answers \u2014 raise the wait with --opencode-start-timeout <seconds> (env ${OPENCODE_START_TIMEOUT_ENV}).`;
10893
12884
  logActivity(state, { type: "info", level: "warn", message });
10894
12885
  } else {
10895
- const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
12886
+ const versionWarning = buildOpenCodeVersionWarning(
12887
+ state.opencodeVersion,
12888
+ state.opencodeApiVersion
12889
+ );
10896
12890
  if (versionWarning) {
10897
12891
  log2(state, versionWarning, "warn");
10898
12892
  if (state.interactive && !state.json) {
10899
12893
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
10900
12894
  }
10901
12895
  }
12896
+ await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
10902
12897
  const noProviderWarning = buildNoProviderWarning(
10903
- await hasAnyConfiguredProvider(state.port)
12898
+ await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
10904
12899
  );
10905
12900
  if (noProviderWarning) {
10906
12901
  log2(state, noProviderWarning, "warn");
10907
12902
  if (state.interactive && !state.json) {
10908
12903
  logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
10909
12904
  blank();
10910
- console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
12905
+ console.log(chalk7.yellow("\u26A0 No OpenCode model provider is configured."));
10911
12906
  console.log(
10912
- chalk6.dim(
10913
- `Run ${chalk6.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
12907
+ chalk7.dim(
12908
+ `Run ${chalk7.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
10914
12909
  )
10915
12910
  );
10916
12911
  blank();
@@ -11023,18 +13018,19 @@ async function run(options) {
11023
13018
  });
11024
13019
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
11025
13020
  }
11026
- const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
13021
+ const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
11027
13022
  const channelDriver = new ChannelDriver({
11028
13023
  agentId: state.agentId,
11029
13024
  port: state.port,
11030
13025
  apiUrl: getApiUrlConfig(),
13026
+ openCodeClient: state.opencodeClient ?? void 0,
11031
13027
  getAuthHeader: () => state.authHeader,
11032
13028
  conversationFilter: state.conversationFilter,
11033
13029
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
11034
13030
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
11035
13031
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
11036
13032
  fileSyncDirectories,
11037
- homeDir: homedir5(),
13033
+ homeDir: homedir6(),
11038
13034
  maxActiveSessions,
11039
13035
  log: (entry) => (
11040
13036
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -11053,6 +13049,7 @@ async function run(options) {
11053
13049
  agentId: state.agentId,
11054
13050
  getAuthHeader: () => state.authHeader,
11055
13051
  port: state.port,
13052
+ openCodePassword: state.opencodePassword,
11056
13053
  isRunning: () => state.running,
11057
13054
  events: {
11058
13055
  onConnected: (agentId, isReconnect) => {
@@ -11077,7 +13074,11 @@ async function run(options) {
11077
13074
  emitAgentConnected(state.agentId, {
11078
13075
  port: state.port,
11079
13076
  cli_version: getCliVersion(),
11080
- opencode_version: state.opencodeVersion
13077
+ opencode_version: reportedOpenCodeVersion({
13078
+ version: state.opencodeVersion,
13079
+ major: state.opencodeApiVersion,
13080
+ connected: state.opencodeConnected
13081
+ })
11081
13082
  });
11082
13083
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
11083
13084
  if (state.interactive) displayStatus(state);
@@ -11193,7 +13194,7 @@ async function run(options) {
11193
13194
  state.openaiUsageTimer = timer;
11194
13195
  },
11195
13196
  fetchUsage: async () => {
11196
- const usage = await getOpenAiUsage(state.port);
13197
+ const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
11197
13198
  if (usage.subscription === null) {
11198
13199
  logActivity(state, {
11199
13200
  type: "info",
@@ -11276,6 +13277,9 @@ program.command("run").description("Connect to Evident and process messages").op
11276
13277
  ).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option(
11277
13278
  "--opencode-start-timeout <seconds>",
11278
13279
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
13280
+ ).option(
13281
+ "--opencode-version <v1|v2>",
13282
+ "Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
11279
13283
  ).option("--json", "Output in JSON format").option(
11280
13284
  "--session-cleanup-max-age <duration>",
11281
13285
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -11344,6 +13348,7 @@ program.command("run").description("Connect to Evident and process messages").op
11344
13348
  // Raw string — validation/precedence is single-sourced in run.ts's
11345
13349
  // resolveOpenCodeStartTimeoutMs (flag > EVIDENT_OPENCODE_START_TIMEOUT > 180s default).
11346
13350
  opencodeStartTimeout: options.opencodeStartTimeout,
13351
+ opencodeVersion: options.opencodeVersion,
11347
13352
  json: options.json,
11348
13353
  // Raw strings — the resolver in run.ts single-sources parsing (M1).
11349
13354
  sessionCleanupMaxAge: options.sessionCleanupMaxAge,