@evident-ai/cli 3.4.1-dev.0b03f1c → 3.4.1-dev.0c8854e

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
@@ -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
  }
@@ -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
  }
@@ -1253,11 +1178,11 @@ function stripQuery(url) {
1253
1178
  }
1254
1179
 
1255
1180
  // src/commands/run.ts
1256
- import ora3 from "ora";
1181
+ import ora4 from "ora";
1257
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.0c8854e" : void 0) ?? process.env.npm_package_version ?? "unknown";
1261
1186
  function getCliVersion() {
1262
1187
  return CLI_VERSION;
1263
1188
  }
@@ -1731,6 +1656,11 @@ function isSessionDbRecoveryRecord(value) {
1731
1656
  );
1732
1657
  }
1733
1658
 
1659
+ // src/lib/opencode/auth.ts
1660
+ function buildOpenCodeBasicAuthHeader(password) {
1661
+ return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
1662
+ }
1663
+
1734
1664
  // src/lib/opencode/health.ts
1735
1665
  async function checkOpenCodeHealth(port) {
1736
1666
  try {
@@ -1748,6 +1678,27 @@ async function checkOpenCodeHealth(port) {
1748
1678
  return { healthy: false, error: message };
1749
1679
  }
1750
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
+ }
1751
1702
  async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1752
1703
  const startTime = Date.now();
1753
1704
  while (Date.now() - startTime < timeoutMs) {
@@ -1759,6 +1710,61 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1759
1710
  }
1760
1711
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1761
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
+ }
1762
1768
 
1763
1769
  // src/lib/opencode/session-db-boot.ts
1764
1770
  import { spawn as spawn2 } from "node:child_process";
@@ -2422,15 +2428,23 @@ function isQueueValidatedVersion(version2) {
2422
2428
  if (!version2) return false;
2423
2429
  return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
2424
2430
  }
2425
- function buildOpenCodeVersionWarning(version2) {
2426
- if (isQueueValidatedVersion(version2)) return null;
2427
- const detected = version2 ? `v${version2}` : "unknown";
2431
+ function buildOpenCodeVersionWarning(version2, major) {
2432
+ if (major === "v2") return null;
2428
2433
  const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
2429
- 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`;
2430
2443
  }
2431
2444
 
2432
2445
  // src/lib/opencode/process.ts
2433
2446
  import { execSync, spawn as spawn3 } from "child_process";
2447
+ import { randomBytes } from "node:crypto";
2434
2448
 
2435
2449
  // src/lib/process-stop.ts
2436
2450
  async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
@@ -2489,6 +2503,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2489
2503
  // src/lib/opencode/process.ts
2490
2504
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2491
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
+ ]);
2492
2517
  function resolveOpenCodeLogLevel(env) {
2493
2518
  const raw = env.OPENCODE_LOG_LEVEL;
2494
2519
  if (!raw) return "INFO";
@@ -2499,6 +2524,16 @@ function resolveOpenCodeLogLevel(env) {
2499
2524
  );
2500
2525
  return "INFO";
2501
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
+ }
2502
2537
  function getProcessCwd(pid) {
2503
2538
  const platform = process.platform;
2504
2539
  try {
@@ -2681,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
2681
2716
  });
2682
2717
  return child;
2683
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
+ }
2684
2750
  function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
2685
2751
  const sendSignal = (signal) => {
2686
2752
  if (process.platform === "win32") {
@@ -2828,27 +2894,500 @@ function buildNoProviderWarning(hasProvider) {
2828
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).";
2829
2895
  }
2830
2896
 
2831
- // src/lib/http-timeout.ts
2832
- var REQUEST_TIMEOUT_MS = 6e4;
2833
- function withRequestTimeout(fetchImpl, timeoutMs) {
2834
- 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);
2900
+ }
2901
+ function finiteNumber(value) {
2902
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2903
+ }
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
+ };
2913
+ }
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;
2925
+ }
2926
+ return {
2927
+ ...input !== void 0 ? { input } : {},
2928
+ ...output !== void 0 ? { output } : {},
2929
+ ...reasoning !== void 0 ? { reasoning } : {},
2930
+ ...cache ? { cache } : {}
2931
+ };
2932
+ }
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
+ };
2978
+ }
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 };
2984
+ }
2985
+ function adaptV2FormWire(value) {
2986
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
2987
+ return null;
2988
+ }
2989
+ return value;
2990
+ }
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
+ };
3022
+ }
3023
+ function adaptV2FormList(value) {
3024
+ if (!isRecord(value) || !Array.isArray(value.data)) return null;
3025
+ return value.data.map(adaptV2Form).filter((form) => form !== null);
3026
+ }
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
+ }
2835
3367
  }
2836
3368
 
2837
3369
  // src/lib/opencode/session.ts
3370
+ var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
2838
3371
  function timedFetch(input, init) {
2839
3372
  return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
2840
3373
  }
3374
+ function requestWithClient(port, client, path, init, options) {
3375
+ return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
3376
+ }
2841
3377
  function opencodeBase(port) {
2842
3378
  return `http://127.0.0.1:${port}`;
2843
3379
  }
2844
- async function getOpenCodeDirectory(port) {
3380
+ async function getOpenCodeDirectory(port, client) {
2845
3381
  try {
2846
- const res = await timedFetch(`${opencodeBase(port)}/path`);
3382
+ const res = await requestWithClient(port, client, "/path");
2847
3383
  if (!res.ok) return null;
2848
3384
  const body = await res.json();
2849
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;
2850
3386
  return dir && dir.trim() ? dir.trim() : null;
2851
- } catch {
3387
+ } catch (error2) {
3388
+ console.error(
3389
+ `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3390
+ );
2852
3391
  return null;
2853
3392
  }
2854
3393
  }
@@ -2892,16 +3431,48 @@ function isAssistantInFlight(m) {
2892
3431
  if (completedOf(m) == null) return true;
2893
3432
  return finishOf(m) === "tool-calls";
2894
3433
  }
2895
- async function getSessionMessages(port, sessionId) {
3434
+ async function getSessionMessages(port, sessionId, client) {
2896
3435
  try {
2897
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/message`);
3436
+ const path = `/session/${sessionId}/message`;
3437
+ const res = await requestWithClient(port, client, path);
2898
3438
  if (!res.ok) return null;
2899
3439
  const body = await res.json();
2900
3440
  return Array.isArray(body) ? body : null;
2901
- } catch {
3441
+ } catch (error2) {
3442
+ console.error(
3443
+ `[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3444
+ );
2902
3445
  return null;
2903
3446
  }
2904
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
+ }
2905
3476
  function isSessionActivelyGenerating(messages) {
2906
3477
  if (!messages || messages.length === 0) return false;
2907
3478
  const last = messages[messages.length - 1];
@@ -2922,27 +3493,37 @@ function sessionLastActivityMs(session) {
2922
3493
  }
2923
3494
  return null;
2924
3495
  }
2925
- async function listSessions(port) {
3496
+ async function listSessions(port, client) {
3497
+ if (client?.version === "v2") return listV2Sessions(client);
2926
3498
  try {
2927
- const res = await timedFetch(`${opencodeBase(port)}/session`);
3499
+ const res = await requestWithClient(port, client, "/session");
2928
3500
  if (!res.ok) return null;
2929
3501
  const body = await res.json();
2930
3502
  return Array.isArray(body) ? body : null;
2931
- } catch {
3503
+ } catch (error2) {
3504
+ console.error(
3505
+ `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3506
+ );
2932
3507
  return null;
2933
3508
  }
2934
3509
  }
2935
- async function deleteSession(port, id) {
3510
+ async function deleteSession(port, id, client) {
3511
+ if (client?.version === "v2") return deleteV2Session(client, id);
2936
3512
  try {
2937
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`, { method: "DELETE" });
3513
+ const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2938
3514
  return res.status >= 200 && res.status < 300;
2939
- } catch {
3515
+ } catch (error2) {
3516
+ console.error(
3517
+ `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3518
+ );
2940
3519
  return false;
2941
3520
  }
2942
3521
  }
2943
- async function sessionExists(port, id) {
3522
+ async function sessionExists(port, id, client) {
2944
3523
  try {
2945
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`);
3524
+ const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
3525
+ allowStatuses: [404]
3526
+ });
2946
3527
  if (res.status >= 200 && res.status < 300) return true;
2947
3528
  if (res.status === 404) return false;
2948
3529
  return null;
@@ -2950,9 +3531,22 @@ async function sessionExists(port, id) {
2950
3531
  return null;
2951
3532
  }
2952
3533
  }
2953
- async function getSessionStatuses(port) {
3534
+ async function getOpenCodeSession(port, id, client) {
3535
+ try {
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;
2954
3548
  try {
2955
- const res = await timedFetch(`${opencodeBase(port)}/session/status`);
3549
+ const res = await requestWithClient(port, client, "/session/status");
2956
3550
  if (!res.ok) {
2957
3551
  console.error(
2958
3552
  `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
@@ -2974,22 +3568,28 @@ async function getSessionStatuses(port) {
2974
3568
  return null;
2975
3569
  }
2976
3570
  }
2977
- async function isSessionOngoing(port, id) {
2978
- 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);
2979
3574
  if (map == null) return null;
2980
3575
  const entry = map[id];
2981
3576
  return entry != null && entry.type !== "idle";
2982
3577
  }
2983
- async function createOpenCodeSession(port, directory) {
2984
- const url = new URL(`${opencodeBase(port)}/session`);
2985
- if (directory && directory.trim()) {
2986
- url.searchParams.set("directory", directory.trim());
2987
- }
2988
- const response = await timedFetch(url, {
2989
- method: "POST",
2990
- headers: { "Content-Type": "application/json" },
2991
- body: JSON.stringify({})
2992
- });
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
+ );
2993
3593
  if (!response.ok) {
2994
3594
  const text = await response.text().catch(() => "");
2995
3595
  throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
@@ -2997,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
2997
3597
  const data = await response.json();
2998
3598
  return data.id;
2999
3599
  }
3000
- async function getModelAttachmentCapability(port, model) {
3600
+ async function getModelAttachmentCapability(port, model, client) {
3001
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
+ }
3002
3608
  try {
3003
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
3609
+ const res = await requestWithClient(port, client, "/config/providers");
3004
3610
  if (!res.ok) {
3005
3611
  console.error(
3006
3612
  `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3120,19 +3726,43 @@ function applyModelOptions(body, options) {
3120
3726
  }
3121
3727
  if (variant) body.variant = variant;
3122
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
+ }
3123
3753
  function messageText(m) {
3124
3754
  if (!m || !Array.isArray(m.parts)) return "";
3125
3755
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
3126
3756
  }
3127
- async function sendPromptAsync(port, sessionId, content, options, attachments) {
3128
- const before = await getSessionMessages(port, sessionId);
3757
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3758
+ const before = await getSessionMessages(port, sessionId, client);
3129
3759
  const knownUserIds = new Set(
3130
3760
  (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3131
3761
  );
3132
3762
  const parts = [{ type: "text", text: content }];
3133
3763
  let pendingOutcomes = null;
3134
3764
  if (attachments && attachments.inputs.length > 0) {
3135
- const capable = await getModelAttachmentCapability(port, options?.model);
3765
+ const capable = await getModelAttachmentCapability(port, options?.model, client);
3136
3766
  const {
3137
3767
  parts: fileParts,
3138
3768
  outcomes,
@@ -3145,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3145
3775
  parts
3146
3776
  };
3147
3777
  applyModelOptions(body, options);
3148
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
3149
- method: "POST",
3150
- headers: { "Content-Type": "application/json" },
3151
- body: JSON.stringify(body)
3152
- });
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
+ );
3153
3789
  if (res.status < 200 || res.status >= 300) {
3154
3790
  const text = await res.text().catch(() => "");
3155
3791
  const { variant } = splitModelVariant(options?.model);
@@ -3160,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3160
3796
  const READ_BACK_ATTEMPTS = 5;
3161
3797
  const READ_BACK_DELAY_MS = 150;
3162
3798
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
3163
- const after = await getSessionMessages(port, sessionId);
3799
+ const after = await getSessionMessages(port, sessionId, client);
3164
3800
  if (after) {
3165
3801
  let best = null;
3166
3802
  for (const m of after) {
@@ -3263,21 +3899,74 @@ function collectSubagentSessions(messages, userMessageId) {
3263
3899
  }
3264
3900
  return refs;
3265
3901
  }
3266
- function messageUsage(messages, userMessageId) {
3267
- if (!messages || messages.length === 0) return null;
3268
- const byParentAll = messages.filter(
3269
- (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)
3270
3941
  );
3271
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
3272
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
3273
- let correlated;
3274
- if (byParent.length > 0) {
3275
- correlated = byParent;
3276
- } else {
3277
- const reply = findAssistantReplyAfter(messages, userMessageId);
3278
- 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);
3279
3957
  }
3280
- 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;
3281
3970
  let sawAnyUsage = false;
3282
3971
  let inputSum = 0;
3283
3972
  let outputSum = 0;
@@ -3288,7 +3977,7 @@ function messageUsage(messages, userMessageId) {
3288
3977
  let sawCost = false;
3289
3978
  let modelId = null;
3290
3979
  let providerId = null;
3291
- for (const m of correlated) {
3980
+ for (const m of selected) {
3292
3981
  const info = m.info;
3293
3982
  if (!info) continue;
3294
3983
  const tokens = info.tokens;
@@ -3323,12 +4012,28 @@ function messageUsage(messages, userMessageId) {
3323
4012
  usage_tokens_reasoning: reasoningSum,
3324
4013
  usage_tokens_cache_read: cacheReadSum,
3325
4014
  usage_tokens_cache_write: cacheWriteSum,
3326
- // NULL means "OpenCode never reported a cost" (never inferred from
3327
- // tokens) distinct from a genuine 0-cost turn, which would set
3328
- // `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`.
3329
4017
  usage_cost_usd: sawCost ? costSum : null
3330
4018
  };
3331
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
+ }
3332
4037
  function messageRunState(messages, userMessageId) {
3333
4038
  if (!messages || messages.length === 0) return "unknown";
3334
4039
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -3458,9 +4163,73 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
3458
4163
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3459
4164
  );
3460
4165
  }
3461
- 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
+ }
3462
4231
  try {
3463
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
4232
+ const res = await requestWithClient(port, client, "/config/providers");
3464
4233
  if (!res.ok) {
3465
4234
  console.error(
3466
4235
  `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3489,9 +4258,84 @@ async function hasAnyConfiguredProvider(port) {
3489
4258
  return null;
3490
4259
  }
3491
4260
  }
3492
- async function reloadProviderCache(port) {
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;
3493
4337
  try {
3494
- const res = await timedFetch(`${opencodeBase(port)}/config`, {
4338
+ const res = await requestWithClient(port, client, "/config", {
3495
4339
  method: "PATCH",
3496
4340
  headers: { "Content-Type": "application/json" },
3497
4341
  body: JSON.stringify({})
@@ -3869,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
3869
4713
  "content-length"
3870
4714
  ]);
3871
4715
  var StreamForwarder = class {
3872
- constructor(ws, port, callbacks = {}) {
4716
+ constructor(ws, port, callbacks = {}, options = {}) {
3873
4717
  this.ws = ws;
3874
4718
  this.port = port;
3875
4719
  this.callbacks = callbacks;
4720
+ this.options = options;
3876
4721
  }
3877
4722
  inflight = /* @__PURE__ */ new Map();
3878
4723
  /**
@@ -3956,7 +4801,15 @@ var StreamForwarder = class {
3956
4801
  }
3957
4802
  const fwdHeaders = {};
3958
4803
  for (const [k, v] of Object.entries(headers ?? {})) {
3959
- 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);
3960
4813
  }
3961
4814
  this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
3962
4815
  const body = bodyPromise ? await bodyPromise : void 0;
@@ -4069,6 +4922,7 @@ function connectTunnel(options) {
4069
4922
  agentId,
4070
4923
  authHeader,
4071
4924
  port,
4925
+ openCodePassword,
4072
4926
  onConnected,
4073
4927
  onDisconnected,
4074
4928
  onError,
@@ -4086,11 +4940,16 @@ function connectTunnel(options) {
4086
4940
  Authorization: authHeader
4087
4941
  }
4088
4942
  });
4089
- const forwarder = new StreamForwarder(ws, port, {
4090
- onHead: () => onResponse?.(),
4091
- onDrainPing: () => onDrainPing?.(),
4092
- onUsageRearmPing: () => onUsageRearmPing?.()
4093
- });
4943
+ const forwarder = new StreamForwarder(
4944
+ ws,
4945
+ port,
4946
+ {
4947
+ onHead: () => onResponse?.(),
4948
+ onDrainPing: () => onDrainPing?.(),
4949
+ onUsageRearmPing: () => onUsageRearmPing?.()
4950
+ },
4951
+ { openCodePassword }
4952
+ );
4094
4953
  const connectionTimeout = setTimeout(() => {
4095
4954
  ws.close();
4096
4955
  reject(new Error("Connection timeout"));
@@ -4233,6 +5092,7 @@ var RunnerConnection = class {
4233
5092
  agentId: this.resolvedAgentId,
4234
5093
  authHeader: this.opts.getAuthHeader(),
4235
5094
  port: this.opts.port,
5095
+ openCodePassword: this.opts.openCodePassword,
4236
5096
  onConnected: (agentId) => {
4237
5097
  this.reconnectAttempt = 0;
4238
5098
  this.reconnecting = false;
@@ -4384,7 +5244,7 @@ function parseChatGptIdentity(accessToken) {
4384
5244
  const auth = payload["https://api.openai.com/auth"];
4385
5245
  const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4386
5246
  const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4387
- return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
5247
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
4388
5248
  }
4389
5249
  function toWindow2(headers, name) {
4390
5250
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
@@ -4413,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
4413
5273
  function normalizeProbeModel(model) {
4414
5274
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4415
5275
  }
4416
- 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) {
4417
5284
  try {
4418
- const res = await withRequestTimeout(
4419
- fetch,
4420
- REQUEST_TIMEOUT_MS
4421
- )(`${opencodeBase(port)}/config/providers`);
4422
- if (!res.ok) {
4423
- console.error(
4424
- `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
4425
- );
4426
- 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);
4427
5289
  }
4428
- const body = await res.json();
4429
- const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
4430
- 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;
4431
5295
  const candidates = [
4432
- ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
5296
+ ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4433
5297
  ...Object.keys(provider.models)
4434
5298
  ].map(normalizeProbeModel);
4435
- return [...new Set(candidates)].slice(0, 4);
5299
+ return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
4436
5300
  } catch (err) {
4437
- console.error(
4438
- `[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
4439
5304
  );
4440
- return [];
4441
5305
  }
4442
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
5337
+ );
5338
+ }
5339
+ }
5340
+ async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
5341
+ return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
5342
+ }
4443
5343
  function hasPrimaryHeaders(headers) {
4444
5344
  return [
4445
5345
  "x-codex-primary-used-percent",
@@ -4447,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
4447
5347
  "x-codex-primary-reset-at"
4448
5348
  ].some((name) => headers.has(name));
4449
5349
  }
4450
- async function getOpenAiUsage(port) {
5350
+ async function getOpenAiUsage(port, client) {
4451
5351
  const credentials2 = readOpenCodeChatGptCredentials();
4452
5352
  if (!credentials2) {
4453
5353
  throw new OpenAiUsageError(
@@ -4462,12 +5362,16 @@ async function getOpenAiUsage(port) {
4462
5362
  );
4463
5363
  }
4464
5364
  const subscription = parseChatGptIdentity(credentials2.accessToken);
4465
- const models = await resolveProbeModels(port);
4466
- if (models.length === 0) {
4467
- 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
+ );
4468
5372
  }
4469
5373
  let lastStatus;
4470
- for (const model of models) {
5374
+ for (const model of lookup.models) {
4471
5375
  let res;
4472
5376
  try {
4473
5377
  res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
@@ -4566,13 +5470,6 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
4566
5470
  envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
4567
5471
  });
4568
5472
  }
4569
- function nextReportDelayMs(random = Math.random) {
4570
- return usageReportDelayMs(random);
4571
- }
4572
- var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
4573
- function claudeUsageFailureLogLevel(consecutiveFailures) {
4574
- return usageReportFailureLogLevel(consecutiveFailures);
4575
- }
4576
5473
 
4577
5474
  // src/lib/openai-usage-reporting.ts
4578
5475
  function resolveOpenAiUsageReportingMode(flagValue, env) {
@@ -5228,6 +6125,10 @@ var DEFAULT_RETRY_POLICY = {
5228
6125
  baseDelayMs: 500,
5229
6126
  maxDelayMs: 3e4
5230
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;
5231
6132
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
5232
6133
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
5233
6134
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
@@ -5289,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
5289
6190
  maxActiveSessions;
5290
6191
  watcherStallMs;
5291
6192
  wedgeWarningIntervalMs;
6193
+ openCodeClient;
5292
6194
  /** Cache of conversationId → opencode sessionId. */
5293
6195
  sessions = /* @__PURE__ */ new Map();
5294
6196
  /**
@@ -5368,6 +6270,17 @@ var ChannelDriver = class _ChannelDriver {
5368
6270
  * message; it is removed once its in-flight set empties.
5369
6271
  */
5370
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();
5371
6284
  /**
5372
6285
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
5373
6286
  * dispatched and are still in-flight. A message in this set is never
@@ -5386,20 +6299,23 @@ var ChannelDriver = class _ChannelDriver {
5386
6299
  */
5387
6300
  readopted = /* @__PURE__ */ new Set();
5388
6301
  /**
5389
- * "Don't re-DISPATCH / re-attach this orphan again" (Bug 2/5). Set when a
5390
- * re-adopted running/orphan row's watcher hit its `processed_at`-anchored
5391
- * deadline (or an orphan whose window already elapsed): the still-`processing`
5392
- * server row would otherwise be re-adopted (and re-dispatched) on EVERY ~2s
5393
- * drain until the 15-min cron resets it spamming new turns.
5394
- *
5395
- * CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
5396
- * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
5397
- * in opencode must still be delivered via `markDone` on the next drain — so
5398
- * `readoptOne` computes `state` FIRST and this set is checked only on the
5399
- * non-done path. It is cleared once the row leaves the processing list (cron
5400
- * reset → it drains normally as `pending`), so it can never leak.
6302
+ * Readopt give-up fence. Set when recovery declines to start or continue a turn
6303
+ * for a row that is still `processing`, so the next drain does not re-dispatch or
6304
+ * re-attach it before the cron safety net acts. It suppresses only non-done
6305
+ * recovery paths; DONE delivery still runs. Clear it when
6306
+ * `!stillProcessing.has(id)`, because leaving `processing` hands the row back to
6307
+ * normal processing.
5401
6308
  */
5402
6309
  dontRedispatch = /* @__PURE__ */ new Set();
6310
+ /**
6311
+ * Untrackable-ack fence. Set after OpenCode accepts a prompt without returning a
6312
+ * usable message id, because another POST could create a duplicate turn. Keep it
6313
+ * fenced while the row is `processing` or `pending`; clear it only when the row
6314
+ * is absent from both lists.
6315
+ */
6316
+ untrackableAck = /* @__PURE__ */ new Set();
6317
+ /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6318
+ pendingMessageIds = /* @__PURE__ */ new Set();
5403
6319
  /**
5404
6320
  * "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
5405
6321
  * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
@@ -5522,7 +6438,8 @@ var ChannelDriver = class _ChannelDriver {
5522
6438
  */
5523
6439
  attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
5524
6440
  /**
5525
- * Cache of the opencode root directory (from `GET /path`). Resolved lazily on
6441
+ * Cache of the opencode root directory from the selected client's location lookup.
6442
+ * Resolved lazily on
5526
6443
  * first session creation so drain-created sessions are rooted at the project
5527
6444
  * directory and thus visible in `opencode web`'s session list. `undefined` =
5528
6445
  * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
@@ -5551,6 +6468,13 @@ var ChannelDriver = class _ChannelDriver {
5551
6468
  * no watcher) can resolve the title.
5552
6469
  */
5553
6470
  sessionTitles = /* @__PURE__ */ new Map();
6471
+ /** One best-effort terminal subagent collection per Evident message id. */
6472
+ subagentInvocationCollections = /* @__PURE__ */ new Map();
6473
+ /**
6474
+ * Early snapshots are only liveness hints; they must not become the terminal
6475
+ * collection when the task parts or child transcript have advanced.
6476
+ */
6477
+ subagentInvocationPrefetches = /* @__PURE__ */ new Map();
5554
6478
  /** Serialises drains so a reconnect during a drain doesn't double-process. */
5555
6479
  draining = false;
5556
6480
  /**
@@ -5609,6 +6533,11 @@ var ChannelDriver = class _ChannelDriver {
5609
6533
  config.fetchImpl ?? fetch,
5610
6534
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
5611
6535
  );
6536
+ this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
6537
+ port: config.port,
6538
+ version: "v1",
6539
+ fetchImpl: config.fetchImpl
6540
+ });
5612
6541
  this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5613
6542
  this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
5614
6543
  this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
@@ -5620,9 +6549,39 @@ var ChannelDriver = class _ChannelDriver {
5620
6549
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5621
6550
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
5622
6551
  }
5623
- /** The IPv4-loopback base URL for the local `opencode serve`. */
5624
- get opencodeBase() {
5625
- return `http://127.0.0.1:${this.port}`;
6552
+ get isV2() {
6553
+ return this.openCodeClient.version === "v2";
6554
+ }
6555
+ async getSessionMessages(sessionId) {
6556
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6557
+ }
6558
+ async getSubagentSessionMessages(sessionId) {
6559
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
6560
+ }
6561
+ async getTelemetrySubagentSessionMessages(sessionId) {
6562
+ if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
6563
+ return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6564
+ }
6565
+ async listSessions() {
6566
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
6567
+ }
6568
+ async sessionExists(sessionId) {
6569
+ return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
6570
+ }
6571
+ async isSessionOngoing(sessionId) {
6572
+ return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
6573
+ }
6574
+ async getOpenCodeDirectory() {
6575
+ return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
6576
+ }
6577
+ async createOpenCodeSession(directory) {
6578
+ return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
6579
+ }
6580
+ async hasAnyConfiguredProvider() {
6581
+ return hasAnyConfiguredProvider(this.port, this.openCodeClient);
6582
+ }
6583
+ async readOpenCodeSessionErrorStream(options) {
6584
+ return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
5626
6585
  }
5627
6586
  /**
5628
6587
  * Drain all pending channel conversations once: poll → dispatch → register.
@@ -5700,6 +6659,7 @@ var ChannelDriver = class _ChannelDriver {
5700
6659
  async runDrain() {
5701
6660
  let dispatched = 0;
5702
6661
  try {
6662
+ this.pendingMessageIds.clear();
5703
6663
  const conversations = await this.getPendingConversations();
5704
6664
  if (this.recycleRequestedFlag) {
5705
6665
  this.stop();
@@ -5765,6 +6725,21 @@ var ChannelDriver = class _ChannelDriver {
5765
6725
  }
5766
6726
  return ids;
5767
6727
  }
6728
+ /**
6729
+ * OpenCode user-message ids tracked for other Evident messages in a session.
6730
+ * Excluding this message makes an unattributed later row fail safe; a missing
6731
+ * watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
6732
+ */
6733
+ siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
6734
+ const ids = /* @__PURE__ */ new Set();
6735
+ if (!watcher) return ids;
6736
+ for (const inFlight of watcher.inFlight.values()) {
6737
+ if (inFlight.evidentMessageId !== ownEvidentMessageId) {
6738
+ ids.add(inFlight.opencodeMessageId);
6739
+ }
6740
+ }
6741
+ return ids;
6742
+ }
5768
6743
  /**
5769
6744
  * File-pull work, for `run.ts`'s idle accounting (#559).
5770
6745
  *
@@ -5831,6 +6806,8 @@ var ChannelDriver = class _ChannelDriver {
5831
6806
  */
5832
6807
  stop() {
5833
6808
  this.stopped = true;
6809
+ this.sessionErrorStream?.abort.abort();
6810
+ this.sessionErrorStream = null;
5834
6811
  }
5835
6812
  /**
5836
6813
  * The server clears this request when a new MicroVM identity is recorded, so a
@@ -5905,7 +6882,9 @@ var ChannelDriver = class _ChannelDriver {
5905
6882
  */
5906
6883
  async processConversation(conv) {
5907
6884
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6885
+ this.ensureSessionErrorStream();
5908
6886
  const messages = await this.getPendingMessages(conv.id);
6887
+ for (const message of messages) this.pendingMessageIds.add(message.id);
5909
6888
  let dispatched = 0;
5910
6889
  let skippedAlreadyDispatched = 0;
5911
6890
  if (refusedSessionId && messages.length > 0) {
@@ -5919,6 +6898,15 @@ var ChannelDriver = class _ChannelDriver {
5919
6898
  skippedAlreadyDispatched += 1;
5920
6899
  continue;
5921
6900
  }
6901
+ if (this.untrackableAck.has(message.id)) {
6902
+ this.log({
6903
+ level: "warn",
6904
+ message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
6905
+ conversation_id: conv.id,
6906
+ message_id: message.id
6907
+ });
6908
+ break;
6909
+ }
5922
6910
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
5923
6911
  if (effectiveOpencodeMessageId) {
5924
6912
  const outcome = await this.resolveRedrive(
@@ -5947,15 +6935,55 @@ var ChannelDriver = class _ChannelDriver {
5947
6935
  conversation_id: conv.id,
5948
6936
  message_id: message.id
5949
6937
  });
5950
- const sendAttachments = this.buildSendAttachments(conv, message);
5951
- opencodeMessageId = await this.dispatchLocked(
6938
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
6939
+ if (this.isV2 && message.attachments && message.attachments.length > 0) {
6940
+ this.signalAttachmentsSkipped(
6941
+ conv.id,
6942
+ message.id,
6943
+ message.attachments.map((attachment, index) => ({
6944
+ index,
6945
+ mime: attachment.mime,
6946
+ ...attachment.filename ? { filename: attachment.filename } : {},
6947
+ status: "skipped"
6948
+ })),
6949
+ false
6950
+ );
6951
+ }
6952
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
5952
6953
  sessionId,
5953
- () => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
6954
+ () => sendPromptAsync(
6955
+ this.port,
6956
+ sessionId,
6957
+ message.content,
6958
+ options,
6959
+ sendAttachments,
6960
+ this.openCodeClient
6961
+ )
5954
6962
  );
5955
6963
  } catch (err) {
5956
6964
  if (err instanceof ChannelAuthError) throw err;
6965
+ if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
6966
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
6967
+ this.untrackableAck.add(message.id);
6968
+ this.log({
6969
+ level: "error",
6970
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
6971
+ conversation_id: conv.id,
6972
+ message_id: message.id
6973
+ });
6974
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
6975
+ this.log({
6976
+ level: "warn",
6977
+ message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
6978
+ conversation_id: conv.id,
6979
+ message_id: message.id
6980
+ });
6981
+ void this.postSignal(conv.id, message.id, "ack_untrackable");
6982
+ });
6983
+ break;
6984
+ }
5957
6985
  this.dispatched.delete(message.id);
5958
- const exists = await sessionExists(this.port, sessionId);
6986
+ const exists = await this.sessionExists(sessionId);
5959
6987
  if (exists === false) {
5960
6988
  this.sessions.delete(conv.id);
5961
6989
  this.log({
@@ -6004,6 +7032,9 @@ var ChannelDriver = class _ChannelDriver {
6004
7032
  break;
6005
7033
  }
6006
7034
  if (opencodeMessageId === null) {
7035
+ if (this.isV2) {
7036
+ throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7037
+ }
6007
7038
  const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
6008
7039
  if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
6009
7040
  this.log({
@@ -6151,29 +7182,38 @@ var ChannelDriver = class _ChannelDriver {
6151
7182
  */
6152
7183
  async pollSessionMessagesForRedrive(conv, message, sessionId) {
6153
7184
  try {
6154
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
6155
- if (!res.ok) {
6156
- const rawBody = await res.text();
6157
- const normalized = normalizeRedrivePollFailureBody(rawBody);
6158
- this.log({
6159
- level: "warn",
6160
- 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`,
6161
- conversation_id: conv.id,
6162
- message_id: message.id
6163
- });
6164
- return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ""}` };
7185
+ if (this.isV2) {
7186
+ const messages = await this.getSessionMessages(sessionId);
7187
+ if (messages === null) {
7188
+ this.log({
7189
+ level: "warn",
7190
+ 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`,
7191
+ conversation_id: conv.id,
7192
+ message_id: message.id
7193
+ });
7194
+ return { ok: false, signature: null };
7195
+ }
7196
+ return { ok: true, messages };
6165
7197
  }
6166
- const body = await res.json();
6167
- if (!Array.isArray(body)) {
7198
+ const polledV1 = await pollSessionMessagesForRedrive(
7199
+ this.port,
7200
+ sessionId,
7201
+ this.openCodeClient
7202
+ );
7203
+ if (!polledV1.ok) {
7204
+ const normalized = normalizeRedrivePollFailureBody(polledV1.body);
6168
7205
  this.log({
6169
7206
  level: "warn",
6170
- 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`,
7207
+ 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`,
6171
7208
  conversation_id: conv.id,
6172
7209
  message_id: message.id
6173
7210
  });
6174
- return { ok: false, signature: "non-array message body" };
7211
+ return {
7212
+ ok: false,
7213
+ signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
7214
+ };
6175
7215
  }
6176
- return { ok: true, messages: body };
7216
+ return { ok: true, messages: polledV1.messages };
6177
7217
  } catch (err) {
6178
7218
  this.log({
6179
7219
  level: "warn",
@@ -6232,11 +7272,11 @@ var ChannelDriver = class _ChannelDriver {
6232
7272
  }
6233
7273
  const state = messageRunState(messages, ocId ?? "");
6234
7274
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
6235
- const ongoing = await isSessionOngoing(this.port, sessionId);
7275
+ const ongoing = await this.isSessionOngoing(sessionId);
6236
7276
  if (ongoing === false) {
6237
7277
  this.log({
6238
7278
  level: "info",
6239
- 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`,
7279
+ 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`,
6240
7280
  conversation_id: conv.id,
6241
7281
  message_id: message.id
6242
7282
  });
@@ -6249,8 +7289,25 @@ var ChannelDriver = class _ChannelDriver {
6249
7289
  return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
6250
7290
  }
6251
7291
  if (state === "running" || state === "queued") {
6252
- const ongoing = await isSessionOngoing(this.port, sessionId);
7292
+ const ongoing = await this.isSessionOngoing(sessionId);
6253
7293
  if (ongoing === true) {
7294
+ if (state === "queued") {
7295
+ const siblingOcIds = this.siblingOpencodeMessageIds(
7296
+ this.watchers.get(sessionId),
7297
+ message.id
7298
+ );
7299
+ if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
7300
+ this.log({
7301
+ level: "warn",
7302
+ 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`,
7303
+ conversation_id: conv.id,
7304
+ message_id: message.id
7305
+ });
7306
+ this.clearRedriveUnresolved(message.id);
7307
+ void this.postSignal(conv.id, message.id, "redrive_redispatched");
7308
+ return "dispatch";
7309
+ }
7310
+ }
6254
7311
  return this.reattachRedrive(conv, sessionId, message, ocId);
6255
7312
  }
6256
7313
  if (ongoing === false) {
@@ -6340,16 +7397,37 @@ var ChannelDriver = class _ChannelDriver {
6340
7397
  if (state === "done") {
6341
7398
  const title = await this.resolveSessionTitle(sessionId, conv.id);
6342
7399
  const usage = messageUsage(messages, ocId ?? "");
7400
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
7401
+ const subagentInvocations = await this.resolveSubagentInvocations(
7402
+ messages,
7403
+ ocId ?? "",
7404
+ message.id
7405
+ );
6343
7406
  this.log({
6344
7407
  level: "info",
6345
7408
  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`,
6346
7409
  conversation_id: conv.id,
6347
7410
  message_id: message.id
6348
7411
  });
6349
- await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);
7412
+ await this.markDone(
7413
+ conv.id,
7414
+ message.id,
7415
+ sessionId,
7416
+ ocId,
7417
+ title,
7418
+ usage,
7419
+ usageAgentName,
7420
+ subagentInvocations
7421
+ );
6350
7422
  } else {
6351
7423
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
6352
7424
  const usage = messageUsage(messages, ocId ?? "");
7425
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
7426
+ const subagentInvocations = await this.resolveSubagentInvocations(
7427
+ messages,
7428
+ ocId ?? "",
7429
+ message.id
7430
+ );
6353
7431
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
6354
7432
  this.log({
6355
7433
  level: "error",
@@ -6357,7 +7435,16 @@ var ChannelDriver = class _ChannelDriver {
6357
7435
  conversation_id: conv.id,
6358
7436
  message_id: message.id
6359
7437
  });
6360
- await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
7438
+ await this.markFailed(
7439
+ conv.id,
7440
+ message.id,
7441
+ sessionId,
7442
+ error2,
7443
+ usage,
7444
+ failure,
7445
+ usageAgentName,
7446
+ subagentInvocations
7447
+ );
6361
7448
  }
6362
7449
  if (ocId !== null) {
6363
7450
  await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
@@ -6662,7 +7749,7 @@ var ChannelDriver = class _ChannelDriver {
6662
7749
  };
6663
7750
  }
6664
7751
  if (bound) {
6665
- const exists = await sessionExists(this.port, bound);
7752
+ const exists = await this.sessionExists(bound);
6666
7753
  if (exists === false) {
6667
7754
  this.log({
6668
7755
  level: "debug",
@@ -6695,7 +7782,7 @@ var ChannelDriver = class _ChannelDriver {
6695
7782
  */
6696
7783
  async createAndBindSession(conversationId) {
6697
7784
  const directory = await this.resolveOpenCodeDirectory();
6698
- const sessionId = await createOpenCodeSession(this.port, directory);
7785
+ const sessionId = await this.createOpenCodeSession(directory);
6699
7786
  this.sessions.set(conversationId, sessionId);
6700
7787
  await this.persistSession(conversationId, sessionId).catch((err) => {
6701
7788
  this.log({
@@ -6707,17 +7794,18 @@ var ChannelDriver = class _ChannelDriver {
6707
7794
  return sessionId;
6708
7795
  }
6709
7796
  /**
6710
- * Lazily resolve (and cache) opencode's root directory via `GET /path`.
7797
+ * Lazily resolve (and cache) opencode's root directory via the selected client's
7798
+ * location lookup.
6711
7799
  * Resolved once per driver: `undefined` until first lookup, then the directory
6712
- * string or `null` if unavailable (we don't keep retrying a missing `/path`).
7800
+ * string or `null` if unavailable (we don't keep retrying a failed lookup).
6713
7801
  */
6714
7802
  async resolveOpenCodeDirectory() {
6715
7803
  if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
6716
- this.opencodeDirectory = await getOpenCodeDirectory(this.port);
7804
+ this.opencodeDirectory = await this.getOpenCodeDirectory();
6717
7805
  if (!this.opencodeDirectory) {
6718
7806
  this.log({
6719
7807
  level: "warn",
6720
- message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
7808
+ message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
6721
7809
  });
6722
7810
  }
6723
7811
  return this.opencodeDirectory;
@@ -6901,6 +7989,24 @@ var ChannelDriver = class _ChannelDriver {
6901
7989
  ambiguousPinnedSinceMs: 0,
6902
7990
  ambiguousResolved: false
6903
7991
  });
7992
+ const buffered = this.bufferedSessionErrors.get(sessionId);
7993
+ if (!buffered) return;
7994
+ this.bufferedSessionErrors.delete(sessionId);
7995
+ if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
7996
+ this.handleSessionError(buffered.event);
7997
+ }
7998
+ }
7999
+ bufferSessionError(event) {
8000
+ this.bufferedSessionErrors.delete(event.sessionId);
8001
+ this.bufferedSessionErrors.set(event.sessionId, {
8002
+ event,
8003
+ receivedAt: this.now()
8004
+ });
8005
+ while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
8006
+ const oldest = this.bufferedSessionErrors.keys().next().value;
8007
+ if (typeof oldest !== "string") break;
8008
+ this.bufferedSessionErrors.delete(oldest);
8009
+ }
6904
8010
  }
6905
8011
  /**
6906
8012
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
@@ -7079,47 +8185,196 @@ var ChannelDriver = class _ChannelDriver {
7079
8185
  this.ensureWatcherRunning(sessionId);
7080
8186
  this.log({
7081
8187
  level: "warn",
7082
- message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had not ticked in ${stalledForMs}ms \u2014 restarted under generation ${watcher.generation} (${watcher.consecutiveStallRestarts}/${MAX_WATCHER_STALL_RESTARTS})`,
7083
- conversation_id: watcher.conv.id
8188
+ message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had not ticked in ${stalledForMs}ms \u2014 restarted under generation ${watcher.generation} (${watcher.consecutiveStallRestarts}/${MAX_WATCHER_STALL_RESTARTS})`,
8189
+ conversation_id: watcher.conv.id
8190
+ });
8191
+ for (const evidentMessageId of watcher.inFlight.keys()) {
8192
+ void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
8193
+ recovery: "loop_stalled"
8194
+ });
8195
+ }
8196
+ }
8197
+ }
8198
+ }
8199
+ /**
8200
+ * Start (but do NOT await) the per-session watcher loop if it has in-flight
8201
+ * work and is not already running. Single-flight per session. The loop is
8202
+ * tracked on the watcher and cleared when it settles; it never rejects (fully
8203
+ * guarded), so a failed poll/callback can never crash the run loop — the cron
8204
+ * stays as the safety net.
8205
+ *
8206
+ * The generation started here (#1618) is captured in the `.finally` closure
8207
+ * so a RETIRED loop settling late — after `reconcileWatchers` has already
8208
+ * restarted this watcher under a newer generation — can neither null the new
8209
+ * loop's handle nor delete a watcher that still has live work.
8210
+ */
8211
+ ensureWatcherRunning(sessionId) {
8212
+ const watcher = this.watchers.get(sessionId);
8213
+ if (!watcher) return;
8214
+ this.ensureSessionErrorStream();
8215
+ if (watcher.loop) return;
8216
+ if (watcher.inFlight.size === 0) {
8217
+ this.watchers.delete(sessionId);
8218
+ return;
8219
+ }
8220
+ const generation = watcher.generation;
8221
+ const loop = this.runWatcherLoop(sessionId, watcher, generation).finally(() => {
8222
+ if (watcher.generation !== generation) return;
8223
+ watcher.loop = null;
8224
+ if (watcher.inFlight.size === 0) {
8225
+ this.watchers.delete(sessionId);
8226
+ }
8227
+ });
8228
+ watcher.loop = loop;
8229
+ }
8230
+ ensureSessionErrorStream() {
8231
+ if (this.sessionErrorStream || this.stopped) return;
8232
+ const abort = new AbortController();
8233
+ const loop = this.runSessionErrorStream(abort.signal);
8234
+ this.sessionErrorStream = { abort, loop };
8235
+ }
8236
+ async runSessionErrorStream(signal) {
8237
+ let attempt = 0;
8238
+ let warned = false;
8239
+ while (!this.stopped && !signal.aborted) {
8240
+ const openedAt = this.now();
8241
+ try {
8242
+ const outcome = await this.readOpenCodeSessionErrorStream({
8243
+ signal,
8244
+ onSessionError: (event) => this.handleSessionError(event)
8245
+ });
8246
+ if (outcome.reason === "aborted" || signal.aborted) return;
8247
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
8248
+ if (outcome.reason === "unavailable" || outcome.reason === "ended") {
8249
+ if (!healthy) {
8250
+ const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
8251
+ this.log({
8252
+ level: warned ? "debug" : "warn",
8253
+ message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
8254
+ });
8255
+ warned = true;
8256
+ }
8257
+ }
8258
+ if (healthy) {
8259
+ if (warned) {
8260
+ this.log({
8261
+ level: "info",
8262
+ message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
8263
+ });
8264
+ warned = false;
8265
+ }
8266
+ attempt = 0;
8267
+ } else {
8268
+ attempt += 1;
8269
+ }
8270
+ if (this.stopped || signal.aborted) return;
8271
+ await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
8272
+ } catch (err) {
8273
+ if (this.stopped || signal.aborted) return;
8274
+ this.log({
8275
+ level: "error",
8276
+ message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
8277
+ });
8278
+ const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
8279
+ const delayAttempt = healthy ? 0 : attempt;
8280
+ attempt = healthy ? 0 : attempt + 1;
8281
+ try {
8282
+ await this.sleep(backoffDelay(delayAttempt, this.retry));
8283
+ } catch (sleepErr) {
8284
+ this.log({
8285
+ level: "error",
8286
+ message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
8287
+ });
8288
+ }
8289
+ }
8290
+ }
8291
+ }
8292
+ handleSessionError(event) {
8293
+ try {
8294
+ const watcher = this.watchers.get(event.sessionId);
8295
+ if (!watcher) {
8296
+ this.bufferSessionError(event);
8297
+ this.log({
8298
+ level: "debug",
8299
+ message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
8300
+ });
8301
+ return;
8302
+ }
8303
+ if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
8304
+ this.log({
8305
+ level: "debug",
8306
+ message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
8307
+ conversation_id: watcher.conv.id
8308
+ });
8309
+ return;
8310
+ }
8311
+ const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
8312
+ if (!inFlight) {
8313
+ this.bufferSessionError(event);
8314
+ this.log({
8315
+ level: "debug",
8316
+ message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
8317
+ conversation_id: watcher.conv.id
8318
+ });
8319
+ return;
8320
+ }
8321
+ if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
8322
+ this.sessionErrorHandled.add(inFlight.evidentMessageId);
8323
+ void this.failFromSessionError(watcher, event, inFlight);
8324
+ } catch (err) {
8325
+ this.log({
8326
+ level: "error",
8327
+ message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
8328
+ });
8329
+ }
8330
+ }
8331
+ async failFromSessionError(watcher, event, inFlight) {
8332
+ try {
8333
+ const messages = await this.getSessionMessages(event.sessionId);
8334
+ const state = messageRunState(messages, inFlight.opencodeMessageId);
8335
+ if (state !== "queued") {
8336
+ this.log({
8337
+ level: "debug",
8338
+ message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
8339
+ conversation_id: watcher.conv.id,
8340
+ message_id: inFlight.evidentMessageId
8341
+ });
8342
+ return;
8343
+ }
8344
+ this.log({
8345
+ level: "error",
8346
+ message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
8347
+ conversation_id: watcher.conv.id,
8348
+ message_id: inFlight.evidentMessageId
8349
+ });
8350
+ await this.markFailed(
8351
+ watcher.conv.id,
8352
+ inFlight.evidentMessageId,
8353
+ event.sessionId,
8354
+ `OpenCode could not run this turn: ${event.reason}`
8355
+ );
8356
+ inFlight.done = true;
8357
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
8358
+ } catch (err) {
8359
+ if (err instanceof ChannelAuthError) {
8360
+ this.log({
8361
+ level: "warn",
8362
+ 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`,
8363
+ conversation_id: watcher.conv.id,
8364
+ message_id: inFlight.evidentMessageId
8365
+ });
8366
+ } else {
8367
+ this.log({
8368
+ level: "warn",
8369
+ 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`,
8370
+ conversation_id: watcher.conv.id,
8371
+ message_id: inFlight.evidentMessageId
7084
8372
  });
7085
- for (const evidentMessageId of watcher.inFlight.keys()) {
7086
- void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
7087
- recovery: "loop_stalled"
7088
- });
7089
- }
7090
8373
  }
8374
+ } finally {
8375
+ this.sessionErrorHandled.delete(inFlight.evidentMessageId);
7091
8376
  }
7092
8377
  }
7093
- /**
7094
- * Start (but do NOT await) the per-session watcher loop if it has in-flight
7095
- * work and is not already running. Single-flight per session. The loop is
7096
- * tracked on the watcher and cleared when it settles; it never rejects (fully
7097
- * guarded), so a failed poll/callback can never crash the run loop — the cron
7098
- * stays as the safety net.
7099
- *
7100
- * The generation started here (#1618) is captured in the `.finally` closure
7101
- * so a RETIRED loop settling late — after `reconcileWatchers` has already
7102
- * restarted this watcher under a newer generation — can neither null the new
7103
- * loop's handle nor delete a watcher that still has live work.
7104
- */
7105
- ensureWatcherRunning(sessionId) {
7106
- const watcher = this.watchers.get(sessionId);
7107
- if (!watcher) return;
7108
- if (watcher.loop) return;
7109
- if (watcher.inFlight.size === 0) {
7110
- this.watchers.delete(sessionId);
7111
- return;
7112
- }
7113
- const generation = watcher.generation;
7114
- const loop = this.runWatcherLoop(sessionId, watcher, generation).finally(() => {
7115
- if (watcher.generation !== generation) return;
7116
- watcher.loop = null;
7117
- if (watcher.inFlight.size === 0) {
7118
- this.watchers.delete(sessionId);
7119
- }
7120
- });
7121
- watcher.loop = loop;
7122
- }
7123
8378
  /**
7124
8379
  * The per-session polling loop (WI-3). Once per tick it:
7125
8380
  * 1. polls `GET /session/:id/message` once and, per in-flight message,
@@ -7127,7 +8382,8 @@ var ChannelDriver = class _ChannelDriver {
7127
8382
  * markDone (done) exactly once per transition;
7128
8383
  * 2. applies the idle-path re-dispatch guard (a dispatched message that never
7129
8384
  * APPEARS → re-dispatch — D1 obligation 2);
7130
- * 3. polls `/question` + `/permission` (scoped to the session) and surfaces
8385
+ * 3. polls V1's global `/question` + `/permission`, or V2's
8386
+ * `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
7131
8387
  * NEW ones via `reportInteraction`, carrying the PAUSED message's own
7132
8388
  * `source_message_id`;
7133
8389
  * 4. drops messages that completed or timed out from the in-flight set.
@@ -7153,11 +8409,7 @@ var ChannelDriver = class _ChannelDriver {
7153
8409
  if (watcher.generation !== generation) return;
7154
8410
  let messages = null;
7155
8411
  try {
7156
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
7157
- if (res.ok) {
7158
- const body = await res.json();
7159
- messages = Array.isArray(body) ? body : null;
7160
- }
8412
+ messages = await this.getSessionMessages(sessionId);
7161
8413
  } catch {
7162
8414
  }
7163
8415
  if (messages != null && messages.length > 0) {
@@ -7232,6 +8484,21 @@ var ChannelDriver = class _ChannelDriver {
7232
8484
  const conv = watcher.conv;
7233
8485
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7234
8486
  const id = inFlight.evidentMessageId;
8487
+ if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
8488
+ void this.resolveSubagentInvocations(
8489
+ messages,
8490
+ inFlight.opencodeMessageId,
8491
+ id,
8492
+ "prefetch"
8493
+ ).catch((err) => {
8494
+ this.log({
8495
+ level: "warn",
8496
+ message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
8497
+ conversation_id: conv.id,
8498
+ message_id: id
8499
+ });
8500
+ });
8501
+ }
7235
8502
  if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
7236
8503
  else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
7237
8504
  if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
@@ -7285,6 +8552,12 @@ var ChannelDriver = class _ChannelDriver {
7285
8552
  message_id: inFlight.evidentMessageId
7286
8553
  });
7287
8554
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
8555
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
8556
+ const subagentInvocations = await this.resolveSubagentInvocations(
8557
+ messages,
8558
+ inFlight.opencodeMessageId,
8559
+ inFlight.evidentMessageId
8560
+ );
7288
8561
  const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
7289
8562
  try {
7290
8563
  await this.markFailed(
@@ -7293,7 +8566,9 @@ var ChannelDriver = class _ChannelDriver {
7293
8566
  sessionId,
7294
8567
  error2,
7295
8568
  usage,
7296
- failure
8569
+ failure,
8570
+ usageAgentName,
8571
+ subagentInvocations
7297
8572
  );
7298
8573
  } catch (err) {
7299
8574
  if (err instanceof ChannelAuthError) throw err;
@@ -7336,9 +8611,11 @@ var ChannelDriver = class _ChannelDriver {
7336
8611
  this.removeInFlight(watcher, inFlight.evidentMessageId);
7337
8612
  return;
7338
8613
  }
8614
+ const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
8615
+ const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
7339
8616
  const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
7340
8617
  const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
7341
- if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
8618
+ if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
7342
8619
  inFlight.stuckReported = true;
7343
8620
  void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
7344
8621
  stuck_for_ms: this.now() - inFlight.dispatchedAt
@@ -7365,7 +8642,7 @@ var ChannelDriver = class _ChannelDriver {
7365
8642
  inFlight.b2LastDescendantCheckMs = this.now();
7366
8643
  const [descendantOngoing, rootOngoing] = await Promise.all([
7367
8644
  this.isAnyDescendantSessionOngoing(sessionId),
7368
- isSessionOngoing(this.port, sessionId)
8645
+ this.isSessionOngoing(sessionId)
7369
8646
  ]);
7370
8647
  if (isB2AbandonmentConfirmed({
7371
8648
  pinnedForMs,
@@ -7425,7 +8702,7 @@ var ChannelDriver = class _ChannelDriver {
7425
8702
  });
7426
8703
  }
7427
8704
  const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
7428
- const ongoing = await isSessionOngoing(this.port, sessionId);
8705
+ const ongoing = await this.isSessionOngoing(sessionId);
7429
8706
  if (isAmbiguousFinishResolved({
7430
8707
  pinnedForMs,
7431
8708
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
@@ -7499,7 +8776,7 @@ var ChannelDriver = class _ChannelDriver {
7499
8776
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
7500
8777
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
7501
8778
  );
7502
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
8779
+ const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
7503
8780
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
7504
8781
  this.log({
7505
8782
  level: "debug",
@@ -7534,6 +8811,12 @@ var ChannelDriver = class _ChannelDriver {
7534
8811
  });
7535
8812
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
7536
8813
  const usage = messageUsage(messages, inFlight.opencodeMessageId);
8814
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
8815
+ const subagentInvocations = await this.resolveSubagentInvocations(
8816
+ messages,
8817
+ inFlight.opencodeMessageId,
8818
+ inFlight.evidentMessageId
8819
+ );
7537
8820
  try {
7538
8821
  await this.markDone(
7539
8822
  conv.id,
@@ -7541,7 +8824,9 @@ var ChannelDriver = class _ChannelDriver {
7541
8824
  sessionId,
7542
8825
  inFlight.opencodeMessageId,
7543
8826
  title,
7544
- usage
8827
+ usage,
8828
+ usageAgentName,
8829
+ subagentInvocations
7545
8830
  );
7546
8831
  } catch (err) {
7547
8832
  if (err instanceof ChannelAuthError) throw err;
@@ -7600,7 +8885,7 @@ var ChannelDriver = class _ChannelDriver {
7600
8885
  */
7601
8886
  async readoptProcessing() {
7602
8887
  const rows = await this.getProcessingMessages();
7603
- if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
8888
+ if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
7604
8889
  const stillProcessing = new Set(rows.map((r) => r.id));
7605
8890
  for (const id of [
7606
8891
  ...this.dontRedispatch,
@@ -7620,6 +8905,16 @@ var ChannelDriver = class _ChannelDriver {
7620
8905
  }
7621
8906
  }
7622
8907
  }
8908
+ for (const id of [...this.untrackableAck]) {
8909
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
8910
+ this.untrackableAck.delete(id);
8911
+ this.log({
8912
+ level: "debug",
8913
+ message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
8914
+ message_id: id
8915
+ });
8916
+ }
8917
+ }
7623
8918
  }
7624
8919
  if (rows.length === 0) return;
7625
8920
  const bySession = /* @__PURE__ */ new Map();
@@ -7640,23 +8935,32 @@ var ChannelDriver = class _ChannelDriver {
7640
8935
  for (const [sessionId, sessionRows] of bySession) {
7641
8936
  let messages;
7642
8937
  try {
7643
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
7644
- if (!res.ok) {
7645
- this.log({
7646
- level: "warn",
7647
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
7648
- });
7649
- continue;
7650
- }
7651
- const body = await res.json();
7652
- if (!Array.isArray(body)) {
7653
- this.log({
7654
- level: "warn",
7655
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
7656
- });
7657
- continue;
8938
+ if (this.isV2) {
8939
+ const snapshot = await this.getSessionMessages(sessionId);
8940
+ if (snapshot === null) {
8941
+ this.log({
8942
+ level: "warn",
8943
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
8944
+ });
8945
+ continue;
8946
+ }
8947
+ messages = snapshot;
8948
+ } else {
8949
+ const polled = await pollSessionMessagesForRedrive(
8950
+ this.port,
8951
+ sessionId,
8952
+ this.openCodeClient
8953
+ );
8954
+ if (!polled.ok) {
8955
+ const normalized = normalizeRedrivePollFailureBody(polled.body);
8956
+ this.log({
8957
+ level: "warn",
8958
+ 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`
8959
+ });
8960
+ continue;
8961
+ }
8962
+ messages = polled.messages;
7658
8963
  }
7659
- messages = body;
7660
8964
  } catch (err) {
7661
8965
  this.log({
7662
8966
  level: "warn",
@@ -7665,7 +8969,7 @@ var ChannelDriver = class _ChannelDriver {
7665
8969
  continue;
7666
8970
  }
7667
8971
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
7668
- const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
8972
+ const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
7669
8973
  for (const row of sessionRows) {
7670
8974
  await this.readoptOne(sessionId, row, messages, sessionOngoing);
7671
8975
  }
@@ -7712,7 +9016,7 @@ var ChannelDriver = class _ChannelDriver {
7712
9016
  if (restartAborted) {
7713
9017
  this.log({
7714
9018
  level: "info",
7715
- 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`,
9019
+ 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`,
7716
9020
  conversation_id: row.conversation_id,
7717
9021
  message_id: row.id
7718
9022
  });
@@ -7720,6 +9024,12 @@ var ChannelDriver = class _ChannelDriver {
7720
9024
  if (state === "failed" && !restartAborted) {
7721
9025
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
7722
9026
  const usage = messageUsage(messages, ocId ?? "");
9027
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
9028
+ const subagentInvocations = await this.resolveSubagentInvocations(
9029
+ messages,
9030
+ ocId ?? "",
9031
+ row.id
9032
+ );
7723
9033
  const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
7724
9034
  this.log({
7725
9035
  level: "error",
@@ -7728,7 +9038,16 @@ var ChannelDriver = class _ChannelDriver {
7728
9038
  message_id: row.id
7729
9039
  });
7730
9040
  try {
7731
- await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage, failure);
9041
+ await this.markFailed(
9042
+ row.conversation_id,
9043
+ row.id,
9044
+ sessionId,
9045
+ error2,
9046
+ usage,
9047
+ failure,
9048
+ usageAgentName,
9049
+ subagentInvocations
9050
+ );
7732
9051
  } catch (err) {
7733
9052
  if (err instanceof ChannelAuthError) throw err;
7734
9053
  if (err instanceof ChannelTerminalError) {
@@ -7752,10 +9071,11 @@ var ChannelDriver = class _ChannelDriver {
7752
9071
  }
7753
9072
  await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
7754
9073
  this.dontRedispatch.delete(row.id);
9074
+ this.untrackableAck.delete(row.id);
7755
9075
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
7756
9076
  return;
7757
9077
  }
7758
- if (this.dontRedispatch.has(row.id)) {
9078
+ if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
7759
9079
  this.log({
7760
9080
  level: "debug",
7761
9081
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
@@ -7775,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
7775
9095
  const finish = reply?.info?.finish ?? reply?.finish;
7776
9096
  this.log({
7777
9097
  level: "info",
7778
- 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`,
9098
+ 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`,
7779
9099
  conversation_id: row.conversation_id,
7780
9100
  message_id: row.id
7781
9101
  });
@@ -7784,7 +9104,7 @@ var ChannelDriver = class _ChannelDriver {
7784
9104
  }
7785
9105
  this.log({
7786
9106
  level: "info",
7787
- 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)`,
9107
+ 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)`,
7788
9108
  conversation_id: row.conversation_id,
7789
9109
  message_id: row.id
7790
9110
  });
@@ -7794,7 +9114,7 @@ var ChannelDriver = class _ChannelDriver {
7794
9114
  if (ongoing === true) {
7795
9115
  this.log({
7796
9116
  level: "debug",
7797
- 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)`,
9117
+ 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)`,
7798
9118
  conversation_id: row.conversation_id,
7799
9119
  message_id: row.id
7800
9120
  });
@@ -7802,7 +9122,7 @@ var ChannelDriver = class _ChannelDriver {
7802
9122
  if (shape === "b1") {
7803
9123
  this.log({
7804
9124
  level: "debug",
7805
- 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`,
9125
+ 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`,
7806
9126
  conversation_id: row.conversation_id,
7807
9127
  message_id: row.id
7808
9128
  });
@@ -7814,7 +9134,7 @@ var ChannelDriver = class _ChannelDriver {
7814
9134
  }
7815
9135
  this.log({
7816
9136
  level: "debug",
7817
- 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`,
9137
+ 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`,
7818
9138
  conversation_id: row.conversation_id,
7819
9139
  message_id: row.id
7820
9140
  });
@@ -7864,7 +9184,7 @@ var ChannelDriver = class _ChannelDriver {
7864
9184
  * extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
7865
9185
  * SAME delivery instead of duplicating it.
7866
9186
  *
7867
- * EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops
9187
+ * EVEN IF the row was previously parked by either recovery fence (a give-up stops
7868
9188
  * re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
7869
9189
  * `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
7870
9190
  * leave for cron; transient → log + leave for the next drain (the still-
@@ -7890,7 +9210,22 @@ var ChannelDriver = class _ChannelDriver {
7890
9210
  try {
7891
9211
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
7892
9212
  const usage = messageUsage(messages, ocId ?? "");
7893
- await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
9213
+ const usageAgentName = this.usageAgentName(messages, ocId ?? "");
9214
+ const subagentInvocations = await this.resolveSubagentInvocations(
9215
+ messages,
9216
+ ocId ?? "",
9217
+ row.id
9218
+ );
9219
+ await this.markDone(
9220
+ row.conversation_id,
9221
+ row.id,
9222
+ sessionId,
9223
+ ocId,
9224
+ title,
9225
+ usage,
9226
+ usageAgentName,
9227
+ subagentInvocations
9228
+ );
7894
9229
  } catch (err) {
7895
9230
  if (err instanceof ChannelAuthError) throw err;
7896
9231
  if (err instanceof ChannelTerminalError) {
@@ -7916,6 +9251,7 @@ var ChannelDriver = class _ChannelDriver {
7916
9251
  await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
7917
9252
  }
7918
9253
  this.dontRedispatch.delete(row.id);
9254
+ this.untrackableAck.delete(row.id);
7919
9255
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
7920
9256
  }
7921
9257
  /**
@@ -7979,19 +9315,90 @@ var ChannelDriver = class _ChannelDriver {
7979
9315
  conversation_id: row.conversation_id,
7980
9316
  message_id: row.id
7981
9317
  });
7982
- this.awaitingReadopt.add(row.id);
9318
+ if (!this.isV2) this.awaitingReadopt.add(row.id);
7983
9319
  const readoptConv = this.convForRow(sessionId, row);
7984
9320
  const readoptMessage = this.queuedMessageForRow(row);
7985
- const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
9321
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
9322
+ if (this.isV2 && row.attachments && row.attachments.length > 0) {
9323
+ this.signalAttachmentsSkipped(
9324
+ row.conversation_id,
9325
+ row.id,
9326
+ row.attachments.map((attachment, index) => ({
9327
+ index,
9328
+ mime: attachment.mime,
9329
+ ...attachment.filename ? { filename: attachment.filename } : {},
9330
+ status: "skipped"
9331
+ })),
9332
+ false
9333
+ );
9334
+ }
7986
9335
  let ocId;
7987
9336
  try {
7988
- ocId = await this.dispatchLocked(
9337
+ ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
7989
9338
  sessionId,
7990
- () => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
9339
+ () => sendPromptAsync(
9340
+ this.port,
9341
+ sessionId,
9342
+ row.content,
9343
+ options,
9344
+ sendAttachments,
9345
+ this.openCodeClient
9346
+ )
7991
9347
  );
7992
9348
  } catch (err) {
7993
9349
  this.awaitingReadopt.delete(row.id);
7994
9350
  if (err instanceof ChannelAuthError) throw err;
9351
+ if (this.isV2) {
9352
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
9353
+ const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
9354
+ if (!invalidPromptAck) {
9355
+ const exists = await this.sessionExists(sessionId);
9356
+ if (exists === false) {
9357
+ this.log({
9358
+ level: "warn",
9359
+ 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}`,
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
+ if (exists === null) {
9367
+ this.log({
9368
+ level: "warn",
9369
+ 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}`,
9370
+ conversation_id: row.conversation_id,
9371
+ message_id: row.id
9372
+ });
9373
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9374
+ return;
9375
+ }
9376
+ } else {
9377
+ this.untrackableAck.add(row.id);
9378
+ }
9379
+ this.log({
9380
+ level: "error",
9381
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
9382
+ conversation_id: row.conversation_id,
9383
+ message_id: row.id
9384
+ });
9385
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
9386
+ (markErr) => {
9387
+ this.log({
9388
+ level: "warn",
9389
+ message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9390
+ conversation_id: row.conversation_id,
9391
+ message_id: row.id
9392
+ });
9393
+ if (invalidPromptAck) {
9394
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9395
+ } else {
9396
+ this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
9397
+ }
9398
+ }
9399
+ );
9400
+ return;
9401
+ }
7995
9402
  this.log({
7996
9403
  level: "warn",
7997
9404
  message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
@@ -8002,6 +9409,17 @@ var ChannelDriver = class _ChannelDriver {
8002
9409
  return;
8003
9410
  }
8004
9411
  if (ocId === null) {
9412
+ if (this.isV2) {
9413
+ this.untrackableAck.add(row.id);
9414
+ this.log({
9415
+ level: "error",
9416
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} completed without an acknowledged message id`,
9417
+ conversation_id: row.conversation_id,
9418
+ message_id: row.id
9419
+ });
9420
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9421
+ return;
9422
+ }
8005
9423
  this.awaitingReadopt.delete(row.id);
8006
9424
  const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
8007
9425
  if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
@@ -8123,12 +9541,13 @@ var ChannelDriver = class _ChannelDriver {
8123
9541
  this.dispatched.delete(evidentMessageId);
8124
9542
  }
8125
9543
  /**
8126
- * Poll `/question` + `/permission` (scoped to the session) and surface NEW ones
8127
- * via `reportInteraction` (Task 3.5), carrying the PAUSED message's own
8128
- * `source_message_id` so the server @mentions the correct person under
8129
- * concurrency. Dedups by interaction id across ticks (reused per-session sets).
9544
+ * Poll V1's global `/question` + `/permission`, or V2's watched-session form and
9545
+ * permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
9546
+ * carrying the PAUSED message's own `source_message_id` so the server @mentions
9547
+ * the correct person under concurrency. Dedups by interaction id across ticks
9548
+ * (reused per-session sets).
8130
9549
  *
8131
- * The interaction is attributed to the in-flight message it paused on. opencode
9550
+ * The interaction is attributed to the in-flight message it paused on. OpenCode
8132
9551
  * stamps a `messageID` on a permission (and `tool.messageID` on a question) =
8133
9552
  * the assistant message id, whose `parentID` is the user message id — but the
8134
9553
  * simplest robust attribution here is: the single in-flight message that is
@@ -8151,16 +9570,14 @@ var ChannelDriver = class _ChannelDriver {
8151
9570
  let permissionsPolledOk = true;
8152
9571
  let questions = [];
8153
9572
  try {
8154
- const res = await this.fetchImpl(`${this.opencodeBase}/question`);
8155
- if (res.ok) {
8156
- const body = await res.json();
8157
- if (Array.isArray(body)) {
8158
- questions = body;
8159
- } else {
8160
- questionsPolledOk = false;
8161
- }
9573
+ if (this.isV2) {
9574
+ const forms = await listV2Forms(this.openCodeClient, sessionId);
9575
+ if (forms === null) questionsPolledOk = false;
9576
+ else questions = forms;
8162
9577
  } else {
8163
- questionsPolledOk = false;
9578
+ const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
9579
+ if (listed === null) questionsPolledOk = false;
9580
+ else questions = listed;
8164
9581
  }
8165
9582
  } catch {
8166
9583
  questionsPolledOk = false;
@@ -8180,16 +9597,14 @@ var ChannelDriver = class _ChannelDriver {
8180
9597
  }
8181
9598
  let permissions = [];
8182
9599
  try {
8183
- const res = await this.fetchImpl(`${this.opencodeBase}/permission`);
8184
- if (res.ok) {
8185
- const body = await res.json();
8186
- if (Array.isArray(body)) {
8187
- permissions = body;
8188
- } else {
8189
- permissionsPolledOk = false;
8190
- }
9600
+ if (this.isV2) {
9601
+ const listed = await listV2Permissions(this.openCodeClient, sessionId);
9602
+ if (listed === null) permissionsPolledOk = false;
9603
+ else permissions = listed;
8191
9604
  } else {
8192
- permissionsPolledOk = false;
9605
+ const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
9606
+ if (listed === null) permissionsPolledOk = false;
9607
+ else permissions = listed;
8193
9608
  }
8194
9609
  } catch {
8195
9610
  permissionsPolledOk = false;
@@ -8283,10 +9698,14 @@ var ChannelDriver = class _ChannelDriver {
8283
9698
  if (cached !== void 0) return cached;
8284
9699
  let parent = void 0;
8285
9700
  try {
8286
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8287
- if (res.ok) {
8288
- const body = await res.json();
8289
- parent = body && typeof body.parentID === "string" ? body.parentID : null;
9701
+ if (this.isV2) {
9702
+ const session = await getV2Session(this.openCodeClient, sessionId);
9703
+ parent = null;
9704
+ const candidate = session.parentID;
9705
+ if (typeof candidate === "string") parent = candidate;
9706
+ } else {
9707
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9708
+ parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
8290
9709
  }
8291
9710
  } catch {
8292
9711
  parent = void 0;
@@ -8294,6 +9713,164 @@ var ChannelDriver = class _ChannelDriver {
8294
9713
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
8295
9714
  return parent;
8296
9715
  }
9716
+ usageAgentName(messages, userMessageId) {
9717
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
9718
+ const mode = reply?.info?.mode;
9719
+ if (typeof mode === "string" && mode.length > 0) return mode;
9720
+ const agent = reply?.info?.agent;
9721
+ return typeof agent === "string" && agent.length > 0 ? agent : null;
9722
+ }
9723
+ async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
9724
+ if (!messages) return void 0;
9725
+ const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
9726
+ const cached = cache.get(messageId);
9727
+ if (cached) return cached;
9728
+ const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
9729
+ (err) => {
9730
+ this.log({
9731
+ level: "warn",
9732
+ message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
9733
+ message_id: messageId
9734
+ });
9735
+ return void 0;
9736
+ }
9737
+ );
9738
+ cache.set(messageId, collection);
9739
+ const result = await collection;
9740
+ if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
9741
+ return result;
9742
+ }
9743
+ clearSubagentInvocationCaches(messageId) {
9744
+ this.subagentInvocationCollections.delete(messageId);
9745
+ this.subagentInvocationPrefetches.delete(messageId);
9746
+ }
9747
+ async buildSubagentInvocations(messages, userMessageId, messageId) {
9748
+ const rootCalls = collectTaskCalls(messages, userMessageId);
9749
+ if (rootCalls.length === 0) return void 0;
9750
+ const childMessages = /* @__PURE__ */ new Map();
9751
+ const seenCallIds = new Set(rootCalls.map((call) => call.callID));
9752
+ const work = rootCalls.map((call) => ({
9753
+ call,
9754
+ depth: 1
9755
+ }));
9756
+ const payload = [];
9757
+ const fetchChildMessages = (sessionId) => {
9758
+ const cached = childMessages.get(sessionId);
9759
+ if (cached) return cached;
9760
+ const pending = (async () => {
9761
+ try {
9762
+ const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
9763
+ if (messages2 === null) {
9764
+ this.log({
9765
+ level: "warn",
9766
+ 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`,
9767
+ message_id: messageId
9768
+ });
9769
+ return null;
9770
+ }
9771
+ return messages2;
9772
+ } catch (err) {
9773
+ this.log({
9774
+ level: "warn",
9775
+ 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)}`,
9776
+ message_id: messageId
9777
+ });
9778
+ return null;
9779
+ }
9780
+ })();
9781
+ childMessages.set(sessionId, pending);
9782
+ return pending;
9783
+ };
9784
+ const fetchChildWithoutBlocking = async (sessionId) => {
9785
+ const pending = fetchChildMessages(sessionId);
9786
+ let timer;
9787
+ const timeout = new Promise((resolve4) => {
9788
+ timer = setTimeout(() => {
9789
+ this.log({
9790
+ level: "warn",
9791
+ 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`,
9792
+ message_id: messageId
9793
+ });
9794
+ resolve4(null);
9795
+ }, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
9796
+ });
9797
+ try {
9798
+ return await Promise.race([pending, timeout]);
9799
+ } finally {
9800
+ if (timer !== void 0) clearTimeout(timer);
9801
+ }
9802
+ };
9803
+ while (work.length > 0) {
9804
+ const groups = /* @__PURE__ */ new Map();
9805
+ for (const item of work.splice(0)) {
9806
+ const group = groups.get(item.call.childSessionId) ?? [];
9807
+ group.push(item);
9808
+ groups.set(item.call.childSessionId, group);
9809
+ }
9810
+ const groupResults = await Promise.all(
9811
+ [...groups].map(async ([sessionId, items]) => ({
9812
+ sessionId,
9813
+ items,
9814
+ messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
9815
+ }))
9816
+ );
9817
+ for (const { sessionId, items, messages: child } of groupResults) {
9818
+ if (sessionId !== null && child === null) continue;
9819
+ const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
9820
+ child,
9821
+ items.map(({ call }) => ({
9822
+ callID: call.callID,
9823
+ timeStart: call.timeStart,
9824
+ timeEnd: call.timeEnd
9825
+ }))
9826
+ );
9827
+ if (sessionId !== null && attribution.unattributed.length > 0) {
9828
+ this.log({
9829
+ level: "warn",
9830
+ 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`,
9831
+ message_id: messageId
9832
+ });
9833
+ }
9834
+ const usageByCall = new Map(
9835
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
9836
+ );
9837
+ const messagesByCall = new Map(
9838
+ attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
9839
+ );
9840
+ for (const { call, depth } of items) {
9841
+ const usage = usageByCall.get(call.callID) ?? null;
9842
+ payload.push({
9843
+ tool_call_id: call.callID,
9844
+ agent_name: call.subagentName,
9845
+ opencode_session_id: call.childSessionId,
9846
+ parent_opencode_session_id: call.parentSessionId,
9847
+ depth,
9848
+ status: call.status,
9849
+ started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
9850
+ ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
9851
+ usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
9852
+ usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
9853
+ usage_tokens_input: usage?.usage_tokens_input ?? null,
9854
+ usage_tokens_output: usage?.usage_tokens_output ?? null,
9855
+ usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
9856
+ usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
9857
+ usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
9858
+ usage_cost_usd: usage?.usage_cost_usd ?? null
9859
+ });
9860
+ for (const assigned of messagesByCall.get(call.callID) ?? []) {
9861
+ const parentId = assigned.info?.parentID ?? assigned.parentID;
9862
+ if (!parentId) continue;
9863
+ for (const nested of collectTaskCalls([assigned], parentId)) {
9864
+ if (seenCallIds.has(nested.callID)) continue;
9865
+ seenCallIds.add(nested.callID);
9866
+ work.push({ call: nested, depth: depth + 1 });
9867
+ }
9868
+ }
9869
+ }
9870
+ }
9871
+ }
9872
+ return payload.length > 0 ? payload : void 0;
9873
+ }
8297
9874
  /**
8298
9875
  * OpenCode's synchronous default session title (e.g.
8299
9876
  * `"New session - 1737800000000"`), assigned immediately when a session is
@@ -8332,21 +9909,18 @@ var ChannelDriver = class _ChannelDriver {
8332
9909
  const cached = this.sessionTitles.get(sessionId);
8333
9910
  if (cached != null) return cached;
8334
9911
  try {
8335
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8336
- if (res.ok) {
8337
- const body = await res.json();
8338
- const title = body && typeof body.title === "string" ? body.title.trim() : "";
8339
- if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
8340
- this.sessionTitles.set(sessionId, title);
8341
- return title;
8342
- }
8343
- return null;
9912
+ let title = "";
9913
+ if (this.isV2) {
9914
+ title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
9915
+ } else {
9916
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9917
+ title = typeof body?.title === "string" ? body.title.trim() : "";
8344
9918
  }
8345
- this.log({
8346
- level: "debug",
8347
- message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
8348
- conversation_id: conversationId
8349
- });
9919
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
9920
+ this.sessionTitles.set(sessionId, title);
9921
+ return title;
9922
+ }
9923
+ return null;
8350
9924
  } catch (err) {
8351
9925
  this.log({
8352
9926
  level: "debug",
@@ -8444,7 +10018,7 @@ var ChannelDriver = class _ChannelDriver {
8444
10018
  * `SessionStatus` only.
8445
10019
  */
8446
10020
  async isAnyDescendantSessionAlive(rootSessionId) {
8447
- const sessions = await listSessions(this.port);
10021
+ const sessions = await this.listSessions();
8448
10022
  if (!sessions) {
8449
10023
  this.log({
8450
10024
  level: "warn",
@@ -8455,7 +10029,7 @@ var ChannelDriver = class _ChannelDriver {
8455
10029
  for (const candidate of sessions) {
8456
10030
  if (!candidate?.id || candidate.id === rootSessionId) continue;
8457
10031
  if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
8458
- const childMsgs = await getSessionMessages(this.port, candidate.id);
10032
+ const childMsgs = await this.getSubagentSessionMessages(candidate.id);
8459
10033
  if (isSessionActivelyGenerating(childMsgs)) {
8460
10034
  return true;
8461
10035
  }
@@ -8517,7 +10091,7 @@ var ChannelDriver = class _ChannelDriver {
8517
10091
  * `isB2AbandonmentConfirmed`.
8518
10092
  */
8519
10093
  async isAnyDescendantSessionOngoing(rootSessionId) {
8520
- const sessions = await listSessions(this.port);
10094
+ const sessions = await this.listSessions();
8521
10095
  if (!sessions) {
8522
10096
  this.log({
8523
10097
  level: "warn",
@@ -8534,7 +10108,7 @@ var ChannelDriver = class _ChannelDriver {
8534
10108
  continue;
8535
10109
  }
8536
10110
  if (membership === false) continue;
8537
- const ongoing = await isSessionOngoing(this.port, candidate.id);
10111
+ const ongoing = await this.isSessionOngoing(candidate.id);
8538
10112
  if (ongoing === true) return true;
8539
10113
  if (ongoing === null) indeterminate = true;
8540
10114
  }
@@ -8777,7 +10351,7 @@ var ChannelDriver = class _ChannelDriver {
8777
10351
  * watcher retries next tick within the
8778
10352
  * deadline, Finding 4).
8779
10353
  */
8780
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
10354
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
8781
10355
  const res = await this.fetchImpl(
8782
10356
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8783
10357
  {
@@ -8793,15 +10367,21 @@ var ChannelDriver = class _ChannelDriver {
8793
10367
  opencode_session_id: sessionId,
8794
10368
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
8795
10369
  ...title ? { title } : {},
8796
- ...usage ? usage : {}
10370
+ ...usage ? usage : {},
10371
+ ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
10372
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
8797
10373
  })
8798
10374
  }
8799
10375
  );
8800
10376
  this.assertAuth(res, "marking message as done");
8801
- if (res.ok) return;
10377
+ if (res.ok) {
10378
+ this.clearSubagentInvocationCaches(messageId);
10379
+ return;
10380
+ }
8802
10381
  if (isRetryableStatus(res.status)) {
8803
10382
  throw new Error(`marking message as done: HTTP ${res.status}`);
8804
10383
  }
10384
+ this.clearSubagentInvocationCaches(messageId);
8805
10385
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
8806
10386
  }
8807
10387
  /**
@@ -8816,7 +10396,7 @@ var ChannelDriver = class _ChannelDriver {
8816
10396
  * exists but is wedged, so the next attempt must get a fresh one
8817
10397
  * instead of reusing it — see WI-1's server-side null-clearing PATCH).
8818
10398
  */
8819
- async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
10399
+ async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
8820
10400
  const body = { status: "failed" };
8821
10401
  if (sessionId === null) {
8822
10402
  body.opencode_session_id = null;
@@ -8825,23 +10405,33 @@ var ChannelDriver = class _ChannelDriver {
8825
10405
  }
8826
10406
  if (error2 !== void 0) body.error = error2;
8827
10407
  if (usage) Object.assign(body, usage);
10408
+ if (usageAgentName) body.usage_agent_name = usageAgentName;
10409
+ if (subagentInvocations && subagentInvocations.length > 0) {
10410
+ body.subagent_invocations = subagentInvocations;
10411
+ }
8828
10412
  if (failure) {
8829
10413
  body.failure_kind = failure.kind;
8830
10414
  body.failure_provider_id = failure.providerId;
8831
10415
  body.failure_model_id = failure.modelId;
8832
10416
  body.failure_reason = failure.reason;
8833
10417
  }
8834
- await this.callWithRetry(
8835
- "marking message as failed",
8836
- () => this.fetchImpl(
8837
- `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
8838
- {
8839
- method: "PATCH",
8840
- headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
8841
- body: JSON.stringify(body)
8842
- }
8843
- )
8844
- );
10418
+ try {
10419
+ await this.callWithRetry(
10420
+ "marking message as failed",
10421
+ () => this.fetchImpl(
10422
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
10423
+ {
10424
+ method: "PATCH",
10425
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
10426
+ body: JSON.stringify(body)
10427
+ }
10428
+ )
10429
+ );
10430
+ } catch (err) {
10431
+ if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
10432
+ throw err;
10433
+ }
10434
+ this.clearSubagentInvocationCaches(messageId);
8845
10435
  }
8846
10436
  /**
8847
10437
  * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
@@ -8858,7 +10448,7 @@ var ChannelDriver = class _ChannelDriver {
8858
10448
  const classified = messageFailure(messages, userMessageId);
8859
10449
  if (classified != null) return classified;
8860
10450
  const reply = findLastAssistantReplyFor(messages, userMessageId);
8861
- const hasProvider = await hasAnyConfiguredProvider(this.port);
10451
+ const hasProvider = await this.hasAnyConfiguredProvider();
8862
10452
  return applyZeroProviderFallback(
8863
10453
  classified,
8864
10454
  hasProvider,
@@ -8931,7 +10521,7 @@ var ChannelDriver = class _ChannelDriver {
8931
10521
  const succeededProviders = /* @__PURE__ */ new Set();
8932
10522
  for (const ref of refs) {
8933
10523
  try {
8934
- const childMessages = await getSessionMessages(this.port, ref.sessionId);
10524
+ const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
8935
10525
  if (childMessages === null) {
8936
10526
  this.log({
8937
10527
  level: "debug",
@@ -9262,6 +10852,7 @@ Port ${port} is already in use.`));
9262
10852
 
9263
10853
  // src/commands/ensure-opencode-v2.ts
9264
10854
  import chalk6 from "chalk";
10855
+ import ora3 from "ora";
9265
10856
  import { select as select3 } from "@inquirer/prompts";
9266
10857
  async function probeOpenCode2WithoutPassword(port) {
9267
10858
  try {
@@ -9287,11 +10878,7 @@ function unknownPasswordError(port) {
9287
10878
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
9288
10879
  );
9289
10880
  }
9290
- function v2SessionSupportIncompleteError() {
9291
- return new Error(
9292
- "OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
9293
- );
9294
- }
10881
+ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
9295
10882
  async function ensureOpenCode2Running(ctx) {
9296
10883
  const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
9297
10884
  if (initialHealth.authFailed) {
@@ -9334,13 +10921,37 @@ Port ${port} is already in use.`));
9334
10921
  }
9335
10922
  }
9336
10923
  if (!ctx.interactive) {
9337
- throw v2SessionSupportIncompleteError();
10924
+ ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
10925
+ const { child: proc, password } = await startOpenCode2(port, {
10926
+ inheritStdio: ctx.inheritStdio
10927
+ });
10928
+ const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
10929
+ if (!health.healthy) {
10930
+ return {
10931
+ port,
10932
+ process: proc,
10933
+ version: null,
10934
+ notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
10935
+ password
10936
+ };
10937
+ }
10938
+ ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
10939
+ return {
10940
+ port,
10941
+ process: proc,
10942
+ version: health.version ?? null,
10943
+ notReadyReason: null,
10944
+ password
10945
+ };
9338
10946
  }
9339
- console.log(chalk6.yellow(`
9340
- ${v2SessionSupportIncompleteError().message}`));
9341
10947
  const action = await select3({
9342
10948
  message: "OpenCode V2 is not running. What would you like to do?",
9343
10949
  choices: [
10950
+ {
10951
+ name: "Start OpenCode V2 for me",
10952
+ value: "start",
10953
+ description: `Run 'opencode2 serve --port ${port}'`
10954
+ },
9344
10955
  {
9345
10956
  name: "Show me the command",
9346
10957
  value: "manual",
@@ -9361,6 +10972,25 @@ ${v2SessionSupportIncompleteError().message}`));
9361
10972
  blank();
9362
10973
  throw new Error("Please start OpenCode V2 manually");
9363
10974
  }
10975
+ if (action === "start") {
10976
+ const spinner = ora3("Starting OpenCode V2...").start();
10977
+ const { child: proc, password } = await startOpenCode2(port, {
10978
+ inheritStdio: ctx.inheritStdio
10979
+ });
10980
+ const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
10981
+ if (!health.healthy) {
10982
+ spinner.fail("Failed to start OpenCode V2");
10983
+ throw new Error("OpenCode V2 failed to start");
10984
+ }
10985
+ spinner.stop();
10986
+ return {
10987
+ port,
10988
+ process: proc,
10989
+ version: health.version ?? null,
10990
+ notReadyReason: null,
10991
+ password
10992
+ };
10993
+ }
9364
10994
  return {
9365
10995
  port,
9366
10996
  process: null,
@@ -10254,7 +11884,7 @@ async function driveChannels(state, driver) {
10254
11884
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
10255
11885
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
10256
11886
  if (claudeCredentialApplied || opencodeAuthApplied) {
10257
- void reloadProviderCache(state.port).catch(
11887
+ void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
10258
11888
  (error2) => logActivity(state, {
10259
11889
  type: "error",
10260
11890
  error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
@@ -10379,7 +12009,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
10379
12009
  async function runSweep(state, driver, config) {
10380
12010
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
10381
12011
  try {
10382
- const sessions = await listSessions(state.port);
12012
+ const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
10383
12013
  if (sessions === null) {
10384
12014
  logActivity(state, {
10385
12015
  type: "info",
@@ -10409,7 +12039,7 @@ async function runSweep(state, driver, config) {
10409
12039
  });
10410
12040
  continue;
10411
12041
  }
10412
- if (await deleteSession(state.port, id)) deleted++;
12042
+ if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
10413
12043
  else failed++;
10414
12044
  }
10415
12045
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
@@ -10637,14 +12267,11 @@ function scheduleClaudeUsageReporting(state, options) {
10637
12267
  report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
10638
12268
  isLocalCredentialProblem,
10639
12269
  forcedOnHint: "run `claude` to sign in",
10640
- firstDelayMs: () => FIRST_REPORT_DELAY_MS,
10641
- nextDelayMs: nextReportDelayMs,
10642
- failureLogLevel: claudeUsageFailureLogLevel
12270
+ firstDelayMs: firstReportDelayMs,
12271
+ nextDelayMs: usageReportDelayMs,
12272
+ failureLogLevel: usageReportFailureLogLevel
10643
12273
  });
10644
12274
  }
10645
- var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
10646
- var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
10647
- var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
10648
12275
  function scheduleResourceUsageReporting(state, options) {
10649
12276
  const { enabled, warnings } = resolveResourceUsageReportingEnabled(
10650
12277
  options.resourceUsageReporting,
@@ -10697,10 +12324,7 @@ function scheduleResourceUsageReporting(state, options) {
10697
12324
  consecutiveFailures++;
10698
12325
  logActivity(state, {
10699
12326
  type: "info",
10700
- level: reportFailureLogLevel(
10701
- consecutiveFailures,
10702
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10703
- ),
12327
+ level: usageReportFailureLogLevel(consecutiveFailures),
10704
12328
  message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
10705
12329
  });
10706
12330
  }
@@ -10709,20 +12333,11 @@ function scheduleResourceUsageReporting(state, options) {
10709
12333
  const message = error2 instanceof Error ? error2.message : String(error2);
10710
12334
  logActivity(state, {
10711
12335
  type: "info",
10712
- level: reportFailureLogLevel(
10713
- consecutiveFailures,
10714
- RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
10715
- ),
12336
+ level: usageReportFailureLogLevel(consecutiveFailures),
10716
12337
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
10717
12338
  });
10718
12339
  } finally {
10719
- state.resourceUsageTimer = setTimeout(
10720
- () => void tick(),
10721
- jitteredDelayMs(
10722
- RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
10723
- RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
10724
- )
10725
- );
12340
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
10726
12341
  }
10727
12342
  };
10728
12343
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -10931,6 +12546,9 @@ async function run(options) {
10931
12546
  connected: false,
10932
12547
  opencodeConnected: false,
10933
12548
  opencodeVersion: null,
12549
+ opencodeApiVersion: "v1",
12550
+ opencodePassword: null,
12551
+ opencodeClient: null,
10934
12552
  sessionDbProvenanceAnomaly: false,
10935
12553
  opencodeProcess: null,
10936
12554
  stopOpenCodeLogTail: null,
@@ -11085,7 +12703,7 @@ async function run(options) {
11085
12703
  console.log(chalk7.bold("Evident Run"));
11086
12704
  console.log(chalk7.dim("-".repeat(40)));
11087
12705
  }
11088
- const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
12706
+ const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
11089
12707
  let validation = await getAgentInfo(state.agentId, state.authHeader);
11090
12708
  if (!validation.valid && validation.authFailed && interactive) {
11091
12709
  spinner?.fail("Authentication failed");
@@ -11206,7 +12824,7 @@ async function run(options) {
11206
12824
  logActivity(state, { type: "info", level: "warn", message: warning2 });
11207
12825
  }
11208
12826
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
11209
- const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
12827
+ const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
11210
12828
  try {
11211
12829
  const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
11212
12830
  port: state.port,
@@ -11226,6 +12844,19 @@ async function run(options) {
11226
12844
  state.port = oc.port;
11227
12845
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
11228
12846
  state.opencodeVersion = oc.version;
12847
+ state.opencodeApiVersion = opencodeVersion;
12848
+ let opencodePassword = null;
12849
+ if (opencodeVersion === "v2" && "password" in oc) {
12850
+ const value = oc.password;
12851
+ if (typeof value === "string" || value === null) opencodePassword = value;
12852
+ }
12853
+ state.opencodePassword = opencodePassword;
12854
+ const openCodeClient = createOpenCodeClient({
12855
+ port: state.port,
12856
+ version: state.opencodeApiVersion,
12857
+ password: state.opencodePassword
12858
+ });
12859
+ state.opencodeClient = openCodeClient;
11229
12860
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
11230
12861
  try {
11231
12862
  writeFileSync6(options.opencodePidFile, `${oc.process.pid}
@@ -11262,15 +12893,19 @@ async function run(options) {
11262
12893
  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}).`;
11263
12894
  logActivity(state, { type: "info", level: "warn", message });
11264
12895
  } else {
11265
- const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
12896
+ const versionWarning = buildOpenCodeVersionWarning(
12897
+ state.opencodeVersion,
12898
+ state.opencodeApiVersion
12899
+ );
11266
12900
  if (versionWarning) {
11267
12901
  log2(state, versionWarning, "warn");
11268
12902
  if (state.interactive && !state.json) {
11269
12903
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
11270
12904
  }
11271
12905
  }
12906
+ await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
11272
12907
  const noProviderWarning = buildNoProviderWarning(
11273
- await hasAnyConfiguredProvider(state.port)
12908
+ await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
11274
12909
  );
11275
12910
  if (noProviderWarning) {
11276
12911
  log2(state, noProviderWarning, "warn");
@@ -11393,11 +13028,12 @@ async function run(options) {
11393
13028
  });
11394
13029
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
11395
13030
  }
11396
- const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
13031
+ const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
11397
13032
  const channelDriver = new ChannelDriver({
11398
13033
  agentId: state.agentId,
11399
13034
  port: state.port,
11400
13035
  apiUrl: getApiUrlConfig(),
13036
+ openCodeClient: state.opencodeClient ?? void 0,
11401
13037
  getAuthHeader: () => state.authHeader,
11402
13038
  conversationFilter: state.conversationFilter,
11403
13039
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
@@ -11423,6 +13059,7 @@ async function run(options) {
11423
13059
  agentId: state.agentId,
11424
13060
  getAuthHeader: () => state.authHeader,
11425
13061
  port: state.port,
13062
+ openCodePassword: state.opencodePassword,
11426
13063
  isRunning: () => state.running,
11427
13064
  events: {
11428
13065
  onConnected: (agentId, isReconnect) => {
@@ -11447,7 +13084,11 @@ async function run(options) {
11447
13084
  emitAgentConnected(state.agentId, {
11448
13085
  port: state.port,
11449
13086
  cli_version: getCliVersion(),
11450
- opencode_version: state.opencodeVersion
13087
+ opencode_version: reportedOpenCodeVersion({
13088
+ version: state.opencodeVersion,
13089
+ major: state.opencodeApiVersion,
13090
+ connected: state.opencodeConnected
13091
+ })
11451
13092
  });
11452
13093
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
11453
13094
  if (state.interactive) displayStatus(state);
@@ -11563,7 +13204,7 @@ async function run(options) {
11563
13204
  state.openaiUsageTimer = timer;
11564
13205
  },
11565
13206
  fetchUsage: async () => {
11566
- const usage = await getOpenAiUsage(state.port);
13207
+ const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
11567
13208
  if (usage.subscription === null) {
11568
13209
  logActivity(state, {
11569
13210
  type: "info",