@evident-ai/cli 3.4.1-dev.7393414 → 3.4.1-dev.7703679

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
@@ -1138,6 +1138,37 @@ var TelemetryEventTypes = {
1138
1138
  RUNNER_ACTIVITY: "runner.activity"
1139
1139
  };
1140
1140
 
1141
+ // ../../packages/types/src/tunnel/binary-frame.ts
1142
+ var BINARY_FRAME_REQ_DATA = 1;
1143
+ var BINARY_FRAME_RES_DATA = 2;
1144
+ var textEncoder = new TextEncoder();
1145
+ var textDecoder = new TextDecoder();
1146
+ function encodeBinaryBodyFrame(type, sid, payload) {
1147
+ const sidBytes = textEncoder.encode(sid);
1148
+ if (sidBytes.length === 0 || sidBytes.length > 255) {
1149
+ throw new RangeError("sid must contain between 1 and 255 UTF-8 bytes");
1150
+ }
1151
+ const frame = new Uint8Array(2 + sidBytes.length + payload.length);
1152
+ frame[0] = type;
1153
+ frame[1] = sidBytes.length;
1154
+ frame.set(sidBytes, 2);
1155
+ frame.set(payload, 2 + sidBytes.length);
1156
+ return frame;
1157
+ }
1158
+ function decodeBinaryBodyFrame(bytes) {
1159
+ if (bytes.length < 2) return null;
1160
+ const type = bytes[0];
1161
+ if (type !== BINARY_FRAME_REQ_DATA && type !== BINARY_FRAME_RES_DATA) return null;
1162
+ const sidLen = bytes[1];
1163
+ if (sidLen === 0 || bytes.length < 2 + sidLen) return null;
1164
+ const payloadOffset = 2 + sidLen;
1165
+ return {
1166
+ type,
1167
+ sid: textDecoder.decode(bytes.subarray(2, payloadOffset)),
1168
+ payload: bytes.subarray(payloadOffset)
1169
+ };
1170
+ }
1171
+
1141
1172
  // ../../packages/types/src/tunnel/index.ts
1142
1173
  var MAX_FRAME_BYTES = 256 * 1024;
1143
1174
  var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
@@ -1178,11 +1209,11 @@ function stripQuery(url) {
1178
1209
  }
1179
1210
 
1180
1211
  // src/commands/run.ts
1181
- import ora3 from "ora";
1212
+ import ora4 from "ora";
1182
1213
  import { select as select4 } from "@inquirer/prompts";
1183
1214
 
1184
1215
  // src/lib/telemetry.ts
1185
- var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
1216
+ var CLI_VERSION = (true ? "3.4.1-dev.7703679" : void 0) ?? process.env.npm_package_version ?? "unknown";
1186
1217
  function getCliVersion() {
1187
1218
  return CLI_VERSION;
1188
1219
  }
@@ -1656,6 +1687,11 @@ function isSessionDbRecoveryRecord(value) {
1656
1687
  );
1657
1688
  }
1658
1689
 
1690
+ // src/lib/opencode/auth.ts
1691
+ function buildOpenCodeBasicAuthHeader(password) {
1692
+ return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
1693
+ }
1694
+
1659
1695
  // src/lib/opencode/health.ts
1660
1696
  async function checkOpenCodeHealth(port) {
1661
1697
  try {
@@ -1673,6 +1709,27 @@ async function checkOpenCodeHealth(port) {
1673
1709
  return { healthy: false, error: message };
1674
1710
  }
1675
1711
  }
1712
+ async function checkOpenCode2Health(port, password) {
1713
+ try {
1714
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
1715
+ headers: {
1716
+ Authorization: buildOpenCodeBasicAuthHeader(password)
1717
+ },
1718
+ signal: AbortSignal.timeout(2e3)
1719
+ });
1720
+ if (response.status === 401) {
1721
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
1722
+ }
1723
+ if (!response.ok) {
1724
+ return { healthy: false, error: `HTTP ${response.status}` };
1725
+ }
1726
+ const data = await response.json().catch(() => ({}));
1727
+ return { healthy: true, version: data.version };
1728
+ } catch (error2) {
1729
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1730
+ return { healthy: false, error: message };
1731
+ }
1732
+ }
1676
1733
  async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1677
1734
  const startTime = Date.now();
1678
1735
  while (Date.now() - startTime < timeoutMs) {
@@ -1684,6 +1741,61 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1684
1741
  }
1685
1742
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1686
1743
  }
1744
+ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1745
+ const startTime = Date.now();
1746
+ while (Date.now() - startTime < timeoutMs) {
1747
+ const health = await checkOpenCode2Health(port, password);
1748
+ if (health.healthy || health.authFailed) {
1749
+ return health;
1750
+ }
1751
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
1752
+ }
1753
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1754
+ }
1755
+
1756
+ // src/lib/http-timeout.ts
1757
+ var REQUEST_TIMEOUT_MS = 6e4;
1758
+ function withRequestTimeout(fetchImpl, timeoutMs) {
1759
+ return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
1760
+ }
1761
+
1762
+ // src/lib/opencode/client.ts
1763
+ function redactPassword(message, password) {
1764
+ return message.replaceAll(password, "[redacted]");
1765
+ }
1766
+ function createOpenCodeClient(options) {
1767
+ const password = options.password ?? null;
1768
+ const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
1769
+ const baseUrl = `http://127.0.0.1:${options.port}`;
1770
+ return {
1771
+ port: options.port,
1772
+ version: options.version,
1773
+ password,
1774
+ async request(path, init, requestOptions) {
1775
+ const requestInit = options.version === "v2" && password !== null ? (() => {
1776
+ const headers = new Headers(init?.headers);
1777
+ headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
1778
+ return { ...init, headers };
1779
+ })() : init;
1780
+ try {
1781
+ const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
1782
+ if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
1783
+ const body = await response.text();
1784
+ throw new Error(
1785
+ `OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
1786
+ );
1787
+ }
1788
+ return response;
1789
+ } catch (error2) {
1790
+ if (options.version === "v2" && password !== null) {
1791
+ const message = error2 instanceof Error ? error2.message : String(error2);
1792
+ throw new Error(redactPassword(message, password));
1793
+ }
1794
+ throw error2;
1795
+ }
1796
+ }
1797
+ };
1798
+ }
1687
1799
 
1688
1800
  // src/lib/opencode/session-db-boot.ts
1689
1801
  import { spawn as spawn2 } from "node:child_process";
@@ -2347,15 +2459,23 @@ function isQueueValidatedVersion(version2) {
2347
2459
  if (!version2) return false;
2348
2460
  return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
2349
2461
  }
2350
- function buildOpenCodeVersionWarning(version2) {
2351
- if (isQueueValidatedVersion(version2)) return null;
2352
- const detected = version2 ? `v${version2}` : "unknown";
2462
+ function buildOpenCodeVersionWarning(version2, major) {
2463
+ if (major === "v2") return null;
2353
2464
  const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
2354
- 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.`;
2465
+ if (!version2) {
2466
+ 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.`;
2467
+ }
2468
+ if (isQueueValidatedVersion(version2)) return null;
2469
+ 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.`;
2470
+ }
2471
+ function reportedOpenCodeVersion(input) {
2472
+ if (!input.connected) return null;
2473
+ return input.version || `${input.major}-unknown`;
2355
2474
  }
2356
2475
 
2357
2476
  // src/lib/opencode/process.ts
2358
2477
  import { execSync, spawn as spawn3 } from "child_process";
2478
+ import { randomBytes } from "node:crypto";
2359
2479
 
2360
2480
  // src/lib/process-stop.ts
2361
2481
  async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
@@ -2414,6 +2534,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2414
2534
  // src/lib/opencode/process.ts
2415
2535
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2416
2536
  var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
2537
+ var VALID_OPENCODE2_LOG_LEVELS = /* @__PURE__ */ new Set([
2538
+ "all",
2539
+ "trace",
2540
+ "debug",
2541
+ "info",
2542
+ "warn",
2543
+ "warning",
2544
+ "error",
2545
+ "fatal",
2546
+ "none"
2547
+ ]);
2417
2548
  function resolveOpenCodeLogLevel(env) {
2418
2549
  const raw = env.OPENCODE_LOG_LEVEL;
2419
2550
  if (!raw) return "INFO";
@@ -2424,6 +2555,16 @@ function resolveOpenCodeLogLevel(env) {
2424
2555
  );
2425
2556
  return "INFO";
2426
2557
  }
2558
+ function resolveOpenCode2LogLevel(env) {
2559
+ const raw = env.OPENCODE_LOG_LEVEL;
2560
+ if (!raw) return "info";
2561
+ const lower = raw.toLowerCase();
2562
+ if (VALID_OPENCODE2_LOG_LEVELS.has(lower)) return lower;
2563
+ console.warn(
2564
+ `startOpenCode2: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected all|trace|debug|info|warn|warning|error|fatal|none) \u2014 using info`
2565
+ );
2566
+ return "info";
2567
+ }
2427
2568
  function getProcessCwd(pid) {
2428
2569
  const platform = process.platform;
2429
2570
  try {
@@ -2606,6 +2747,37 @@ async function startOpenCode(port, options = {}) {
2606
2747
  });
2607
2748
  return child;
2608
2749
  }
2750
+ async function startOpenCode2(port, options = {}) {
2751
+ const password = randomBytes(24).toString("hex");
2752
+ let command = "opencode2";
2753
+ const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
2754
+ let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
2755
+ try {
2756
+ execSync("which opencode2", { stdio: "ignore" });
2757
+ } catch {
2758
+ command = "npx";
2759
+ args = [
2760
+ "-y",
2761
+ "-p",
2762
+ "@opencode-ai/cli@beta",
2763
+ "--",
2764
+ "opencode2",
2765
+ "serve",
2766
+ "--port",
2767
+ port.toString(),
2768
+ "--hostname",
2769
+ "127.0.0.1",
2770
+ ...logLevel
2771
+ ];
2772
+ }
2773
+ const child = spawn3(command, args, {
2774
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
2775
+ detached: true,
2776
+ stdio: options.inheritStdio ? "inherit" : "ignore",
2777
+ cwd: process.cwd()
2778
+ });
2779
+ return { child, password };
2780
+ }
2609
2781
  function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
2610
2782
  const sendSignal = (signal) => {
2611
2783
  if (process.platform === "win32") {
@@ -2753,27 +2925,500 @@ function buildNoProviderWarning(hasProvider) {
2753
2925
  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).";
2754
2926
  }
2755
2927
 
2756
- // src/lib/http-timeout.ts
2757
- var REQUEST_TIMEOUT_MS = 6e4;
2758
- function withRequestTimeout(fetchImpl, timeoutMs) {
2759
- return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
2928
+ // src/lib/opencode/session-v2.ts
2929
+ function isRecord(value) {
2930
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2931
+ }
2932
+ function finiteNumber(value) {
2933
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2934
+ }
2935
+ function adaptTime(value) {
2936
+ if (!isRecord(value)) return void 0;
2937
+ const created = finiteNumber(value.created);
2938
+ const completed = finiteNumber(value.completed);
2939
+ if (created === void 0 && completed === void 0) return void 0;
2940
+ return {
2941
+ ...created !== void 0 ? { created } : {},
2942
+ ...completed !== void 0 ? { completed } : {}
2943
+ };
2944
+ }
2945
+ function adaptTokens(value) {
2946
+ if (!isRecord(value)) return void 0;
2947
+ const input = finiteNumber(value.input);
2948
+ const output = finiteNumber(value.output);
2949
+ const reasoning = finiteNumber(value.reasoning);
2950
+ const cache = isRecord(value.cache) ? {
2951
+ ...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
2952
+ ...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
2953
+ } : void 0;
2954
+ if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
2955
+ return void 0;
2956
+ }
2957
+ return {
2958
+ ...input !== void 0 ? { input } : {},
2959
+ ...output !== void 0 ? { output } : {},
2960
+ ...reasoning !== void 0 ? { reasoning } : {},
2961
+ ...cache ? { cache } : {}
2962
+ };
2963
+ }
2964
+ function adaptMessageInfo(value, role) {
2965
+ const info = {
2966
+ id: value.id,
2967
+ role
2968
+ };
2969
+ const time = adaptTime(value.time);
2970
+ if (time) info.time = time;
2971
+ if (typeof value.finish === "string") info.finish = value.finish;
2972
+ if ("error" in value) info.error = value.error;
2973
+ if (typeof value.agent === "string") info.agent = value.agent;
2974
+ if (isRecord(value.model)) {
2975
+ if (typeof value.model.id === "string") info.modelID = value.model.id;
2976
+ if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
2977
+ }
2978
+ if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
2979
+ const tokens = adaptTokens(value.tokens);
2980
+ if (tokens) info.tokens = tokens;
2981
+ return info;
2982
+ }
2983
+ function adaptV2Message(value) {
2984
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
2985
+ return null;
2986
+ }
2987
+ if (value.type === "user") {
2988
+ if (typeof value.text !== "string") return null;
2989
+ return {
2990
+ info: adaptMessageInfo(value, "user"),
2991
+ parts: [{ type: "text", text: value.text }]
2992
+ };
2993
+ }
2994
+ if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
2995
+ const parts = [];
2996
+ for (const content of value.content) {
2997
+ if (!isRecord(content) || typeof content.type !== "string") return null;
2998
+ if (content.type === "text") {
2999
+ if (typeof content.text !== "string") return null;
3000
+ parts.push({ type: "text", text: content.text });
3001
+ } else {
3002
+ parts.push({ type: content.type });
3003
+ }
3004
+ }
3005
+ return {
3006
+ info: adaptMessageInfo(value, "assistant"),
3007
+ parts
3008
+ };
3009
+ }
3010
+ function adaptFormTool(value) {
3011
+ if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
3012
+ return void 0;
3013
+ }
3014
+ return { messageID: value.messageID, callID: value.id };
3015
+ }
3016
+ function adaptV2FormWire(value) {
3017
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
3018
+ return null;
3019
+ }
3020
+ return value;
3021
+ }
3022
+ function adaptV2FormField(value, header) {
3023
+ if (!isRecord(value)) return null;
3024
+ const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
3025
+ if (!question) return null;
3026
+ const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
3027
+ if (!isRecord(option)) return [];
3028
+ const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
3029
+ if (!label) return [];
3030
+ return [
3031
+ {
3032
+ label,
3033
+ description: typeof option.description === "string" ? option.description : ""
3034
+ }
3035
+ ];
3036
+ }) : [];
3037
+ return { question, header, options };
3038
+ }
3039
+ function adaptV2Form(value) {
3040
+ const form = adaptV2FormWire(value);
3041
+ if (!form || !Array.isArray(form.fields)) return null;
3042
+ const header = typeof form.title === "string" ? form.title : "";
3043
+ const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
3044
+ if (questions.length === 0) return null;
3045
+ const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
3046
+ return {
3047
+ id: form.id,
3048
+ sessionID: form.sessionID,
3049
+ questions,
3050
+ ...tool ? { tool } : {},
3051
+ raw: form
3052
+ };
3053
+ }
3054
+ function adaptV2FormList(value) {
3055
+ if (!isRecord(value) || !Array.isArray(value.data)) return null;
3056
+ return value.data.map(adaptV2Form).filter((form) => form !== null);
3057
+ }
3058
+ function adaptPattern(value) {
3059
+ if (typeof value === "string" && value.length > 0) return value;
3060
+ if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
3061
+ return value;
3062
+ }
3063
+ return void 0;
3064
+ }
3065
+ function adaptV2PermissionWire(value) {
3066
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
3067
+ return null;
3068
+ }
3069
+ if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
3070
+ return value;
3071
+ }
3072
+ function adaptV2Permission(value) {
3073
+ const permission = adaptV2PermissionWire(value);
3074
+ if (!permission) return null;
3075
+ const type = permission.permission ?? permission.action;
3076
+ if (!type) return null;
3077
+ const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
3078
+ const time = isRecord(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
3079
+ return {
3080
+ id: permission.id,
3081
+ type,
3082
+ sessionID: permission.sessionID,
3083
+ metadata: isRecord(permission.metadata) ? permission.metadata : {},
3084
+ raw: permission,
3085
+ ...pattern !== void 0 ? { pattern } : {},
3086
+ ...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
3087
+ ...typeof permission.callID === "string" ? { callID: permission.callID } : {},
3088
+ ...typeof permission.title === "string" ? { title: permission.title } : {},
3089
+ ...time ? { time } : {}
3090
+ };
3091
+ }
3092
+ function adaptV2PermissionList(value) {
3093
+ if (!isRecord(value) || !Array.isArray(value.data)) return null;
3094
+ return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
3095
+ }
3096
+ function adaptV2Session(value) {
3097
+ if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
3098
+ const time = isRecord(value.time) ? {
3099
+ ...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
3100
+ ...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
3101
+ } : void 0;
3102
+ return {
3103
+ id: value.id,
3104
+ ...typeof value.title === "string" ? { title: value.title } : {},
3105
+ ...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
3106
+ ...time && Object.keys(time).length > 0 ? { time } : {}
3107
+ };
3108
+ }
3109
+ function adaptV2SessionList(value) {
3110
+ if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
3111
+ return {
3112
+ data: value.data.map(adaptV2Session).filter((session) => session !== null),
3113
+ cursor: value.cursor
3114
+ };
3115
+ }
3116
+ function adaptV2Location(value) {
3117
+ const candidates = [
3118
+ value,
3119
+ isRecord(value) ? value.data : void 0,
3120
+ isRecord(value) ? value.location : void 0
3121
+ ];
3122
+ for (const candidate of candidates) {
3123
+ if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
3124
+ const directory = candidate.directory.trim();
3125
+ if (directory) return directory;
3126
+ }
3127
+ return null;
3128
+ }
3129
+ function adaptV2Messages(value) {
3130
+ if (!isRecord(value) || !Array.isArray(value.data)) return [];
3131
+ return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
3132
+ }
3133
+ async function readJson(response) {
3134
+ try {
3135
+ return await response.json();
3136
+ } catch (error2) {
3137
+ throw new Error(
3138
+ `OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
3139
+ );
3140
+ }
3141
+ }
3142
+ async function readData(client, path, init) {
3143
+ const response = await client.request(path, init);
3144
+ const body = await readJson(response);
3145
+ if (!isRecord(body) || !("data" in body)) {
3146
+ throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
3147
+ }
3148
+ return body.data;
3149
+ }
3150
+ var OpenCodeV2PromptAckError = class extends Error {
3151
+ constructor(message) {
3152
+ super(message);
3153
+ this.name = "OpenCodeV2PromptAckError";
3154
+ }
3155
+ };
3156
+ async function getOpenCodeDirectoryV2(client) {
3157
+ try {
3158
+ return adaptV2Location(await readJson(await client.request("/api/location")));
3159
+ } catch (error2) {
3160
+ console.error(
3161
+ `[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3162
+ );
3163
+ return null;
3164
+ }
3165
+ }
3166
+ async function createV2Session(client, directory) {
3167
+ const data = await readData(client, "/api/session", {
3168
+ method: "POST",
3169
+ headers: { "Content-Type": "application/json" },
3170
+ body: JSON.stringify({ location: { directory } })
3171
+ });
3172
+ if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
3173
+ throw new Error("OpenCode V2 create session response was missing data.id");
3174
+ }
3175
+ return data.id;
3176
+ }
3177
+ async function getV2Session(client, sessionId) {
3178
+ const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
3179
+ const session = adaptV2Session(data);
3180
+ if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
3181
+ return session;
3182
+ }
3183
+ async function listV2SessionPage(client, cursor) {
3184
+ const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
3185
+ try {
3186
+ return adaptV2SessionList(await readJson(await client.request(path)));
3187
+ } catch (error2) {
3188
+ console.error(
3189
+ `[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3190
+ );
3191
+ return null;
3192
+ }
3193
+ }
3194
+ async function listV2Sessions(client) {
3195
+ const sessions = [];
3196
+ const seenCursors = /* @__PURE__ */ new Set();
3197
+ let cursor;
3198
+ let hasNextPage = true;
3199
+ try {
3200
+ while (hasNextPage) {
3201
+ const page = await listV2SessionPage(client, cursor);
3202
+ if (!page) return null;
3203
+ sessions.push(...page.data);
3204
+ const next = page.cursor.next;
3205
+ if (next === void 0 || next === null) {
3206
+ hasNextPage = false;
3207
+ continue;
3208
+ }
3209
+ if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
3210
+ throw new Error("OpenCode V2 session list contained an invalid next cursor");
3211
+ }
3212
+ seenCursors.add(next);
3213
+ cursor = next;
3214
+ }
3215
+ return sessions;
3216
+ } catch (error2) {
3217
+ console.error(
3218
+ `[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3219
+ );
3220
+ return null;
3221
+ }
3222
+ }
3223
+ async function deleteV2Session(client, sessionId) {
3224
+ try {
3225
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3226
+ return true;
3227
+ } catch (error2) {
3228
+ console.error(
3229
+ `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3230
+ );
3231
+ return false;
3232
+ }
3233
+ }
3234
+ async function v2SessionExists(client, sessionId) {
3235
+ try {
3236
+ const response = await client.request(
3237
+ `/api/session/${encodeURIComponent(sessionId)}`,
3238
+ void 0,
3239
+ { allowStatuses: [404] }
3240
+ );
3241
+ return response.status === 404 ? false : true;
3242
+ } catch (error2) {
3243
+ console.error(
3244
+ `[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3245
+ );
3246
+ return null;
3247
+ }
3248
+ }
3249
+ async function sendV2Prompt(client, sessionId, text) {
3250
+ const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
3251
+ const response = await client.request(path, {
3252
+ method: "POST",
3253
+ headers: { "Content-Type": "application/json" },
3254
+ body: JSON.stringify({ text, delivery: "queue" })
3255
+ });
3256
+ let body;
3257
+ try {
3258
+ body = await readJson(response);
3259
+ } catch (error2) {
3260
+ throw new OpenCodeV2PromptAckError(
3261
+ `OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
3262
+ );
3263
+ }
3264
+ const data = isRecord(body) && "data" in body ? body.data : void 0;
3265
+ if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
3266
+ throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
3267
+ }
3268
+ return data.id;
3269
+ }
3270
+ async function getV2SessionMessages(client, sessionId) {
3271
+ const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
3272
+ try {
3273
+ const body = await readJson(await client.request(path));
3274
+ if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
3275
+ return adaptV2Messages(body);
3276
+ } catch (error2) {
3277
+ console.error(
3278
+ `[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3279
+ );
3280
+ return null;
3281
+ }
3282
+ }
3283
+ async function listV2Forms(client, sessionId) {
3284
+ const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
3285
+ try {
3286
+ return adaptV2FormList(await readJson(await client.request(path)));
3287
+ } catch (error2) {
3288
+ console.error(
3289
+ `[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3290
+ );
3291
+ return null;
3292
+ }
3293
+ }
3294
+ async function listV2Permissions(client, sessionId) {
3295
+ const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
3296
+ try {
3297
+ return adaptV2PermissionList(await readJson(await client.request(path)));
3298
+ } catch (error2) {
3299
+ console.error(
3300
+ `[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3301
+ );
3302
+ return null;
3303
+ }
3304
+ }
3305
+ async function getV2ActiveSessions(client) {
3306
+ try {
3307
+ const body = await readJson(await client.request("/api/session/active"));
3308
+ if (!isRecord(body) || !isRecord(body.data)) return null;
3309
+ return body.data;
3310
+ } catch (error2) {
3311
+ console.error(
3312
+ `[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3313
+ );
3314
+ return null;
3315
+ }
3316
+ }
3317
+ async function isV2SessionOngoing(client, sessionId) {
3318
+ const activeSessions = await getV2ActiveSessions(client);
3319
+ if (activeSessions === null) return null;
3320
+ return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
3321
+ }
3322
+ function sessionErrorReason(value) {
3323
+ if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
3324
+ if (isRecord(value)) {
3325
+ const data = isRecord(value.data) ? value.data : void 0;
3326
+ const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
3327
+ if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
3328
+ }
3329
+ return "OpenCode reported a session error with no details";
3330
+ }
3331
+ function adaptV2SessionErrorEvent(value) {
3332
+ let parsed = value;
3333
+ if (typeof value === "string") {
3334
+ try {
3335
+ parsed = JSON.parse(value);
3336
+ } catch (error2) {
3337
+ void error2;
3338
+ return null;
3339
+ }
3340
+ }
3341
+ if (!isRecord(parsed)) return null;
3342
+ try {
3343
+ const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
3344
+ if (establishedShape) return establishedShape;
3345
+ } catch (error2) {
3346
+ void error2;
3347
+ }
3348
+ const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
3349
+ for (const event of candidates) {
3350
+ if (event.type !== "session.error") continue;
3351
+ const properties = [event.properties, event.data, event].find(isRecord);
3352
+ if (!properties) continue;
3353
+ const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
3354
+ if (!sessionId) continue;
3355
+ return {
3356
+ sessionId,
3357
+ reason: sessionErrorReason(properties.error ?? properties)
3358
+ };
3359
+ }
3360
+ return null;
3361
+ }
3362
+ async function readV2SessionErrorStream(client, options) {
3363
+ let reader = null;
3364
+ try {
3365
+ const response = await client.request("/api/event", {
3366
+ headers: { accept: "text/event-stream" },
3367
+ signal: options.signal
3368
+ });
3369
+ if (!response.ok || !response.body) {
3370
+ return { reason: "unavailable", detail: `HTTP ${response.status}` };
3371
+ }
3372
+ reader = response.body.getReader();
3373
+ const decoder = new TextDecoder();
3374
+ let buffer = "";
3375
+ const processLine = (line) => {
3376
+ const trimmed = line.trimEnd();
3377
+ if (!trimmed.startsWith("data:")) return;
3378
+ const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
3379
+ if (event) options.onSessionError(event);
3380
+ };
3381
+ while (true) {
3382
+ const { done, value } = await reader.read();
3383
+ if (done) return { reason: "ended" };
3384
+ buffer += decoder.decode(value, { stream: true });
3385
+ const lines = buffer.split("\n");
3386
+ buffer = lines.pop() ?? "";
3387
+ for (const line of lines) processLine(line);
3388
+ }
3389
+ } catch (error2) {
3390
+ if (options.signal.aborted) return { reason: "aborted" };
3391
+ return {
3392
+ reason: "unavailable",
3393
+ detail: error2 instanceof Error ? error2.message : String(error2)
3394
+ };
3395
+ } finally {
3396
+ if (reader) void reader.cancel().catch(() => void 0);
3397
+ }
2760
3398
  }
2761
3399
 
2762
3400
  // src/lib/opencode/session.ts
3401
+ var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
2763
3402
  function timedFetch(input, init) {
2764
3403
  return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
2765
3404
  }
3405
+ function requestWithClient(port, client, path, init, options) {
3406
+ return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
3407
+ }
2766
3408
  function opencodeBase(port) {
2767
3409
  return `http://127.0.0.1:${port}`;
2768
3410
  }
2769
- async function getOpenCodeDirectory(port) {
3411
+ async function getOpenCodeDirectory(port, client) {
2770
3412
  try {
2771
- const res = await timedFetch(`${opencodeBase(port)}/path`);
3413
+ const res = await requestWithClient(port, client, "/path");
2772
3414
  if (!res.ok) return null;
2773
3415
  const body = await res.json();
2774
3416
  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;
2775
3417
  return dir && dir.trim() ? dir.trim() : null;
2776
- } catch {
3418
+ } catch (error2) {
3419
+ console.error(
3420
+ `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3421
+ );
2777
3422
  return null;
2778
3423
  }
2779
3424
  }
@@ -2817,16 +3462,48 @@ function isAssistantInFlight(m) {
2817
3462
  if (completedOf(m) == null) return true;
2818
3463
  return finishOf(m) === "tool-calls";
2819
3464
  }
2820
- async function getSessionMessages(port, sessionId) {
3465
+ async function getSessionMessages(port, sessionId, client) {
2821
3466
  try {
2822
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/message`);
3467
+ const path = `/session/${sessionId}/message`;
3468
+ const res = await requestWithClient(port, client, path);
2823
3469
  if (!res.ok) return null;
2824
3470
  const body = await res.json();
2825
3471
  return Array.isArray(body) ? body : null;
2826
- } catch {
3472
+ } catch (error2) {
3473
+ console.error(
3474
+ `[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3475
+ );
2827
3476
  return null;
2828
3477
  }
2829
3478
  }
3479
+ async function fetchSessionMessages(port, sessionId, client) {
3480
+ const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
3481
+ if (!response.ok) return null;
3482
+ const body = await response.json();
3483
+ return Array.isArray(body) ? body : null;
3484
+ }
3485
+ async function pollSessionMessagesForRedrive(port, sessionId, client) {
3486
+ try {
3487
+ const response = await requestWithClient(
3488
+ port,
3489
+ client,
3490
+ `/session/${sessionId}/message`,
3491
+ void 0,
3492
+ { allowStatuses: ALL_HTTP_STATUSES }
3493
+ );
3494
+ if (!response.ok) {
3495
+ return { ok: false, status: response.status, body: await response.text(), malformed: false };
3496
+ }
3497
+ const body = await response.json();
3498
+ if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
3499
+ return { ok: true, messages: body };
3500
+ } catch (error2) {
3501
+ console.error(
3502
+ `[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3503
+ );
3504
+ return { ok: false, status: null, body: "", malformed: false };
3505
+ }
3506
+ }
2830
3507
  function isSessionActivelyGenerating(messages) {
2831
3508
  if (!messages || messages.length === 0) return false;
2832
3509
  const last = messages[messages.length - 1];
@@ -2847,27 +3524,37 @@ function sessionLastActivityMs(session) {
2847
3524
  }
2848
3525
  return null;
2849
3526
  }
2850
- async function listSessions(port) {
3527
+ async function listSessions(port, client) {
3528
+ if (client?.version === "v2") return listV2Sessions(client);
2851
3529
  try {
2852
- const res = await timedFetch(`${opencodeBase(port)}/session`);
3530
+ const res = await requestWithClient(port, client, "/session");
2853
3531
  if (!res.ok) return null;
2854
3532
  const body = await res.json();
2855
3533
  return Array.isArray(body) ? body : null;
2856
- } catch {
3534
+ } catch (error2) {
3535
+ console.error(
3536
+ `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3537
+ );
2857
3538
  return null;
2858
3539
  }
2859
3540
  }
2860
- async function deleteSession(port, id) {
3541
+ async function deleteSession(port, id, client) {
3542
+ if (client?.version === "v2") return deleteV2Session(client, id);
2861
3543
  try {
2862
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`, { method: "DELETE" });
3544
+ const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2863
3545
  return res.status >= 200 && res.status < 300;
2864
- } catch {
3546
+ } catch (error2) {
3547
+ console.error(
3548
+ `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3549
+ );
2865
3550
  return false;
2866
3551
  }
2867
3552
  }
2868
- async function sessionExists(port, id) {
3553
+ async function sessionExists(port, id, client) {
2869
3554
  try {
2870
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`);
3555
+ const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
3556
+ allowStatuses: [404]
3557
+ });
2871
3558
  if (res.status >= 200 && res.status < 300) return true;
2872
3559
  if (res.status === 404) return false;
2873
3560
  return null;
@@ -2875,9 +3562,22 @@ async function sessionExists(port, id) {
2875
3562
  return null;
2876
3563
  }
2877
3564
  }
2878
- async function getSessionStatuses(port) {
3565
+ async function getOpenCodeSession(port, id, client) {
3566
+ try {
3567
+ const response = await requestWithClient(port, client, `/session/${id}`);
3568
+ const body = await response.json();
3569
+ return body && typeof body === "object" && !Array.isArray(body) ? body : null;
3570
+ } catch (error2) {
3571
+ console.error(
3572
+ `[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3573
+ );
3574
+ return null;
3575
+ }
3576
+ }
3577
+ async function getSessionStatuses(port, client) {
3578
+ if (client?.version === "v2") return null;
2879
3579
  try {
2880
- const res = await timedFetch(`${opencodeBase(port)}/session/status`);
3580
+ const res = await requestWithClient(port, client, "/session/status");
2881
3581
  if (!res.ok) {
2882
3582
  console.error(
2883
3583
  `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
@@ -2899,22 +3599,28 @@ async function getSessionStatuses(port) {
2899
3599
  return null;
2900
3600
  }
2901
3601
  }
2902
- async function isSessionOngoing(port, id) {
2903
- const map = await getSessionStatuses(port);
3602
+ async function isSessionOngoing(port, id, client) {
3603
+ if (client?.version === "v2") return isV2SessionOngoing(client, id);
3604
+ const map = await getSessionStatuses(port, client);
2904
3605
  if (map == null) return null;
2905
3606
  const entry = map[id];
2906
3607
  return entry != null && entry.type !== "idle";
2907
3608
  }
2908
- async function createOpenCodeSession(port, directory) {
2909
- const url = new URL(`${opencodeBase(port)}/session`);
2910
- if (directory && directory.trim()) {
2911
- url.searchParams.set("directory", directory.trim());
2912
- }
2913
- const response = await timedFetch(url, {
2914
- method: "POST",
2915
- headers: { "Content-Type": "application/json" },
2916
- body: JSON.stringify({})
2917
- });
3609
+ async function createOpenCodeSession(port, directory, client) {
3610
+ const path = new URL(`${opencodeBase(port)}/session`);
3611
+ if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
3612
+ const requestPath = `${path.pathname}${path.search}`;
3613
+ const response = await requestWithClient(
3614
+ port,
3615
+ client,
3616
+ requestPath,
3617
+ {
3618
+ method: "POST",
3619
+ headers: { "Content-Type": "application/json" },
3620
+ body: JSON.stringify({})
3621
+ },
3622
+ { allowStatuses: ALL_HTTP_STATUSES }
3623
+ );
2918
3624
  if (!response.ok) {
2919
3625
  const text = await response.text().catch(() => "");
2920
3626
  throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
@@ -2922,10 +3628,16 @@ async function createOpenCodeSession(port, directory) {
2922
3628
  const data = await response.json();
2923
3629
  return data.id;
2924
3630
  }
2925
- async function getModelAttachmentCapability(port, model) {
3631
+ async function getModelAttachmentCapability(port, model, client) {
2926
3632
  const { model: baseModel } = splitModelVariant(model);
3633
+ if (client?.version === "v2") {
3634
+ console.error(
3635
+ `[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
3636
+ );
3637
+ return null;
3638
+ }
2927
3639
  try {
2928
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
3640
+ const res = await requestWithClient(port, client, "/config/providers");
2929
3641
  if (!res.ok) {
2930
3642
  console.error(
2931
3643
  `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3045,19 +3757,43 @@ function applyModelOptions(body, options) {
3045
3757
  }
3046
3758
  if (variant) body.variant = variant;
3047
3759
  }
3760
+ async function listOpenCodeQuestions(port, client) {
3761
+ try {
3762
+ const response = await requestWithClient(port, client, "/question");
3763
+ const body = await response.json();
3764
+ return Array.isArray(body) ? body : null;
3765
+ } catch (error2) {
3766
+ console.error(
3767
+ `[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3768
+ );
3769
+ return null;
3770
+ }
3771
+ }
3772
+ async function listOpenCodePermissions(port, client) {
3773
+ try {
3774
+ const response = await requestWithClient(port, client, "/permission");
3775
+ const body = await response.json();
3776
+ return Array.isArray(body) ? body : null;
3777
+ } catch (error2) {
3778
+ console.error(
3779
+ `[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3780
+ );
3781
+ return null;
3782
+ }
3783
+ }
3048
3784
  function messageText(m) {
3049
3785
  if (!m || !Array.isArray(m.parts)) return "";
3050
3786
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
3051
3787
  }
3052
- async function sendPromptAsync(port, sessionId, content, options, attachments) {
3053
- const before = await getSessionMessages(port, sessionId);
3788
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3789
+ const before = await getSessionMessages(port, sessionId, client);
3054
3790
  const knownUserIds = new Set(
3055
3791
  (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3056
3792
  );
3057
3793
  const parts = [{ type: "text", text: content }];
3058
3794
  let pendingOutcomes = null;
3059
3795
  if (attachments && attachments.inputs.length > 0) {
3060
- const capable = await getModelAttachmentCapability(port, options?.model);
3796
+ const capable = await getModelAttachmentCapability(port, options?.model, client);
3061
3797
  const {
3062
3798
  parts: fileParts,
3063
3799
  outcomes,
@@ -3070,11 +3806,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3070
3806
  parts
3071
3807
  };
3072
3808
  applyModelOptions(body, options);
3073
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
3074
- method: "POST",
3075
- headers: { "Content-Type": "application/json" },
3076
- body: JSON.stringify(body)
3077
- });
3809
+ const res = await requestWithClient(
3810
+ port,
3811
+ client,
3812
+ `/session/${sessionId}/prompt_async`,
3813
+ {
3814
+ method: "POST",
3815
+ headers: { "Content-Type": "application/json" },
3816
+ body: JSON.stringify(body)
3817
+ },
3818
+ { allowStatuses: ALL_HTTP_STATUSES }
3819
+ );
3078
3820
  if (res.status < 200 || res.status >= 300) {
3079
3821
  const text = await res.text().catch(() => "");
3080
3822
  const { variant } = splitModelVariant(options?.model);
@@ -3085,7 +3827,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3085
3827
  const READ_BACK_ATTEMPTS = 5;
3086
3828
  const READ_BACK_DELAY_MS = 150;
3087
3829
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
3088
- const after = await getSessionMessages(port, sessionId);
3830
+ const after = await getSessionMessages(port, sessionId, client);
3089
3831
  if (after) {
3090
3832
  let best = null;
3091
3833
  for (const m of after) {
@@ -3188,7 +3930,7 @@ function collectSubagentSessions(messages, userMessageId) {
3188
3930
  }
3189
3931
  return refs;
3190
3932
  }
3191
- function finiteNumber(value) {
3933
+ function finiteNumber2(value) {
3192
3934
  return typeof value === "number" && Number.isFinite(value) ? value : null;
3193
3935
  }
3194
3936
  function taskCallModel(value) {
@@ -3217,8 +3959,8 @@ function collectTaskCalls(messages, userMessageId) {
3217
3959
  parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3218
3960
  model: taskCallModel(metadata?.model),
3219
3961
  status: part.state.status ?? "unknown",
3220
- timeStart: finiteNumber(part.state.time?.start),
3221
- timeEnd: finiteNumber(part.state.time?.end)
3962
+ timeStart: finiteNumber2(part.state.time?.start),
3963
+ timeEnd: finiteNumber2(part.state.time?.end)
3222
3964
  });
3223
3965
  }
3224
3966
  }
@@ -3233,7 +3975,7 @@ function attributeTaskCallUsage(messages, windows) {
3233
3975
  const unattributed = [];
3234
3976
  for (const message of messages ?? []) {
3235
3977
  if (roleOf(message) !== "assistant") continue;
3236
- const created = finiteNumber(createdOf(message));
3978
+ const created = finiteNumber2(createdOf(message));
3237
3979
  const matching = created === null ? [] : eligibleWindows.filter(
3238
3980
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3239
3981
  );
@@ -3472,11 +4214,53 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
3472
4214
  hasStartedLaterUser = true;
3473
4215
  }
3474
4216
  }
3475
- return hasLaterUser && hasStartedLaterUser;
3476
- }
3477
- async function hasAnyConfiguredProvider(port) {
4217
+ return hasLaterUser && hasStartedLaterUser;
4218
+ }
4219
+ async function hasAnyConfiguredProvider(port, client) {
4220
+ if (client?.version === "v2") {
4221
+ const directory = await getOpenCodeDirectoryV2(client);
4222
+ if (!directory) {
4223
+ console.error(
4224
+ `[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
4225
+ );
4226
+ return null;
4227
+ }
4228
+ const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4229
+ try {
4230
+ const res = await client.request(path);
4231
+ if (!res.ok) {
4232
+ console.error(
4233
+ `[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
4234
+ );
4235
+ return null;
4236
+ }
4237
+ const body = await res.json();
4238
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
4239
+ console.error(
4240
+ `[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
4241
+ );
4242
+ return null;
4243
+ }
4244
+ for (const integration of body.data) {
4245
+ if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
4246
+ console.error(
4247
+ `[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
4248
+ );
4249
+ return null;
4250
+ }
4251
+ }
4252
+ return body.data.some(
4253
+ (integration) => Array.isArray(integration.connections) && integration.connections.length > 0
4254
+ );
4255
+ } catch (error2) {
4256
+ console.error(
4257
+ `[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
4258
+ );
4259
+ return null;
4260
+ }
4261
+ }
3478
4262
  try {
3479
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
4263
+ const res = await requestWithClient(port, client, "/config/providers");
3480
4264
  if (!res.ok) {
3481
4265
  console.error(
3482
4266
  `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3505,7 +4289,7 @@ async function hasAnyConfiguredProvider(port) {
3505
4289
  return null;
3506
4290
  }
3507
4291
  }
3508
- function sessionErrorReason(error2) {
4292
+ function sessionErrorReason2(error2) {
3509
4293
  const record = typeof error2 === "object" && error2 !== null ? error2 : null;
3510
4294
  const data = record?.data;
3511
4295
  const dataRecord = typeof data === "object" && data !== null ? data : null;
@@ -3535,16 +4319,20 @@ function parseSessionErrorFrame(data) {
3535
4319
  if (typeof sessionId !== "string" || sessionId.length === 0) return null;
3536
4320
  return {
3537
4321
  sessionId,
3538
- reason: sessionErrorReason(propertiesRecord.error)
4322
+ reason: sessionErrorReason2(propertiesRecord.error)
3539
4323
  };
3540
4324
  }
3541
- async function readSessionErrorStream(port, options) {
4325
+ async function readSessionErrorStream(port, options, client) {
4326
+ if (client?.version === "v2") return readV2SessionErrorStream(client, options);
3542
4327
  let reader = null;
3543
4328
  try {
3544
- const response = await fetch(`${opencodeBase(port)}/event`, {
4329
+ const response = await (client?.request("/event", {
3545
4330
  headers: { accept: "text/event-stream" },
3546
4331
  signal: options.signal
3547
- });
4332
+ }) ?? fetch(`${opencodeBase(port)}/event`, {
4333
+ headers: { accept: "text/event-stream" },
4334
+ signal: options.signal
4335
+ }));
3548
4336
  if (!response.ok || !response.body) {
3549
4337
  return { reason: "unavailable", detail: `HTTP ${response.status}` };
3550
4338
  }
@@ -3575,9 +4363,10 @@ async function readSessionErrorStream(port, options) {
3575
4363
  if (reader) void reader.cancel().catch(() => void 0);
3576
4364
  }
3577
4365
  }
3578
- async function reloadProviderCache(port) {
4366
+ async function reloadProviderCache(port, client) {
4367
+ if (client?.version === "v2") return;
3579
4368
  try {
3580
- const res = await timedFetch(`${opencodeBase(port)}/config`, {
4369
+ const res = await requestWithClient(port, client, "/config", {
3581
4370
  method: "PATCH",
3582
4371
  headers: { "Content-Type": "application/json" },
3583
4372
  body: JSON.stringify({})
@@ -3955,12 +4744,14 @@ var STRIP_RES = /* @__PURE__ */ new Set([
3955
4744
  "content-length"
3956
4745
  ]);
3957
4746
  var StreamForwarder = class {
3958
- constructor(ws, port, callbacks = {}) {
4747
+ constructor(ws, port, callbacks = {}, options = {}) {
3959
4748
  this.ws = ws;
3960
4749
  this.port = port;
3961
4750
  this.callbacks = callbacks;
4751
+ this.options = options;
3962
4752
  }
3963
4753
  inflight = /* @__PURE__ */ new Map();
4754
+ binaryFramesSupported = false;
3964
4755
  /**
3965
4756
  * Handle an edge→agent frame. Unknown frame types are ignored.
3966
4757
  */
@@ -3980,6 +4771,12 @@ var StreamForwarder = class {
3980
4771
  break;
3981
4772
  }
3982
4773
  }
4774
+ handleBinaryBodyFrame(sid, payload) {
4775
+ this.inflight.get(sid)?.pushBody?.(payload);
4776
+ }
4777
+ setBinaryFramesSupported(supported) {
4778
+ this.binaryFramesSupported = supported;
4779
+ }
3983
4780
  /**
3984
4781
  * Abort every in-flight stream (e.g. on WebSocket close).
3985
4782
  */
@@ -4042,7 +4839,15 @@ var StreamForwarder = class {
4042
4839
  }
4043
4840
  const fwdHeaders = {};
4044
4841
  for (const [k, v] of Object.entries(headers ?? {})) {
4045
- if (!STRIP_REQ.has(k.toLowerCase())) fwdHeaders[k] = v;
4842
+ const lower = k.toLowerCase();
4843
+ if (STRIP_REQ.has(lower)) continue;
4844
+ if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
4845
+ if (lower === "authorization") continue;
4846
+ }
4847
+ fwdHeaders[k] = v;
4848
+ }
4849
+ if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
4850
+ fwdHeaders.Authorization = buildOpenCodeBasicAuthHeader(this.options.openCodePassword);
4046
4851
  }
4047
4852
  this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
4048
4853
  const body = bodyPromise ? await bodyPromise : void 0;
@@ -4089,7 +4894,13 @@ var StreamForwarder = class {
4089
4894
  const chunk = Buffer.from(value);
4090
4895
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
4091
4896
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
4092
- this.send({ type: "res_data", sid, b64: slice.toString("base64") });
4897
+ if (this.binaryFramesSupported) {
4898
+ if (this.ws.readyState === WebSocket.OPEN) {
4899
+ this.ws.send(encodeBinaryBodyFrame(BINARY_FRAME_RES_DATA, sid, slice));
4900
+ }
4901
+ } else {
4902
+ this.send({ type: "res_data", sid, b64: slice.toString("base64") });
4903
+ }
4093
4904
  }
4094
4905
  }
4095
4906
  }
@@ -4106,6 +4917,11 @@ var StreamForwarder = class {
4106
4917
 
4107
4918
  // src/lib/tunnel/connection.ts
4108
4919
  var FAILURE_REASON_HEADER_LC = FORWARD_FAILURE_REASON_HEADER.toLowerCase();
4920
+ function toUint8Array(data) {
4921
+ if (Array.isArray(data)) return Buffer.concat(data);
4922
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
4923
+ return data;
4924
+ }
4109
4925
  var TunnelUpgradeRejectedError = class extends Error {
4110
4926
  constructor(message, reason) {
4111
4927
  super(message);
@@ -4155,6 +4971,7 @@ function connectTunnel(options) {
4155
4971
  agentId,
4156
4972
  authHeader,
4157
4973
  port,
4974
+ openCodePassword,
4158
4975
  onConnected,
4159
4976
  onDisconnected,
4160
4977
  onError,
@@ -4169,14 +4986,20 @@ function connectTunnel(options) {
4169
4986
  return new Promise((resolve4, reject) => {
4170
4987
  const ws = new WebSocket2(url, {
4171
4988
  headers: {
4172
- Authorization: authHeader
4989
+ Authorization: authHeader,
4990
+ "X-Evident-Tunnel-Binary-Frames": "1"
4173
4991
  }
4174
4992
  });
4175
- const forwarder = new StreamForwarder(ws, port, {
4176
- onHead: () => onResponse?.(),
4177
- onDrainPing: () => onDrainPing?.(),
4178
- onUsageRearmPing: () => onUsageRearmPing?.()
4179
- });
4993
+ const forwarder = new StreamForwarder(
4994
+ ws,
4995
+ port,
4996
+ {
4997
+ onHead: () => onResponse?.(),
4998
+ onDrainPing: () => onDrainPing?.(),
4999
+ onUsageRearmPing: () => onUsageRearmPing?.()
5000
+ },
5001
+ { openCodePassword }
5002
+ );
4180
5003
  const connectionTimeout = setTimeout(() => {
4181
5004
  ws.close();
4182
5005
  reject(new Error("Connection timeout"));
@@ -4213,7 +5036,25 @@ function connectTunnel(options) {
4213
5036
  ws.on("open", () => {
4214
5037
  onInfo?.("WebSocket connection established");
4215
5038
  });
4216
- ws.on("message", (data) => {
5039
+ ws.on("message", (data, isBinary) => {
5040
+ if (isBinary) {
5041
+ try {
5042
+ const frame = decodeBinaryBodyFrame(toUint8Array(data));
5043
+ if (frame === null) {
5044
+ onError?.("Failed to handle binary message: invalid frame");
5045
+ return;
5046
+ }
5047
+ if (frame.type !== BINARY_FRAME_REQ_DATA) {
5048
+ onError?.("Failed to handle binary message: unexpected frame type");
5049
+ return;
5050
+ }
5051
+ forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
5052
+ } catch (error2) {
5053
+ const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
5054
+ onError?.(`Failed to handle binary message: ${errorMessage3}`);
5055
+ }
5056
+ return;
5057
+ }
4217
5058
  let message;
4218
5059
  try {
4219
5060
  message = JSON.parse(data.toString());
@@ -4229,6 +5070,7 @@ function connectTunnel(options) {
4229
5070
  switch (message.type) {
4230
5071
  case "connected": {
4231
5072
  clearTimeout(connectionTimeout);
5073
+ forwarder.setBinaryFramesSupported(message.binary_frames === true);
4232
5074
  const connectedAgentId = message.agent_id ?? agentId;
4233
5075
  onConnected?.(connectedAgentId);
4234
5076
  resolve4({
@@ -4319,6 +5161,7 @@ var RunnerConnection = class {
4319
5161
  agentId: this.resolvedAgentId,
4320
5162
  authHeader: this.opts.getAuthHeader(),
4321
5163
  port: this.opts.port,
5164
+ openCodePassword: this.opts.openCodePassword,
4322
5165
  onConnected: (agentId) => {
4323
5166
  this.reconnectAttempt = 0;
4324
5167
  this.reconnecting = false;
@@ -4499,33 +5342,73 @@ function parseCodexUsageHeaders(headers) {
4499
5342
  function normalizeProbeModel(model) {
4500
5343
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4501
5344
  }
4502
- async function resolveProbeModels(port) {
5345
+ function isRecord2(value) {
5346
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5347
+ }
5348
+ function unsupportedProbeModels(reason, port) {
5349
+ console.error(`[resolveProbeModels] ${reason} (port ${port})`);
5350
+ return { status: "unsupported", reason };
5351
+ }
5352
+ async function resolveV1ProbeModels(client, port) {
4503
5353
  try {
4504
- const res = await withRequestTimeout(
4505
- fetch,
4506
- REQUEST_TIMEOUT_MS
4507
- )(`${opencodeBase(port)}/config/providers`);
4508
- if (!res.ok) {
4509
- console.error(
4510
- `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
4511
- );
4512
- return [];
5354
+ const response = await client.request("/config/providers");
5355
+ const body = await response.json();
5356
+ if (!isRecord2(body) || !Array.isArray(body.providers)) {
5357
+ return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
4513
5358
  }
4514
- const body = await res.json();
4515
- const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
4516
- if (!provider || !provider.models || typeof provider.models !== "object") return [];
5359
+ const provider = body.providers.find(
5360
+ (candidate) => isRecord2(candidate) && candidate.id === "openai"
5361
+ );
5362
+ if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
5363
+ const defaults2 = isRecord2(body.default) ? body.default : void 0;
4517
5364
  const candidates = [
4518
- ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
5365
+ ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4519
5366
  ...Object.keys(provider.models)
4520
5367
  ].map(normalizeProbeModel);
4521
- return [...new Set(candidates)].slice(0, 4);
5368
+ return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
4522
5369
  } catch (err) {
4523
- console.error(
4524
- `[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
5370
+ return unsupportedProbeModels(
5371
+ `V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
5372
+ port
5373
+ );
5374
+ }
5375
+ }
5376
+ async function resolveV2ProbeModels(client, port) {
5377
+ const directory = await getOpenCodeDirectoryV2(client);
5378
+ if (!directory) {
5379
+ return unsupportedProbeModels("V2 working directory could not be verified", port);
5380
+ }
5381
+ const path = `/api/provider?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
5382
+ try {
5383
+ const response = await client.request(path);
5384
+ const body = await response.json();
5385
+ if (!isRecord2(body) || !Array.isArray(body.data)) {
5386
+ return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
5387
+ }
5388
+ const provider = body.data.find(
5389
+ (candidate) => isRecord2(candidate) && candidate.id === "openai"
5390
+ );
5391
+ if (!provider) return { status: "supported", models: [] };
5392
+ if (!isRecord2(provider.models)) {
5393
+ return unsupportedProbeModels(
5394
+ "V2 provider response has no safe OpenAI model catalogue",
5395
+ port
5396
+ );
5397
+ }
5398
+ return {
5399
+ status: "supported",
5400
+ models: [...new Set(Object.keys(provider.models).map(normalizeProbeModel))].slice(0, 4)
5401
+ };
5402
+ } catch (err) {
5403
+ return unsupportedProbeModels(
5404
+ `V2 GET ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
5405
+ port
4525
5406
  );
4526
- return [];
4527
5407
  }
4528
5408
  }
5409
+ async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
5410
+ return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
5411
+ }
4529
5412
  function hasPrimaryHeaders(headers) {
4530
5413
  return [
4531
5414
  "x-codex-primary-used-percent",
@@ -4533,7 +5416,7 @@ function hasPrimaryHeaders(headers) {
4533
5416
  "x-codex-primary-reset-at"
4534
5417
  ].some((name) => headers.has(name));
4535
5418
  }
4536
- async function getOpenAiUsage(port) {
5419
+ async function getOpenAiUsage(port, client) {
4537
5420
  const credentials2 = readOpenCodeChatGptCredentials();
4538
5421
  if (!credentials2) {
4539
5422
  throw new OpenAiUsageError(
@@ -4548,12 +5431,16 @@ async function getOpenAiUsage(port) {
4548
5431
  );
4549
5432
  }
4550
5433
  const subscription = parseChatGptIdentity(credentials2.accessToken);
4551
- const models = await resolveProbeModels(port);
4552
- if (models.length === 0) {
4553
- throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
5434
+ const lookup = await resolveProbeModels(port, client);
5435
+ if (lookup.status === "unsupported" || lookup.models.length === 0) {
5436
+ const detail = lookup.status === "unsupported" ? ` ${lookup.reason}.` : "";
5437
+ throw new OpenAiUsageError(
5438
+ `No supported OpenAI probe model is available.${detail}`,
5439
+ "no_probe_model"
5440
+ );
4554
5441
  }
4555
5442
  let lastStatus;
4556
- for (const model of models) {
5443
+ for (const model of lookup.models) {
4557
5444
  let res;
4558
5445
  try {
4559
5446
  res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
@@ -5372,6 +6259,7 @@ var ChannelDriver = class _ChannelDriver {
5372
6259
  maxActiveSessions;
5373
6260
  watcherStallMs;
5374
6261
  wedgeWarningIntervalMs;
6262
+ openCodeClient;
5375
6263
  /** Cache of conversationId → opencode sessionId. */
5376
6264
  sessions = /* @__PURE__ */ new Map();
5377
6265
  /**
@@ -5480,20 +6368,26 @@ var ChannelDriver = class _ChannelDriver {
5480
6368
  */
5481
6369
  readopted = /* @__PURE__ */ new Set();
5482
6370
  /**
5483
- * "Don't re-DISPATCH / re-attach this orphan again" (Bug 2/5). Set when a
5484
- * re-adopted running/orphan row's watcher hit its `processed_at`-anchored
5485
- * deadline (or an orphan whose window already elapsed): the still-`processing`
5486
- * server row would otherwise be re-adopted (and re-dispatched) on EVERY ~2s
5487
- * drain until the 15-min cron resets it spamming new turns.
5488
- *
5489
- * CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
5490
- * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
5491
- * in opencode must still be delivered via `markDone` on the next drain — so
5492
- * `readoptOne` computes `state` FIRST and this set is checked only on the
5493
- * non-done path. It is cleared once the row leaves the processing list (cron
5494
- * reset → it drains normally as `pending`), so it can never leak.
6371
+ * Readopt give-up fence. Set when recovery declines to start or continue a turn
6372
+ * for a row that is still `processing`, so the next drain does not re-dispatch or
6373
+ * re-attach it before the cron safety net acts. It suppresses only non-done
6374
+ * recovery paths; DONE delivery still runs. Clear it when
6375
+ * `!stillProcessing.has(id)`, because leaving `processing` hands the row back to
6376
+ * normal processing.
5495
6377
  */
5496
6378
  dontRedispatch = /* @__PURE__ */ new Set();
6379
+ /**
6380
+ * Untrackable-ack fence. Stores the report error so a failed terminal report can
6381
+ * be retried without posting the accepted prompt again. Keep it fenced while the
6382
+ * row is `processing` or `pending`; clear it only when absent from both lists.
6383
+ */
6384
+ untrackableAck = /* @__PURE__ */ new Map();
6385
+ /** Messages whose untrackable-ack failure signal was emitted while fenced. */
6386
+ untrackableAckSignalled = /* @__PURE__ */ new Set();
6387
+ /** Messages whose first untrackable-ack fence warning was already logged. */
6388
+ untrackableAckWarned = /* @__PURE__ */ new Set();
6389
+ /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6390
+ pendingMessageIds = /* @__PURE__ */ new Set();
5497
6391
  /**
5498
6392
  * "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
5499
6393
  * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
@@ -5616,7 +6510,8 @@ var ChannelDriver = class _ChannelDriver {
5616
6510
  */
5617
6511
  attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
5618
6512
  /**
5619
- * Cache of the opencode root directory (from `GET /path`). Resolved lazily on
6513
+ * Cache of the opencode root directory from the selected client's location lookup.
6514
+ * Resolved lazily on
5620
6515
  * first session creation so drain-created sessions are rooted at the project
5621
6516
  * directory and thus visible in `opencode web`'s session list. `undefined` =
5622
6517
  * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
@@ -5710,6 +6605,11 @@ var ChannelDriver = class _ChannelDriver {
5710
6605
  config.fetchImpl ?? fetch,
5711
6606
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
5712
6607
  );
6608
+ this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
6609
+ port: config.port,
6610
+ version: "v1",
6611
+ fetchImpl: config.fetchImpl
6612
+ });
5713
6613
  this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5714
6614
  this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
5715
6615
  this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
@@ -5721,9 +6621,39 @@ var ChannelDriver = class _ChannelDriver {
5721
6621
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5722
6622
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
5723
6623
  }
5724
- /** The IPv4-loopback base URL for the local `opencode serve`. */
5725
- get opencodeBase() {
5726
- return `http://127.0.0.1:${this.port}`;
6624
+ get isV2() {
6625
+ return this.openCodeClient.version === "v2";
6626
+ }
6627
+ async getSessionMessages(sessionId) {
6628
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6629
+ }
6630
+ async getSubagentSessionMessages(sessionId) {
6631
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
6632
+ }
6633
+ async getTelemetrySubagentSessionMessages(sessionId) {
6634
+ if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
6635
+ return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6636
+ }
6637
+ async listSessions() {
6638
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
6639
+ }
6640
+ async sessionExists(sessionId) {
6641
+ return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
6642
+ }
6643
+ async isSessionOngoing(sessionId) {
6644
+ return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
6645
+ }
6646
+ async getOpenCodeDirectory() {
6647
+ return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
6648
+ }
6649
+ async createOpenCodeSession(directory) {
6650
+ return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
6651
+ }
6652
+ async hasAnyConfiguredProvider() {
6653
+ return hasAnyConfiguredProvider(this.port, this.openCodeClient);
6654
+ }
6655
+ async readOpenCodeSessionErrorStream(options) {
6656
+ return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
5727
6657
  }
5728
6658
  /**
5729
6659
  * Drain all pending channel conversations once: poll → dispatch → register.
@@ -5801,6 +6731,7 @@ var ChannelDriver = class _ChannelDriver {
5801
6731
  async runDrain() {
5802
6732
  let dispatched = 0;
5803
6733
  try {
6734
+ this.pendingMessageIds.clear();
5804
6735
  const conversations = await this.getPendingConversations();
5805
6736
  if (this.recycleRequestedFlag) {
5806
6737
  this.stop();
@@ -6025,6 +6956,7 @@ var ChannelDriver = class _ChannelDriver {
6025
6956
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6026
6957
  this.ensureSessionErrorStream();
6027
6958
  const messages = await this.getPendingMessages(conv.id);
6959
+ for (const message of messages) this.pendingMessageIds.add(message.id);
6028
6960
  let dispatched = 0;
6029
6961
  let skippedAlreadyDispatched = 0;
6030
6962
  if (refusedSessionId && messages.length > 0) {
@@ -6038,6 +6970,27 @@ var ChannelDriver = class _ChannelDriver {
6038
6970
  skippedAlreadyDispatched += 1;
6039
6971
  continue;
6040
6972
  }
6973
+ if (this.untrackableAck.has(message.id)) {
6974
+ const firstFenceWarning = !this.untrackableAckWarned.has(message.id);
6975
+ this.untrackableAckWarned.add(message.id);
6976
+ this.log({
6977
+ level: firstFenceWarning ? "warn" : "debug",
6978
+ message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
6979
+ conversation_id: conv.id,
6980
+ message_id: message.id
6981
+ });
6982
+ const errorMessage3 = this.untrackableAck.get(message.id);
6983
+ const reportError = await this.reportUntrackableAck(
6984
+ conv.id,
6985
+ message.id,
6986
+ null,
6987
+ errorMessage3
6988
+ );
6989
+ if (reportError === null) {
6990
+ continue;
6991
+ }
6992
+ break;
6993
+ }
6041
6994
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
6042
6995
  if (effectiveOpencodeMessageId) {
6043
6996
  const outcome = await this.resolveRedrive(
@@ -6066,15 +7019,60 @@ var ChannelDriver = class _ChannelDriver {
6066
7019
  conversation_id: conv.id,
6067
7020
  message_id: message.id
6068
7021
  });
6069
- const sendAttachments = this.buildSendAttachments(conv, message);
6070
- opencodeMessageId = await this.dispatchLocked(
7022
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
7023
+ if (this.isV2 && message.attachments && message.attachments.length > 0) {
7024
+ this.signalAttachmentsSkipped(
7025
+ conv.id,
7026
+ message.id,
7027
+ message.attachments.map((attachment, index) => ({
7028
+ index,
7029
+ mime: attachment.mime,
7030
+ ...attachment.filename ? { filename: attachment.filename } : {},
7031
+ status: "skipped"
7032
+ })),
7033
+ false
7034
+ );
7035
+ }
7036
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
6071
7037
  sessionId,
6072
- () => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
7038
+ () => sendPromptAsync(
7039
+ this.port,
7040
+ sessionId,
7041
+ message.content,
7042
+ options,
7043
+ sendAttachments,
7044
+ this.openCodeClient
7045
+ )
6073
7046
  );
6074
7047
  } catch (err) {
6075
7048
  if (err instanceof ChannelAuthError) throw err;
7049
+ if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
7050
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
7051
+ this.untrackableAck.set(message.id, errorMessage4);
7052
+ this.log({
7053
+ level: "error",
7054
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
7055
+ conversation_id: conv.id,
7056
+ message_id: message.id
7057
+ });
7058
+ const markError = await this.reportUntrackableAck(
7059
+ conv.id,
7060
+ message.id,
7061
+ null,
7062
+ errorMessage4
7063
+ );
7064
+ if (markError !== null) {
7065
+ this.log({
7066
+ level: "warn",
7067
+ message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markError.message}`,
7068
+ conversation_id: conv.id,
7069
+ message_id: message.id
7070
+ });
7071
+ }
7072
+ break;
7073
+ }
6076
7074
  this.dispatched.delete(message.id);
6077
- const exists = await sessionExists(this.port, sessionId);
7075
+ const exists = await this.sessionExists(sessionId);
6078
7076
  if (exists === false) {
6079
7077
  this.sessions.delete(conv.id);
6080
7078
  this.log({
@@ -6123,6 +7121,9 @@ var ChannelDriver = class _ChannelDriver {
6123
7121
  break;
6124
7122
  }
6125
7123
  if (opencodeMessageId === null) {
7124
+ if (this.isV2) {
7125
+ throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7126
+ }
6126
7127
  const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
6127
7128
  if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
6128
7129
  this.log({
@@ -6270,29 +7271,38 @@ var ChannelDriver = class _ChannelDriver {
6270
7271
  */
6271
7272
  async pollSessionMessagesForRedrive(conv, message, sessionId) {
6272
7273
  try {
6273
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
6274
- if (!res.ok) {
6275
- const rawBody = await res.text();
6276
- const normalized = normalizeRedrivePollFailureBody(rawBody);
6277
- this.log({
6278
- level: "warn",
6279
- 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`,
6280
- conversation_id: conv.id,
6281
- message_id: message.id
6282
- });
6283
- return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ""}` };
7274
+ if (this.isV2) {
7275
+ const messages = await this.getSessionMessages(sessionId);
7276
+ if (messages === null) {
7277
+ this.log({
7278
+ level: "warn",
7279
+ 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`,
7280
+ conversation_id: conv.id,
7281
+ message_id: message.id
7282
+ });
7283
+ return { ok: false, signature: null };
7284
+ }
7285
+ return { ok: true, messages };
6284
7286
  }
6285
- const body = await res.json();
6286
- if (!Array.isArray(body)) {
7287
+ const polledV1 = await pollSessionMessagesForRedrive(
7288
+ this.port,
7289
+ sessionId,
7290
+ this.openCodeClient
7291
+ );
7292
+ if (!polledV1.ok) {
7293
+ const normalized = normalizeRedrivePollFailureBody(polledV1.body);
6287
7294
  this.log({
6288
7295
  level: "warn",
6289
- 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`,
7296
+ 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`,
6290
7297
  conversation_id: conv.id,
6291
7298
  message_id: message.id
6292
7299
  });
6293
- return { ok: false, signature: "non-array message body" };
7300
+ return {
7301
+ ok: false,
7302
+ signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
7303
+ };
6294
7304
  }
6295
- return { ok: true, messages: body };
7305
+ return { ok: true, messages: polledV1.messages };
6296
7306
  } catch (err) {
6297
7307
  this.log({
6298
7308
  level: "warn",
@@ -6351,11 +7361,11 @@ var ChannelDriver = class _ChannelDriver {
6351
7361
  }
6352
7362
  const state = messageRunState(messages, ocId ?? "");
6353
7363
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
6354
- const ongoing = await isSessionOngoing(this.port, sessionId);
7364
+ const ongoing = await this.isSessionOngoing(sessionId);
6355
7365
  if (ongoing === false) {
6356
7366
  this.log({
6357
7367
  level: "info",
6358
- 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`,
7368
+ 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`,
6359
7369
  conversation_id: conv.id,
6360
7370
  message_id: message.id
6361
7371
  });
@@ -6368,7 +7378,7 @@ var ChannelDriver = class _ChannelDriver {
6368
7378
  return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
6369
7379
  }
6370
7380
  if (state === "running" || state === "queued") {
6371
- const ongoing = await isSessionOngoing(this.port, sessionId);
7381
+ const ongoing = await this.isSessionOngoing(sessionId);
6372
7382
  if (ongoing === true) {
6373
7383
  if (state === "queued") {
6374
7384
  const siblingOcIds = this.siblingOpencodeMessageIds(
@@ -6828,7 +7838,7 @@ var ChannelDriver = class _ChannelDriver {
6828
7838
  };
6829
7839
  }
6830
7840
  if (bound) {
6831
- const exists = await sessionExists(this.port, bound);
7841
+ const exists = await this.sessionExists(bound);
6832
7842
  if (exists === false) {
6833
7843
  this.log({
6834
7844
  level: "debug",
@@ -6861,7 +7871,7 @@ var ChannelDriver = class _ChannelDriver {
6861
7871
  */
6862
7872
  async createAndBindSession(conversationId) {
6863
7873
  const directory = await this.resolveOpenCodeDirectory();
6864
- const sessionId = await createOpenCodeSession(this.port, directory);
7874
+ const sessionId = await this.createOpenCodeSession(directory);
6865
7875
  this.sessions.set(conversationId, sessionId);
6866
7876
  await this.persistSession(conversationId, sessionId).catch((err) => {
6867
7877
  this.log({
@@ -6873,17 +7883,18 @@ var ChannelDriver = class _ChannelDriver {
6873
7883
  return sessionId;
6874
7884
  }
6875
7885
  /**
6876
- * Lazily resolve (and cache) opencode's root directory via `GET /path`.
7886
+ * Lazily resolve (and cache) opencode's root directory via the selected client's
7887
+ * location lookup.
6877
7888
  * Resolved once per driver: `undefined` until first lookup, then the directory
6878
- * string or `null` if unavailable (we don't keep retrying a missing `/path`).
7889
+ * string or `null` if unavailable (we don't keep retrying a failed lookup).
6879
7890
  */
6880
7891
  async resolveOpenCodeDirectory() {
6881
7892
  if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
6882
- this.opencodeDirectory = await getOpenCodeDirectory(this.port);
7893
+ this.opencodeDirectory = await this.getOpenCodeDirectory();
6883
7894
  if (!this.opencodeDirectory) {
6884
7895
  this.log({
6885
7896
  level: "warn",
6886
- message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
7897
+ message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
6887
7898
  });
6888
7899
  }
6889
7900
  return this.opencodeDirectory;
@@ -7317,7 +8328,7 @@ var ChannelDriver = class _ChannelDriver {
7317
8328
  while (!this.stopped && !signal.aborted) {
7318
8329
  const openedAt = this.now();
7319
8330
  try {
7320
- const outcome = await readSessionErrorStream(this.port, {
8331
+ const outcome = await this.readOpenCodeSessionErrorStream({
7321
8332
  signal,
7322
8333
  onSessionError: (event) => this.handleSessionError(event)
7323
8334
  });
@@ -7408,7 +8419,7 @@ var ChannelDriver = class _ChannelDriver {
7408
8419
  }
7409
8420
  async failFromSessionError(watcher, event, inFlight) {
7410
8421
  try {
7411
- const messages = await getSessionMessages(this.port, event.sessionId);
8422
+ const messages = await this.getSessionMessages(event.sessionId);
7412
8423
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7413
8424
  if (state !== "queued") {
7414
8425
  this.log({
@@ -7460,7 +8471,8 @@ var ChannelDriver = class _ChannelDriver {
7460
8471
  * markDone (done) exactly once per transition;
7461
8472
  * 2. applies the idle-path re-dispatch guard (a dispatched message that never
7462
8473
  * APPEARS → re-dispatch — D1 obligation 2);
7463
- * 3. polls `/question` + `/permission` (scoped to the session) and surfaces
8474
+ * 3. polls V1's global `/question` + `/permission`, or V2's
8475
+ * `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
7464
8476
  * NEW ones via `reportInteraction`, carrying the PAUSED message's own
7465
8477
  * `source_message_id`;
7466
8478
  * 4. drops messages that completed or timed out from the in-flight set.
@@ -7486,11 +8498,7 @@ var ChannelDriver = class _ChannelDriver {
7486
8498
  if (watcher.generation !== generation) return;
7487
8499
  let messages = null;
7488
8500
  try {
7489
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
7490
- if (res.ok) {
7491
- const body = await res.json();
7492
- messages = Array.isArray(body) ? body : null;
7493
- }
8501
+ messages = await this.getSessionMessages(sessionId);
7494
8502
  } catch {
7495
8503
  }
7496
8504
  if (messages != null && messages.length > 0) {
@@ -7723,7 +8731,7 @@ var ChannelDriver = class _ChannelDriver {
7723
8731
  inFlight.b2LastDescendantCheckMs = this.now();
7724
8732
  const [descendantOngoing, rootOngoing] = await Promise.all([
7725
8733
  this.isAnyDescendantSessionOngoing(sessionId),
7726
- isSessionOngoing(this.port, sessionId)
8734
+ this.isSessionOngoing(sessionId)
7727
8735
  ]);
7728
8736
  if (isB2AbandonmentConfirmed({
7729
8737
  pinnedForMs,
@@ -7783,7 +8791,7 @@ var ChannelDriver = class _ChannelDriver {
7783
8791
  });
7784
8792
  }
7785
8793
  const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
7786
- const ongoing = await isSessionOngoing(this.port, sessionId);
8794
+ const ongoing = await this.isSessionOngoing(sessionId);
7787
8795
  if (isAmbiguousFinishResolved({
7788
8796
  pinnedForMs,
7789
8797
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
@@ -7966,7 +8974,7 @@ var ChannelDriver = class _ChannelDriver {
7966
8974
  */
7967
8975
  async readoptProcessing() {
7968
8976
  const rows = await this.getProcessingMessages();
7969
- if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
8977
+ if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
7970
8978
  const stillProcessing = new Set(rows.map((r) => r.id));
7971
8979
  for (const id of [
7972
8980
  ...this.dontRedispatch,
@@ -7986,6 +8994,18 @@ var ChannelDriver = class _ChannelDriver {
7986
8994
  }
7987
8995
  }
7988
8996
  }
8997
+ for (const id of [...this.untrackableAck.keys()]) {
8998
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
8999
+ this.untrackableAck.delete(id);
9000
+ this.untrackableAckSignalled.delete(id);
9001
+ this.untrackableAckWarned.delete(id);
9002
+ this.log({
9003
+ level: "debug",
9004
+ message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
9005
+ message_id: id
9006
+ });
9007
+ }
9008
+ }
7989
9009
  }
7990
9010
  if (rows.length === 0) return;
7991
9011
  const bySession = /* @__PURE__ */ new Map();
@@ -8006,23 +9026,32 @@ var ChannelDriver = class _ChannelDriver {
8006
9026
  for (const [sessionId, sessionRows] of bySession) {
8007
9027
  let messages;
8008
9028
  try {
8009
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8010
- if (!res.ok) {
8011
- this.log({
8012
- level: "warn",
8013
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
8014
- });
8015
- continue;
8016
- }
8017
- const body = await res.json();
8018
- if (!Array.isArray(body)) {
8019
- this.log({
8020
- level: "warn",
8021
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
8022
- });
8023
- continue;
9029
+ if (this.isV2) {
9030
+ const snapshot = await this.getSessionMessages(sessionId);
9031
+ if (snapshot === null) {
9032
+ this.log({
9033
+ level: "warn",
9034
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
9035
+ });
9036
+ continue;
9037
+ }
9038
+ messages = snapshot;
9039
+ } else {
9040
+ const polled = await pollSessionMessagesForRedrive(
9041
+ this.port,
9042
+ sessionId,
9043
+ this.openCodeClient
9044
+ );
9045
+ if (!polled.ok) {
9046
+ const normalized = normalizeRedrivePollFailureBody(polled.body);
9047
+ this.log({
9048
+ level: "warn",
9049
+ 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`
9050
+ });
9051
+ continue;
9052
+ }
9053
+ messages = polled.messages;
8024
9054
  }
8025
- messages = body;
8026
9055
  } catch (err) {
8027
9056
  this.log({
8028
9057
  level: "warn",
@@ -8031,7 +9060,7 @@ var ChannelDriver = class _ChannelDriver {
8031
9060
  continue;
8032
9061
  }
8033
9062
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
8034
- const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
9063
+ const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
8035
9064
  for (const row of sessionRows) {
8036
9065
  await this.readoptOne(sessionId, row, messages, sessionOngoing);
8037
9066
  }
@@ -8078,7 +9107,7 @@ var ChannelDriver = class _ChannelDriver {
8078
9107
  if (restartAborted) {
8079
9108
  this.log({
8080
9109
  level: "info",
8081
- 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`,
9110
+ 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`,
8082
9111
  conversation_id: row.conversation_id,
8083
9112
  message_id: row.id
8084
9113
  });
@@ -8133,10 +9162,13 @@ var ChannelDriver = class _ChannelDriver {
8133
9162
  }
8134
9163
  await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
8135
9164
  this.dontRedispatch.delete(row.id);
9165
+ this.untrackableAck.delete(row.id);
9166
+ this.untrackableAckSignalled.delete(row.id);
9167
+ this.untrackableAckWarned.delete(row.id);
8136
9168
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
8137
9169
  return;
8138
9170
  }
8139
- if (this.dontRedispatch.has(row.id)) {
9171
+ if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
8140
9172
  this.log({
8141
9173
  level: "debug",
8142
9174
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
@@ -8156,7 +9188,7 @@ var ChannelDriver = class _ChannelDriver {
8156
9188
  const finish = reply?.info?.finish ?? reply?.finish;
8157
9189
  this.log({
8158
9190
  level: "info",
8159
- 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`,
9191
+ 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`,
8160
9192
  conversation_id: row.conversation_id,
8161
9193
  message_id: row.id
8162
9194
  });
@@ -8165,7 +9197,7 @@ var ChannelDriver = class _ChannelDriver {
8165
9197
  }
8166
9198
  this.log({
8167
9199
  level: "info",
8168
- 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)`,
9200
+ 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)`,
8169
9201
  conversation_id: row.conversation_id,
8170
9202
  message_id: row.id
8171
9203
  });
@@ -8175,7 +9207,7 @@ var ChannelDriver = class _ChannelDriver {
8175
9207
  if (ongoing === true) {
8176
9208
  this.log({
8177
9209
  level: "debug",
8178
- 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)`,
9210
+ 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)`,
8179
9211
  conversation_id: row.conversation_id,
8180
9212
  message_id: row.id
8181
9213
  });
@@ -8183,7 +9215,7 @@ var ChannelDriver = class _ChannelDriver {
8183
9215
  if (shape === "b1") {
8184
9216
  this.log({
8185
9217
  level: "debug",
8186
- 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`,
9218
+ 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`,
8187
9219
  conversation_id: row.conversation_id,
8188
9220
  message_id: row.id
8189
9221
  });
@@ -8195,7 +9227,7 @@ var ChannelDriver = class _ChannelDriver {
8195
9227
  }
8196
9228
  this.log({
8197
9229
  level: "debug",
8198
- 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`,
9230
+ 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`,
8199
9231
  conversation_id: row.conversation_id,
8200
9232
  message_id: row.id
8201
9233
  });
@@ -8245,7 +9277,7 @@ var ChannelDriver = class _ChannelDriver {
8245
9277
  * extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
8246
9278
  * SAME delivery instead of duplicating it.
8247
9279
  *
8248
- * EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops
9280
+ * EVEN IF the row was previously parked by either recovery fence (a give-up stops
8249
9281
  * re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
8250
9282
  * `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
8251
9283
  * leave for cron; transient → log + leave for the next drain (the still-
@@ -8312,6 +9344,9 @@ var ChannelDriver = class _ChannelDriver {
8312
9344
  await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
8313
9345
  }
8314
9346
  this.dontRedispatch.delete(row.id);
9347
+ this.untrackableAck.delete(row.id);
9348
+ this.untrackableAckSignalled.delete(row.id);
9349
+ this.untrackableAckWarned.delete(row.id);
8315
9350
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
8316
9351
  }
8317
9352
  /**
@@ -8375,19 +9410,103 @@ var ChannelDriver = class _ChannelDriver {
8375
9410
  conversation_id: row.conversation_id,
8376
9411
  message_id: row.id
8377
9412
  });
8378
- this.awaitingReadopt.add(row.id);
9413
+ if (!this.isV2) this.awaitingReadopt.add(row.id);
8379
9414
  const readoptConv = this.convForRow(sessionId, row);
8380
9415
  const readoptMessage = this.queuedMessageForRow(row);
8381
- const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
9416
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
9417
+ if (this.isV2 && row.attachments && row.attachments.length > 0) {
9418
+ this.signalAttachmentsSkipped(
9419
+ row.conversation_id,
9420
+ row.id,
9421
+ row.attachments.map((attachment, index) => ({
9422
+ index,
9423
+ mime: attachment.mime,
9424
+ ...attachment.filename ? { filename: attachment.filename } : {},
9425
+ status: "skipped"
9426
+ })),
9427
+ false
9428
+ );
9429
+ }
8382
9430
  let ocId;
8383
9431
  try {
8384
- ocId = await this.dispatchLocked(
9432
+ ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
8385
9433
  sessionId,
8386
- () => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
9434
+ () => sendPromptAsync(
9435
+ this.port,
9436
+ sessionId,
9437
+ row.content,
9438
+ options,
9439
+ sendAttachments,
9440
+ this.openCodeClient
9441
+ )
8387
9442
  );
8388
9443
  } catch (err) {
8389
9444
  this.awaitingReadopt.delete(row.id);
8390
9445
  if (err instanceof ChannelAuthError) throw err;
9446
+ if (this.isV2) {
9447
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
9448
+ const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
9449
+ if (!invalidPromptAck) {
9450
+ const exists = await this.sessionExists(sessionId);
9451
+ if (exists === false) {
9452
+ this.log({
9453
+ level: "warn",
9454
+ 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}`,
9455
+ conversation_id: row.conversation_id,
9456
+ message_id: row.id
9457
+ });
9458
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9459
+ return;
9460
+ }
9461
+ if (exists === null) {
9462
+ this.log({
9463
+ level: "warn",
9464
+ 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}`,
9465
+ conversation_id: row.conversation_id,
9466
+ message_id: row.id
9467
+ });
9468
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9469
+ return;
9470
+ }
9471
+ } else {
9472
+ this.untrackableAck.set(row.id, errorMessage3);
9473
+ }
9474
+ this.log({
9475
+ level: "error",
9476
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
9477
+ conversation_id: row.conversation_id,
9478
+ message_id: row.id
9479
+ });
9480
+ if (invalidPromptAck) {
9481
+ const markError = await this.reportUntrackableAck(
9482
+ row.conversation_id,
9483
+ row.id,
9484
+ sessionId,
9485
+ errorMessage3
9486
+ );
9487
+ if (markError !== null) {
9488
+ this.log({
9489
+ level: "warn",
9490
+ message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markError.message}`,
9491
+ conversation_id: row.conversation_id,
9492
+ message_id: row.id
9493
+ });
9494
+ }
9495
+ } else {
9496
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
9497
+ (markErr) => {
9498
+ this.log({
9499
+ level: "warn",
9500
+ message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9501
+ conversation_id: row.conversation_id,
9502
+ message_id: row.id
9503
+ });
9504
+ this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
9505
+ }
9506
+ );
9507
+ }
9508
+ return;
9509
+ }
8391
9510
  this.log({
8392
9511
  level: "warn",
8393
9512
  message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
@@ -8519,12 +9638,13 @@ var ChannelDriver = class _ChannelDriver {
8519
9638
  this.dispatched.delete(evidentMessageId);
8520
9639
  }
8521
9640
  /**
8522
- * Poll `/question` + `/permission` (scoped to the session) and surface NEW ones
8523
- * via `reportInteraction` (Task 3.5), carrying the PAUSED message's own
8524
- * `source_message_id` so the server @mentions the correct person under
8525
- * concurrency. Dedups by interaction id across ticks (reused per-session sets).
9641
+ * Poll V1's global `/question` + `/permission`, or V2's watched-session form and
9642
+ * permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
9643
+ * carrying the PAUSED message's own `source_message_id` so the server @mentions
9644
+ * the correct person under concurrency. Dedups by interaction id across ticks
9645
+ * (reused per-session sets).
8526
9646
  *
8527
- * The interaction is attributed to the in-flight message it paused on. opencode
9647
+ * The interaction is attributed to the in-flight message it paused on. OpenCode
8528
9648
  * stamps a `messageID` on a permission (and `tool.messageID` on a question) =
8529
9649
  * the assistant message id, whose `parentID` is the user message id — but the
8530
9650
  * simplest robust attribution here is: the single in-flight message that is
@@ -8547,16 +9667,14 @@ var ChannelDriver = class _ChannelDriver {
8547
9667
  let permissionsPolledOk = true;
8548
9668
  let questions = [];
8549
9669
  try {
8550
- const res = await this.fetchImpl(`${this.opencodeBase}/question`);
8551
- if (res.ok) {
8552
- const body = await res.json();
8553
- if (Array.isArray(body)) {
8554
- questions = body;
8555
- } else {
8556
- questionsPolledOk = false;
8557
- }
9670
+ if (this.isV2) {
9671
+ const forms = await listV2Forms(this.openCodeClient, sessionId);
9672
+ if (forms === null) questionsPolledOk = false;
9673
+ else questions = forms;
8558
9674
  } else {
8559
- questionsPolledOk = false;
9675
+ const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
9676
+ if (listed === null) questionsPolledOk = false;
9677
+ else questions = listed;
8560
9678
  }
8561
9679
  } catch {
8562
9680
  questionsPolledOk = false;
@@ -8576,16 +9694,14 @@ var ChannelDriver = class _ChannelDriver {
8576
9694
  }
8577
9695
  let permissions = [];
8578
9696
  try {
8579
- const res = await this.fetchImpl(`${this.opencodeBase}/permission`);
8580
- if (res.ok) {
8581
- const body = await res.json();
8582
- if (Array.isArray(body)) {
8583
- permissions = body;
8584
- } else {
8585
- permissionsPolledOk = false;
8586
- }
9697
+ if (this.isV2) {
9698
+ const listed = await listV2Permissions(this.openCodeClient, sessionId);
9699
+ if (listed === null) permissionsPolledOk = false;
9700
+ else permissions = listed;
8587
9701
  } else {
8588
- permissionsPolledOk = false;
9702
+ const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
9703
+ if (listed === null) permissionsPolledOk = false;
9704
+ else permissions = listed;
8589
9705
  }
8590
9706
  } catch {
8591
9707
  permissionsPolledOk = false;
@@ -8679,10 +9795,14 @@ var ChannelDriver = class _ChannelDriver {
8679
9795
  if (cached !== void 0) return cached;
8680
9796
  let parent = void 0;
8681
9797
  try {
8682
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8683
- if (res.ok) {
8684
- const body = await res.json();
8685
- parent = body && typeof body.parentID === "string" ? body.parentID : null;
9798
+ if (this.isV2) {
9799
+ const session = await getV2Session(this.openCodeClient, sessionId);
9800
+ parent = null;
9801
+ const candidate = session.parentID;
9802
+ if (typeof candidate === "string") parent = candidate;
9803
+ } else {
9804
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9805
+ parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
8686
9806
  }
8687
9807
  } catch {
8688
9808
  parent = void 0;
@@ -8736,18 +9856,16 @@ var ChannelDriver = class _ChannelDriver {
8736
9856
  if (cached) return cached;
8737
9857
  const pending = (async () => {
8738
9858
  try {
8739
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8740
- if (!res.ok) {
9859
+ const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
9860
+ if (messages2 === null) {
8741
9861
  this.log({
8742
9862
  level: "warn",
8743
- message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 omitting invocation telemetry`,
9863
+ 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`,
8744
9864
  message_id: messageId
8745
9865
  });
8746
9866
  return null;
8747
9867
  }
8748
- const body = await res.json();
8749
- if (!Array.isArray(body)) throw new Error("response body was not a message array");
8750
- return body;
9868
+ return messages2;
8751
9869
  } catch (err) {
8752
9870
  this.log({
8753
9871
  level: "warn",
@@ -8888,21 +10006,18 @@ var ChannelDriver = class _ChannelDriver {
8888
10006
  const cached = this.sessionTitles.get(sessionId);
8889
10007
  if (cached != null) return cached;
8890
10008
  try {
8891
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8892
- if (res.ok) {
8893
- const body = await res.json();
8894
- const title = body && typeof body.title === "string" ? body.title.trim() : "";
8895
- if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
8896
- this.sessionTitles.set(sessionId, title);
8897
- return title;
8898
- }
8899
- return null;
10009
+ let title = "";
10010
+ if (this.isV2) {
10011
+ title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
10012
+ } else {
10013
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
10014
+ title = typeof body?.title === "string" ? body.title.trim() : "";
8900
10015
  }
8901
- this.log({
8902
- level: "debug",
8903
- message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
8904
- conversation_id: conversationId
8905
- });
10016
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
10017
+ this.sessionTitles.set(sessionId, title);
10018
+ return title;
10019
+ }
10020
+ return null;
8906
10021
  } catch (err) {
8907
10022
  this.log({
8908
10023
  level: "debug",
@@ -9000,7 +10115,7 @@ var ChannelDriver = class _ChannelDriver {
9000
10115
  * `SessionStatus` only.
9001
10116
  */
9002
10117
  async isAnyDescendantSessionAlive(rootSessionId) {
9003
- const sessions = await listSessions(this.port);
10118
+ const sessions = await this.listSessions();
9004
10119
  if (!sessions) {
9005
10120
  this.log({
9006
10121
  level: "warn",
@@ -9011,7 +10126,7 @@ var ChannelDriver = class _ChannelDriver {
9011
10126
  for (const candidate of sessions) {
9012
10127
  if (!candidate?.id || candidate.id === rootSessionId) continue;
9013
10128
  if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
9014
- const childMsgs = await getSessionMessages(this.port, candidate.id);
10129
+ const childMsgs = await this.getSubagentSessionMessages(candidate.id);
9015
10130
  if (isSessionActivelyGenerating(childMsgs)) {
9016
10131
  return true;
9017
10132
  }
@@ -9073,7 +10188,7 @@ var ChannelDriver = class _ChannelDriver {
9073
10188
  * `isB2AbandonmentConfirmed`.
9074
10189
  */
9075
10190
  async isAnyDescendantSessionOngoing(rootSessionId) {
9076
- const sessions = await listSessions(this.port);
10191
+ const sessions = await this.listSessions();
9077
10192
  if (!sessions) {
9078
10193
  this.log({
9079
10194
  level: "warn",
@@ -9090,7 +10205,7 @@ var ChannelDriver = class _ChannelDriver {
9090
10205
  continue;
9091
10206
  }
9092
10207
  if (membership === false) continue;
9093
- const ongoing = await isSessionOngoing(this.port, candidate.id);
10208
+ const ongoing = await this.isSessionOngoing(candidate.id);
9094
10209
  if (ongoing === true) return true;
9095
10210
  if (ongoing === null) indeterminate = true;
9096
10211
  }
@@ -9366,6 +10481,26 @@ var ChannelDriver = class _ChannelDriver {
9366
10481
  this.clearSubagentInvocationCaches(messageId);
9367
10482
  throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
9368
10483
  }
10484
+ /**
10485
+ * Attempt the terminal report for an accepted prompt whose OpenCode id is not
10486
+ * trackable. Callers use the returned error to keep their path-specific logging
10487
+ * while the same report payload can be retried on a later drain.
10488
+ */
10489
+ async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage3) {
10490
+ try {
10491
+ await this.markFailed(conversationId, messageId, sessionId, errorMessage3);
10492
+ return null;
10493
+ } catch (err) {
10494
+ this.signalUntrackableAck(conversationId, messageId);
10495
+ return err instanceof Error ? err : new Error(String(err));
10496
+ }
10497
+ }
10498
+ /** Emit the untrackable-ack signal at most once while the row remains fenced. */
10499
+ signalUntrackableAck(conversationId, messageId) {
10500
+ if (this.untrackableAckSignalled.has(messageId)) return;
10501
+ this.untrackableAckSignalled.add(messageId);
10502
+ void this.postSignal(conversationId, messageId, "ack_untrackable");
10503
+ }
9369
10504
  /**
9370
10505
  * Mark a message `failed`. `sessionId` / `error` are threaded to the API ONLY
9371
10506
  * when provided (issue #182). Three states for `sessionId`:
@@ -9430,7 +10565,7 @@ var ChannelDriver = class _ChannelDriver {
9430
10565
  const classified = messageFailure(messages, userMessageId);
9431
10566
  if (classified != null) return classified;
9432
10567
  const reply = findLastAssistantReplyFor(messages, userMessageId);
9433
- const hasProvider = await hasAnyConfiguredProvider(this.port);
10568
+ const hasProvider = await this.hasAnyConfiguredProvider();
9434
10569
  return applyZeroProviderFallback(
9435
10570
  classified,
9436
10571
  hasProvider,
@@ -9503,7 +10638,7 @@ var ChannelDriver = class _ChannelDriver {
9503
10638
  const succeededProviders = /* @__PURE__ */ new Set();
9504
10639
  for (const ref of refs) {
9505
10640
  try {
9506
- const childMessages = await getSessionMessages(this.port, ref.sessionId);
10641
+ const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
9507
10642
  if (childMessages === null) {
9508
10643
  this.log({
9509
10644
  level: "debug",
@@ -9834,6 +10969,7 @@ Port ${port} is already in use.`));
9834
10969
 
9835
10970
  // src/commands/ensure-opencode-v2.ts
9836
10971
  import chalk6 from "chalk";
10972
+ import ora3 from "ora";
9837
10973
  import { select as select3 } from "@inquirer/prompts";
9838
10974
  async function probeOpenCode2WithoutPassword(port) {
9839
10975
  try {
@@ -9859,11 +10995,7 @@ function unknownPasswordError(port) {
9859
10995
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
9860
10996
  );
9861
10997
  }
9862
- function v2SessionSupportIncompleteError() {
9863
- return new Error(
9864
- "OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
9865
- );
9866
- }
10998
+ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
9867
10999
  async function ensureOpenCode2Running(ctx) {
9868
11000
  const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
9869
11001
  if (initialHealth.authFailed) {
@@ -9906,13 +11038,37 @@ Port ${port} is already in use.`));
9906
11038
  }
9907
11039
  }
9908
11040
  if (!ctx.interactive) {
9909
- throw v2SessionSupportIncompleteError();
11041
+ ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
11042
+ const { child: proc, password } = await startOpenCode2(port, {
11043
+ inheritStdio: ctx.inheritStdio
11044
+ });
11045
+ const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
11046
+ if (!health.healthy) {
11047
+ return {
11048
+ port,
11049
+ process: proc,
11050
+ version: null,
11051
+ notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
11052
+ password
11053
+ };
11054
+ }
11055
+ ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
11056
+ return {
11057
+ port,
11058
+ process: proc,
11059
+ version: health.version ?? null,
11060
+ notReadyReason: null,
11061
+ password
11062
+ };
9910
11063
  }
9911
- console.log(chalk6.yellow(`
9912
- ${v2SessionSupportIncompleteError().message}`));
9913
11064
  const action = await select3({
9914
11065
  message: "OpenCode V2 is not running. What would you like to do?",
9915
11066
  choices: [
11067
+ {
11068
+ name: "Start OpenCode V2 for me",
11069
+ value: "start",
11070
+ description: `Run 'opencode2 serve --port ${port}'`
11071
+ },
9916
11072
  {
9917
11073
  name: "Show me the command",
9918
11074
  value: "manual",
@@ -9933,6 +11089,25 @@ ${v2SessionSupportIncompleteError().message}`));
9933
11089
  blank();
9934
11090
  throw new Error("Please start OpenCode V2 manually");
9935
11091
  }
11092
+ if (action === "start") {
11093
+ const spinner = ora3("Starting OpenCode V2...").start();
11094
+ const { child: proc, password } = await startOpenCode2(port, {
11095
+ inheritStdio: ctx.inheritStdio
11096
+ });
11097
+ const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
11098
+ if (!health.healthy) {
11099
+ spinner.fail("Failed to start OpenCode V2");
11100
+ throw new Error("OpenCode V2 failed to start");
11101
+ }
11102
+ spinner.stop();
11103
+ return {
11104
+ port,
11105
+ process: proc,
11106
+ version: health.version ?? null,
11107
+ notReadyReason: null,
11108
+ password
11109
+ };
11110
+ }
9936
11111
  return {
9937
11112
  port,
9938
11113
  process: null,
@@ -10826,7 +12001,7 @@ async function driveChannels(state, driver) {
10826
12001
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
10827
12002
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
10828
12003
  if (claudeCredentialApplied || opencodeAuthApplied) {
10829
- void reloadProviderCache(state.port).catch(
12004
+ void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
10830
12005
  (error2) => logActivity(state, {
10831
12006
  type: "error",
10832
12007
  error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
@@ -10951,7 +12126,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
10951
12126
  async function runSweep(state, driver, config) {
10952
12127
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
10953
12128
  try {
10954
- const sessions = await listSessions(state.port);
12129
+ const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
10955
12130
  if (sessions === null) {
10956
12131
  logActivity(state, {
10957
12132
  type: "info",
@@ -10981,7 +12156,7 @@ async function runSweep(state, driver, config) {
10981
12156
  });
10982
12157
  continue;
10983
12158
  }
10984
- if (await deleteSession(state.port, id)) deleted++;
12159
+ if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
10985
12160
  else failed++;
10986
12161
  }
10987
12162
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
@@ -11488,6 +12663,9 @@ async function run(options) {
11488
12663
  connected: false,
11489
12664
  opencodeConnected: false,
11490
12665
  opencodeVersion: null,
12666
+ opencodeApiVersion: "v1",
12667
+ opencodePassword: null,
12668
+ opencodeClient: null,
11491
12669
  sessionDbProvenanceAnomaly: false,
11492
12670
  opencodeProcess: null,
11493
12671
  stopOpenCodeLogTail: null,
@@ -11642,7 +12820,7 @@ async function run(options) {
11642
12820
  console.log(chalk7.bold("Evident Run"));
11643
12821
  console.log(chalk7.dim("-".repeat(40)));
11644
12822
  }
11645
- const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
12823
+ const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
11646
12824
  let validation = await getAgentInfo(state.agentId, state.authHeader);
11647
12825
  if (!validation.valid && validation.authFailed && interactive) {
11648
12826
  spinner?.fail("Authentication failed");
@@ -11763,7 +12941,7 @@ async function run(options) {
11763
12941
  logActivity(state, { type: "info", level: "warn", message: warning2 });
11764
12942
  }
11765
12943
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
11766
- const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
12944
+ const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
11767
12945
  try {
11768
12946
  const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
11769
12947
  port: state.port,
@@ -11783,6 +12961,19 @@ async function run(options) {
11783
12961
  state.port = oc.port;
11784
12962
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
11785
12963
  state.opencodeVersion = oc.version;
12964
+ state.opencodeApiVersion = opencodeVersion;
12965
+ let opencodePassword = null;
12966
+ if (opencodeVersion === "v2" && "password" in oc) {
12967
+ const value = oc.password;
12968
+ if (typeof value === "string" || value === null) opencodePassword = value;
12969
+ }
12970
+ state.opencodePassword = opencodePassword;
12971
+ const openCodeClient = createOpenCodeClient({
12972
+ port: state.port,
12973
+ version: state.opencodeApiVersion,
12974
+ password: state.opencodePassword
12975
+ });
12976
+ state.opencodeClient = openCodeClient;
11786
12977
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
11787
12978
  try {
11788
12979
  writeFileSync6(options.opencodePidFile, `${oc.process.pid}
@@ -11819,16 +13010,19 @@ async function run(options) {
11819
13010
  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}).`;
11820
13011
  logActivity(state, { type: "info", level: "warn", message });
11821
13012
  } else {
11822
- const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
13013
+ const versionWarning = buildOpenCodeVersionWarning(
13014
+ state.opencodeVersion,
13015
+ state.opencodeApiVersion
13016
+ );
11823
13017
  if (versionWarning) {
11824
13018
  log2(state, versionWarning, "warn");
11825
13019
  if (state.interactive && !state.json) {
11826
13020
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
11827
13021
  }
11828
13022
  }
11829
- await reloadProviderCache(state.port);
13023
+ await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
11830
13024
  const noProviderWarning = buildNoProviderWarning(
11831
- await hasAnyConfiguredProvider(state.port)
13025
+ await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
11832
13026
  );
11833
13027
  if (noProviderWarning) {
11834
13028
  log2(state, noProviderWarning, "warn");
@@ -11951,11 +13145,12 @@ async function run(options) {
11951
13145
  });
11952
13146
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
11953
13147
  }
11954
- const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
13148
+ const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
11955
13149
  const channelDriver = new ChannelDriver({
11956
13150
  agentId: state.agentId,
11957
13151
  port: state.port,
11958
13152
  apiUrl: getApiUrlConfig(),
13153
+ openCodeClient: state.opencodeClient ?? void 0,
11959
13154
  getAuthHeader: () => state.authHeader,
11960
13155
  conversationFilter: state.conversationFilter,
11961
13156
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
@@ -11981,6 +13176,7 @@ async function run(options) {
11981
13176
  agentId: state.agentId,
11982
13177
  getAuthHeader: () => state.authHeader,
11983
13178
  port: state.port,
13179
+ openCodePassword: state.opencodePassword,
11984
13180
  isRunning: () => state.running,
11985
13181
  events: {
11986
13182
  onConnected: (agentId, isReconnect) => {
@@ -12005,7 +13201,11 @@ async function run(options) {
12005
13201
  emitAgentConnected(state.agentId, {
12006
13202
  port: state.port,
12007
13203
  cli_version: getCliVersion(),
12008
- opencode_version: state.opencodeVersion
13204
+ opencode_version: reportedOpenCodeVersion({
13205
+ version: state.opencodeVersion,
13206
+ major: state.opencodeApiVersion,
13207
+ connected: state.opencodeConnected
13208
+ })
12009
13209
  });
12010
13210
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
12011
13211
  if (state.interactive) displayStatus(state);
@@ -12121,7 +13321,7 @@ async function run(options) {
12121
13321
  state.openaiUsageTimer = timer;
12122
13322
  },
12123
13323
  fetchUsage: async () => {
12124
- const usage = await getOpenAiUsage(state.port);
13324
+ const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
12125
13325
  if (usage.subscription === null) {
12126
13326
  logActivity(state, {
12127
13327
  type: "info",