@evident-ai/cli 3.4.1-dev.69d24ff → 3.4.1-dev.71ade33

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.4.1-dev.69d24ff" : void 0) ?? process.env.npm_package_version ?? "unknown";
1216
+ var CLI_VERSION = (true ? "3.4.1-dev.71ade33" : 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";
@@ -2363,6 +2475,7 @@ function reportedOpenCodeVersion(input) {
2363
2475
 
2364
2476
  // src/lib/opencode/process.ts
2365
2477
  import { execSync, spawn as spawn3 } from "child_process";
2478
+ import { randomBytes } from "node:crypto";
2366
2479
 
2367
2480
  // src/lib/process-stop.ts
2368
2481
  async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
@@ -2421,6 +2534,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2421
2534
  // src/lib/opencode/process.ts
2422
2535
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2423
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
+ ]);
2424
2548
  function resolveOpenCodeLogLevel(env) {
2425
2549
  const raw = env.OPENCODE_LOG_LEVEL;
2426
2550
  if (!raw) return "INFO";
@@ -2431,6 +2555,16 @@ function resolveOpenCodeLogLevel(env) {
2431
2555
  );
2432
2556
  return "INFO";
2433
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
+ }
2434
2568
  function getProcessCwd(pid) {
2435
2569
  const platform = process.platform;
2436
2570
  try {
@@ -2613,6 +2747,37 @@ async function startOpenCode(port, options = {}) {
2613
2747
  });
2614
2748
  return child;
2615
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
+ }
2616
2781
  function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
2617
2782
  const sendSignal = (signal) => {
2618
2783
  if (process.platform === "win32") {
@@ -2760,27 +2925,500 @@ function buildNoProviderWarning(hasProvider) {
2760
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).";
2761
2926
  }
2762
2927
 
2763
- // src/lib/http-timeout.ts
2764
- var REQUEST_TIMEOUT_MS = 6e4;
2765
- function withRequestTimeout(fetchImpl, timeoutMs) {
2766
- 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
+ }
2767
3398
  }
2768
3399
 
2769
3400
  // src/lib/opencode/session.ts
3401
+ var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
2770
3402
  function timedFetch(input, init) {
2771
3403
  return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
2772
3404
  }
3405
+ function requestWithClient(port, client, path, init, options) {
3406
+ return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
3407
+ }
2773
3408
  function opencodeBase(port) {
2774
3409
  return `http://127.0.0.1:${port}`;
2775
3410
  }
2776
- async function getOpenCodeDirectory(port) {
3411
+ async function getOpenCodeDirectory(port, client) {
2777
3412
  try {
2778
- const res = await timedFetch(`${opencodeBase(port)}/path`);
3413
+ const res = await requestWithClient(port, client, "/path");
2779
3414
  if (!res.ok) return null;
2780
3415
  const body = await res.json();
2781
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;
2782
3417
  return dir && dir.trim() ? dir.trim() : null;
2783
- } catch {
3418
+ } catch (error2) {
3419
+ console.error(
3420
+ `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3421
+ );
2784
3422
  return null;
2785
3423
  }
2786
3424
  }
@@ -2824,16 +3462,48 @@ function isAssistantInFlight(m) {
2824
3462
  if (completedOf(m) == null) return true;
2825
3463
  return finishOf(m) === "tool-calls";
2826
3464
  }
2827
- async function getSessionMessages(port, sessionId) {
3465
+ async function getSessionMessages(port, sessionId, client) {
2828
3466
  try {
2829
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/message`);
3467
+ const path = `/session/${sessionId}/message`;
3468
+ const res = await requestWithClient(port, client, path);
2830
3469
  if (!res.ok) return null;
2831
3470
  const body = await res.json();
2832
3471
  return Array.isArray(body) ? body : null;
2833
- } catch {
3472
+ } catch (error2) {
3473
+ console.error(
3474
+ `[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3475
+ );
2834
3476
  return null;
2835
3477
  }
2836
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
+ }
2837
3507
  function isSessionActivelyGenerating(messages) {
2838
3508
  if (!messages || messages.length === 0) return false;
2839
3509
  const last = messages[messages.length - 1];
@@ -2854,27 +3524,37 @@ function sessionLastActivityMs(session) {
2854
3524
  }
2855
3525
  return null;
2856
3526
  }
2857
- async function listSessions(port) {
3527
+ async function listSessions(port, client) {
3528
+ if (client?.version === "v2") return listV2Sessions(client);
2858
3529
  try {
2859
- const res = await timedFetch(`${opencodeBase(port)}/session`);
3530
+ const res = await requestWithClient(port, client, "/session");
2860
3531
  if (!res.ok) return null;
2861
3532
  const body = await res.json();
2862
3533
  return Array.isArray(body) ? body : null;
2863
- } catch {
3534
+ } catch (error2) {
3535
+ console.error(
3536
+ `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3537
+ );
2864
3538
  return null;
2865
3539
  }
2866
3540
  }
2867
- async function deleteSession(port, id) {
3541
+ async function deleteSession(port, id, client) {
3542
+ if (client?.version === "v2") return deleteV2Session(client, id);
2868
3543
  try {
2869
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`, { method: "DELETE" });
3544
+ const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2870
3545
  return res.status >= 200 && res.status < 300;
2871
- } catch {
3546
+ } catch (error2) {
3547
+ console.error(
3548
+ `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3549
+ );
2872
3550
  return false;
2873
3551
  }
2874
3552
  }
2875
- async function sessionExists(port, id) {
3553
+ async function sessionExists(port, id, client) {
2876
3554
  try {
2877
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`);
3555
+ const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
3556
+ allowStatuses: [404]
3557
+ });
2878
3558
  if (res.status >= 200 && res.status < 300) return true;
2879
3559
  if (res.status === 404) return false;
2880
3560
  return null;
@@ -2882,9 +3562,22 @@ async function sessionExists(port, id) {
2882
3562
  return null;
2883
3563
  }
2884
3564
  }
2885
- 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;
2886
3579
  try {
2887
- const res = await timedFetch(`${opencodeBase(port)}/session/status`);
3580
+ const res = await requestWithClient(port, client, "/session/status");
2888
3581
  if (!res.ok) {
2889
3582
  console.error(
2890
3583
  `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
@@ -2906,22 +3599,28 @@ async function getSessionStatuses(port) {
2906
3599
  return null;
2907
3600
  }
2908
3601
  }
2909
- async function isSessionOngoing(port, id) {
2910
- 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);
2911
3605
  if (map == null) return null;
2912
3606
  const entry = map[id];
2913
3607
  return entry != null && entry.type !== "idle";
2914
3608
  }
2915
- async function createOpenCodeSession(port, directory) {
2916
- const url = new URL(`${opencodeBase(port)}/session`);
2917
- if (directory && directory.trim()) {
2918
- url.searchParams.set("directory", directory.trim());
2919
- }
2920
- const response = await timedFetch(url, {
2921
- method: "POST",
2922
- headers: { "Content-Type": "application/json" },
2923
- body: JSON.stringify({})
2924
- });
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
+ );
2925
3624
  if (!response.ok) {
2926
3625
  const text = await response.text().catch(() => "");
2927
3626
  throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
@@ -2929,10 +3628,16 @@ async function createOpenCodeSession(port, directory) {
2929
3628
  const data = await response.json();
2930
3629
  return data.id;
2931
3630
  }
2932
- async function getModelAttachmentCapability(port, model) {
3631
+ async function getModelAttachmentCapability(port, model, client) {
2933
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
+ }
2934
3639
  try {
2935
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
3640
+ const res = await requestWithClient(port, client, "/config/providers");
2936
3641
  if (!res.ok) {
2937
3642
  console.error(
2938
3643
  `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3050,21 +3755,45 @@ function applyModelOptions(body, options) {
3050
3755
  };
3051
3756
  }
3052
3757
  }
3053
- if (variant) body.variant = variant;
3758
+ if (variant) body.variant = variant;
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
+ }
3054
3783
  }
3055
3784
  function messageText(m) {
3056
3785
  if (!m || !Array.isArray(m.parts)) return "";
3057
3786
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
3058
3787
  }
3059
- async function sendPromptAsync(port, sessionId, content, options, attachments) {
3060
- const before = await getSessionMessages(port, sessionId);
3788
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3789
+ const before = await getSessionMessages(port, sessionId, client);
3061
3790
  const knownUserIds = new Set(
3062
3791
  (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3063
3792
  );
3064
3793
  const parts = [{ type: "text", text: content }];
3065
3794
  let pendingOutcomes = null;
3066
3795
  if (attachments && attachments.inputs.length > 0) {
3067
- const capable = await getModelAttachmentCapability(port, options?.model);
3796
+ const capable = await getModelAttachmentCapability(port, options?.model, client);
3068
3797
  const {
3069
3798
  parts: fileParts,
3070
3799
  outcomes,
@@ -3077,11 +3806,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3077
3806
  parts
3078
3807
  };
3079
3808
  applyModelOptions(body, options);
3080
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
3081
- method: "POST",
3082
- headers: { "Content-Type": "application/json" },
3083
- body: JSON.stringify(body)
3084
- });
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
+ );
3085
3820
  if (res.status < 200 || res.status >= 300) {
3086
3821
  const text = await res.text().catch(() => "");
3087
3822
  const { variant } = splitModelVariant(options?.model);
@@ -3092,7 +3827,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3092
3827
  const READ_BACK_ATTEMPTS = 5;
3093
3828
  const READ_BACK_DELAY_MS = 150;
3094
3829
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
3095
- const after = await getSessionMessages(port, sessionId);
3830
+ const after = await getSessionMessages(port, sessionId, client);
3096
3831
  if (after) {
3097
3832
  let best = null;
3098
3833
  for (const m of after) {
@@ -3195,7 +3930,7 @@ function collectSubagentSessions(messages, userMessageId) {
3195
3930
  }
3196
3931
  return refs;
3197
3932
  }
3198
- function finiteNumber(value) {
3933
+ function finiteNumber2(value) {
3199
3934
  return typeof value === "number" && Number.isFinite(value) ? value : null;
3200
3935
  }
3201
3936
  function taskCallModel(value) {
@@ -3224,8 +3959,8 @@ function collectTaskCalls(messages, userMessageId) {
3224
3959
  parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3225
3960
  model: taskCallModel(metadata?.model),
3226
3961
  status: part.state.status ?? "unknown",
3227
- timeStart: finiteNumber(part.state.time?.start),
3228
- timeEnd: finiteNumber(part.state.time?.end)
3962
+ timeStart: finiteNumber2(part.state.time?.start),
3963
+ timeEnd: finiteNumber2(part.state.time?.end)
3229
3964
  });
3230
3965
  }
3231
3966
  }
@@ -3240,7 +3975,7 @@ function attributeTaskCallUsage(messages, windows) {
3240
3975
  const unattributed = [];
3241
3976
  for (const message of messages ?? []) {
3242
3977
  if (roleOf(message) !== "assistant") continue;
3243
- const created = finiteNumber(createdOf(message));
3978
+ const created = finiteNumber2(createdOf(message));
3244
3979
  const matching = created === null ? [] : eligibleWindows.filter(
3245
3980
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3246
3981
  );
@@ -3481,9 +4216,51 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
3481
4216
  }
3482
4217
  return hasLaterUser && hasStartedLaterUser;
3483
4218
  }
3484
- async function hasAnyConfiguredProvider(port) {
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
+ }
3485
4262
  try {
3486
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
4263
+ const res = await requestWithClient(port, client, "/config/providers");
3487
4264
  if (!res.ok) {
3488
4265
  console.error(
3489
4266
  `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3512,7 +4289,7 @@ async function hasAnyConfiguredProvider(port) {
3512
4289
  return null;
3513
4290
  }
3514
4291
  }
3515
- function sessionErrorReason(error2) {
4292
+ function sessionErrorReason2(error2) {
3516
4293
  const record = typeof error2 === "object" && error2 !== null ? error2 : null;
3517
4294
  const data = record?.data;
3518
4295
  const dataRecord = typeof data === "object" && data !== null ? data : null;
@@ -3542,16 +4319,20 @@ function parseSessionErrorFrame(data) {
3542
4319
  if (typeof sessionId !== "string" || sessionId.length === 0) return null;
3543
4320
  return {
3544
4321
  sessionId,
3545
- reason: sessionErrorReason(propertiesRecord.error)
4322
+ reason: sessionErrorReason2(propertiesRecord.error)
3546
4323
  };
3547
4324
  }
3548
- async function readSessionErrorStream(port, options) {
4325
+ async function readSessionErrorStream(port, options, client) {
4326
+ if (client?.version === "v2") return readV2SessionErrorStream(client, options);
3549
4327
  let reader = null;
3550
4328
  try {
3551
- const response = await fetch(`${opencodeBase(port)}/event`, {
4329
+ const response = await (client?.request("/event", {
3552
4330
  headers: { accept: "text/event-stream" },
3553
4331
  signal: options.signal
3554
- });
4332
+ }) ?? fetch(`${opencodeBase(port)}/event`, {
4333
+ headers: { accept: "text/event-stream" },
4334
+ signal: options.signal
4335
+ }));
3555
4336
  if (!response.ok || !response.body) {
3556
4337
  return { reason: "unavailable", detail: `HTTP ${response.status}` };
3557
4338
  }
@@ -3582,9 +4363,10 @@ async function readSessionErrorStream(port, options) {
3582
4363
  if (reader) void reader.cancel().catch(() => void 0);
3583
4364
  }
3584
4365
  }
3585
- async function reloadProviderCache(port) {
4366
+ async function reloadProviderCache(port, client) {
4367
+ if (client?.version === "v2") return;
3586
4368
  try {
3587
- const res = await timedFetch(`${opencodeBase(port)}/config`, {
4369
+ const res = await requestWithClient(port, client, "/config", {
3588
4370
  method: "PATCH",
3589
4371
  headers: { "Content-Type": "application/json" },
3590
4372
  body: JSON.stringify({})
@@ -3962,12 +4744,14 @@ var STRIP_RES = /* @__PURE__ */ new Set([
3962
4744
  "content-length"
3963
4745
  ]);
3964
4746
  var StreamForwarder = class {
3965
- constructor(ws, port, callbacks = {}) {
4747
+ constructor(ws, port, callbacks = {}, options = {}) {
3966
4748
  this.ws = ws;
3967
4749
  this.port = port;
3968
4750
  this.callbacks = callbacks;
4751
+ this.options = options;
3969
4752
  }
3970
4753
  inflight = /* @__PURE__ */ new Map();
4754
+ binaryFramesSupported = false;
3971
4755
  /**
3972
4756
  * Handle an edge→agent frame. Unknown frame types are ignored.
3973
4757
  */
@@ -3987,6 +4771,12 @@ var StreamForwarder = class {
3987
4771
  break;
3988
4772
  }
3989
4773
  }
4774
+ handleBinaryBodyFrame(sid, payload) {
4775
+ this.inflight.get(sid)?.pushBody?.(payload);
4776
+ }
4777
+ setBinaryFramesSupported(supported) {
4778
+ this.binaryFramesSupported = supported;
4779
+ }
3990
4780
  /**
3991
4781
  * Abort every in-flight stream (e.g. on WebSocket close).
3992
4782
  */
@@ -4049,7 +4839,15 @@ var StreamForwarder = class {
4049
4839
  }
4050
4840
  const fwdHeaders = {};
4051
4841
  for (const [k, v] of Object.entries(headers ?? {})) {
4052
- 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);
4053
4851
  }
4054
4852
  this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
4055
4853
  const body = bodyPromise ? await bodyPromise : void 0;
@@ -4096,7 +4894,13 @@ var StreamForwarder = class {
4096
4894
  const chunk = Buffer.from(value);
4097
4895
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
4098
4896
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
4099
- 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
+ }
4100
4904
  }
4101
4905
  }
4102
4906
  }
@@ -4113,6 +4917,11 @@ var StreamForwarder = class {
4113
4917
 
4114
4918
  // src/lib/tunnel/connection.ts
4115
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
+ }
4116
4925
  var TunnelUpgradeRejectedError = class extends Error {
4117
4926
  constructor(message, reason) {
4118
4927
  super(message);
@@ -4162,6 +4971,7 @@ function connectTunnel(options) {
4162
4971
  agentId,
4163
4972
  authHeader,
4164
4973
  port,
4974
+ openCodePassword,
4165
4975
  onConnected,
4166
4976
  onDisconnected,
4167
4977
  onError,
@@ -4176,14 +4986,20 @@ function connectTunnel(options) {
4176
4986
  return new Promise((resolve4, reject) => {
4177
4987
  const ws = new WebSocket2(url, {
4178
4988
  headers: {
4179
- Authorization: authHeader
4989
+ Authorization: authHeader,
4990
+ "X-Evident-Tunnel-Binary-Frames": "1"
4180
4991
  }
4181
4992
  });
4182
- const forwarder = new StreamForwarder(ws, port, {
4183
- onHead: () => onResponse?.(),
4184
- onDrainPing: () => onDrainPing?.(),
4185
- onUsageRearmPing: () => onUsageRearmPing?.()
4186
- });
4993
+ const forwarder = new StreamForwarder(
4994
+ ws,
4995
+ port,
4996
+ {
4997
+ onHead: () => onResponse?.(),
4998
+ onDrainPing: () => onDrainPing?.(),
4999
+ onUsageRearmPing: () => onUsageRearmPing?.()
5000
+ },
5001
+ { openCodePassword }
5002
+ );
4187
5003
  const connectionTimeout = setTimeout(() => {
4188
5004
  ws.close();
4189
5005
  reject(new Error("Connection timeout"));
@@ -4220,7 +5036,25 @@ function connectTunnel(options) {
4220
5036
  ws.on("open", () => {
4221
5037
  onInfo?.("WebSocket connection established");
4222
5038
  });
4223
- 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
+ }
4224
5058
  let message;
4225
5059
  try {
4226
5060
  message = JSON.parse(data.toString());
@@ -4236,6 +5070,7 @@ function connectTunnel(options) {
4236
5070
  switch (message.type) {
4237
5071
  case "connected": {
4238
5072
  clearTimeout(connectionTimeout);
5073
+ forwarder.setBinaryFramesSupported(message.binary_frames === true);
4239
5074
  const connectedAgentId = message.agent_id ?? agentId;
4240
5075
  onConnected?.(connectedAgentId);
4241
5076
  resolve4({
@@ -4326,6 +5161,7 @@ var RunnerConnection = class {
4326
5161
  agentId: this.resolvedAgentId,
4327
5162
  authHeader: this.opts.getAuthHeader(),
4328
5163
  port: this.opts.port,
5164
+ openCodePassword: this.opts.openCodePassword,
4329
5165
  onConnected: (agentId) => {
4330
5166
  this.reconnectAttempt = 0;
4331
5167
  this.reconnecting = false;
@@ -4506,33 +5342,73 @@ function parseCodexUsageHeaders(headers) {
4506
5342
  function normalizeProbeModel(model) {
4507
5343
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4508
5344
  }
4509
- 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) {
4510
5353
  try {
4511
- const res = await withRequestTimeout(
4512
- fetch,
4513
- REQUEST_TIMEOUT_MS
4514
- )(`${opencodeBase(port)}/config/providers`);
4515
- if (!res.ok) {
4516
- console.error(
4517
- `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
4518
- );
4519
- 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);
4520
5358
  }
4521
- const body = await res.json();
4522
- const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
4523
- 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;
4524
5364
  const candidates = [
4525
- ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
5365
+ ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4526
5366
  ...Object.keys(provider.models)
4527
5367
  ].map(normalizeProbeModel);
4528
- return [...new Set(candidates)].slice(0, 4);
5368
+ return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
4529
5369
  } catch (err) {
4530
- console.error(
4531
- `[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
4532
5406
  );
4533
- return [];
4534
5407
  }
4535
5408
  }
5409
+ async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
5410
+ return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
5411
+ }
4536
5412
  function hasPrimaryHeaders(headers) {
4537
5413
  return [
4538
5414
  "x-codex-primary-used-percent",
@@ -4540,7 +5416,7 @@ function hasPrimaryHeaders(headers) {
4540
5416
  "x-codex-primary-reset-at"
4541
5417
  ].some((name) => headers.has(name));
4542
5418
  }
4543
- async function getOpenAiUsage(port) {
5419
+ async function getOpenAiUsage(port, client) {
4544
5420
  const credentials2 = readOpenCodeChatGptCredentials();
4545
5421
  if (!credentials2) {
4546
5422
  throw new OpenAiUsageError(
@@ -4555,12 +5431,16 @@ async function getOpenAiUsage(port) {
4555
5431
  );
4556
5432
  }
4557
5433
  const subscription = parseChatGptIdentity(credentials2.accessToken);
4558
- const models = await resolveProbeModels(port);
4559
- if (models.length === 0) {
4560
- 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
+ );
4561
5441
  }
4562
5442
  let lastStatus;
4563
- for (const model of models) {
5443
+ for (const model of lookup.models) {
4564
5444
  let res;
4565
5445
  try {
4566
5446
  res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
@@ -5379,6 +6259,7 @@ var ChannelDriver = class _ChannelDriver {
5379
6259
  maxActiveSessions;
5380
6260
  watcherStallMs;
5381
6261
  wedgeWarningIntervalMs;
6262
+ openCodeClient;
5382
6263
  /** Cache of conversationId → opencode sessionId. */
5383
6264
  sessions = /* @__PURE__ */ new Map();
5384
6265
  /**
@@ -5487,20 +6368,23 @@ var ChannelDriver = class _ChannelDriver {
5487
6368
  */
5488
6369
  readopted = /* @__PURE__ */ new Set();
5489
6370
  /**
5490
- * "Don't re-DISPATCH / re-attach this orphan again" (Bug 2/5). Set when a
5491
- * re-adopted running/orphan row's watcher hit its `processed_at`-anchored
5492
- * deadline (or an orphan whose window already elapsed): the still-`processing`
5493
- * server row would otherwise be re-adopted (and re-dispatched) on EVERY ~2s
5494
- * drain until the 15-min cron resets it spamming new turns.
5495
- *
5496
- * CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
5497
- * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
5498
- * in opencode must still be delivered via `markDone` on the next drain — so
5499
- * `readoptOne` computes `state` FIRST and this set is checked only on the
5500
- * non-done path. It is cleared once the row leaves the processing list (cron
5501
- * 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.
5502
6377
  */
5503
6378
  dontRedispatch = /* @__PURE__ */ new Set();
6379
+ /**
6380
+ * Untrackable-ack fence. Set after OpenCode accepts a prompt without returning a
6381
+ * usable message id, because another POST could create a duplicate turn. Keep it
6382
+ * fenced while the row is `processing` or `pending`; clear it only when the row
6383
+ * is absent from both lists.
6384
+ */
6385
+ untrackableAck = /* @__PURE__ */ new Set();
6386
+ /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6387
+ pendingMessageIds = /* @__PURE__ */ new Set();
5504
6388
  /**
5505
6389
  * "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
5506
6390
  * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
@@ -5623,7 +6507,8 @@ var ChannelDriver = class _ChannelDriver {
5623
6507
  */
5624
6508
  attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
5625
6509
  /**
5626
- * Cache of the opencode root directory (from `GET /path`). Resolved lazily on
6510
+ * Cache of the opencode root directory from the selected client's location lookup.
6511
+ * Resolved lazily on
5627
6512
  * first session creation so drain-created sessions are rooted at the project
5628
6513
  * directory and thus visible in `opencode web`'s session list. `undefined` =
5629
6514
  * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
@@ -5717,6 +6602,11 @@ var ChannelDriver = class _ChannelDriver {
5717
6602
  config.fetchImpl ?? fetch,
5718
6603
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
5719
6604
  );
6605
+ this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
6606
+ port: config.port,
6607
+ version: "v1",
6608
+ fetchImpl: config.fetchImpl
6609
+ });
5720
6610
  this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5721
6611
  this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
5722
6612
  this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
@@ -5728,9 +6618,39 @@ var ChannelDriver = class _ChannelDriver {
5728
6618
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5729
6619
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
5730
6620
  }
5731
- /** The IPv4-loopback base URL for the local `opencode serve`. */
5732
- get opencodeBase() {
5733
- return `http://127.0.0.1:${this.port}`;
6621
+ get isV2() {
6622
+ return this.openCodeClient.version === "v2";
6623
+ }
6624
+ async getSessionMessages(sessionId) {
6625
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6626
+ }
6627
+ async getSubagentSessionMessages(sessionId) {
6628
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
6629
+ }
6630
+ async getTelemetrySubagentSessionMessages(sessionId) {
6631
+ if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
6632
+ return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6633
+ }
6634
+ async listSessions() {
6635
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
6636
+ }
6637
+ async sessionExists(sessionId) {
6638
+ return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
6639
+ }
6640
+ async isSessionOngoing(sessionId) {
6641
+ return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
6642
+ }
6643
+ async getOpenCodeDirectory() {
6644
+ return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
6645
+ }
6646
+ async createOpenCodeSession(directory) {
6647
+ return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
6648
+ }
6649
+ async hasAnyConfiguredProvider() {
6650
+ return hasAnyConfiguredProvider(this.port, this.openCodeClient);
6651
+ }
6652
+ async readOpenCodeSessionErrorStream(options) {
6653
+ return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
5734
6654
  }
5735
6655
  /**
5736
6656
  * Drain all pending channel conversations once: poll → dispatch → register.
@@ -5808,6 +6728,7 @@ var ChannelDriver = class _ChannelDriver {
5808
6728
  async runDrain() {
5809
6729
  let dispatched = 0;
5810
6730
  try {
6731
+ this.pendingMessageIds.clear();
5811
6732
  const conversations = await this.getPendingConversations();
5812
6733
  if (this.recycleRequestedFlag) {
5813
6734
  this.stop();
@@ -6032,6 +6953,7 @@ var ChannelDriver = class _ChannelDriver {
6032
6953
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6033
6954
  this.ensureSessionErrorStream();
6034
6955
  const messages = await this.getPendingMessages(conv.id);
6956
+ for (const message of messages) this.pendingMessageIds.add(message.id);
6035
6957
  let dispatched = 0;
6036
6958
  let skippedAlreadyDispatched = 0;
6037
6959
  if (refusedSessionId && messages.length > 0) {
@@ -6045,6 +6967,15 @@ var ChannelDriver = class _ChannelDriver {
6045
6967
  skippedAlreadyDispatched += 1;
6046
6968
  continue;
6047
6969
  }
6970
+ if (this.untrackableAck.has(message.id)) {
6971
+ this.log({
6972
+ level: "warn",
6973
+ message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
6974
+ conversation_id: conv.id,
6975
+ message_id: message.id
6976
+ });
6977
+ break;
6978
+ }
6048
6979
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
6049
6980
  if (effectiveOpencodeMessageId) {
6050
6981
  const outcome = await this.resolveRedrive(
@@ -6073,15 +7004,55 @@ var ChannelDriver = class _ChannelDriver {
6073
7004
  conversation_id: conv.id,
6074
7005
  message_id: message.id
6075
7006
  });
6076
- const sendAttachments = this.buildSendAttachments(conv, message);
6077
- opencodeMessageId = await this.dispatchLocked(
7007
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
7008
+ if (this.isV2 && message.attachments && message.attachments.length > 0) {
7009
+ this.signalAttachmentsSkipped(
7010
+ conv.id,
7011
+ message.id,
7012
+ message.attachments.map((attachment, index) => ({
7013
+ index,
7014
+ mime: attachment.mime,
7015
+ ...attachment.filename ? { filename: attachment.filename } : {},
7016
+ status: "skipped"
7017
+ })),
7018
+ false
7019
+ );
7020
+ }
7021
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
6078
7022
  sessionId,
6079
- () => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
7023
+ () => sendPromptAsync(
7024
+ this.port,
7025
+ sessionId,
7026
+ message.content,
7027
+ options,
7028
+ sendAttachments,
7029
+ this.openCodeClient
7030
+ )
6080
7031
  );
6081
7032
  } catch (err) {
6082
7033
  if (err instanceof ChannelAuthError) throw err;
7034
+ if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
7035
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
7036
+ this.untrackableAck.add(message.id);
7037
+ this.log({
7038
+ level: "error",
7039
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
7040
+ conversation_id: conv.id,
7041
+ message_id: message.id
7042
+ });
7043
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
7044
+ this.log({
7045
+ level: "warn",
7046
+ message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
7047
+ conversation_id: conv.id,
7048
+ message_id: message.id
7049
+ });
7050
+ void this.postSignal(conv.id, message.id, "ack_untrackable");
7051
+ });
7052
+ break;
7053
+ }
6083
7054
  this.dispatched.delete(message.id);
6084
- const exists = await sessionExists(this.port, sessionId);
7055
+ const exists = await this.sessionExists(sessionId);
6085
7056
  if (exists === false) {
6086
7057
  this.sessions.delete(conv.id);
6087
7058
  this.log({
@@ -6130,6 +7101,9 @@ var ChannelDriver = class _ChannelDriver {
6130
7101
  break;
6131
7102
  }
6132
7103
  if (opencodeMessageId === null) {
7104
+ if (this.isV2) {
7105
+ throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7106
+ }
6133
7107
  const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
6134
7108
  if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
6135
7109
  this.log({
@@ -6277,29 +7251,38 @@ var ChannelDriver = class _ChannelDriver {
6277
7251
  */
6278
7252
  async pollSessionMessagesForRedrive(conv, message, sessionId) {
6279
7253
  try {
6280
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
6281
- if (!res.ok) {
6282
- const rawBody = await res.text();
6283
- const normalized = normalizeRedrivePollFailureBody(rawBody);
6284
- this.log({
6285
- level: "warn",
6286
- 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`,
6287
- conversation_id: conv.id,
6288
- message_id: message.id
6289
- });
6290
- return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ""}` };
7254
+ if (this.isV2) {
7255
+ const messages = await this.getSessionMessages(sessionId);
7256
+ if (messages === null) {
7257
+ this.log({
7258
+ level: "warn",
7259
+ 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`,
7260
+ conversation_id: conv.id,
7261
+ message_id: message.id
7262
+ });
7263
+ return { ok: false, signature: null };
7264
+ }
7265
+ return { ok: true, messages };
6291
7266
  }
6292
- const body = await res.json();
6293
- if (!Array.isArray(body)) {
7267
+ const polledV1 = await pollSessionMessagesForRedrive(
7268
+ this.port,
7269
+ sessionId,
7270
+ this.openCodeClient
7271
+ );
7272
+ if (!polledV1.ok) {
7273
+ const normalized = normalizeRedrivePollFailureBody(polledV1.body);
6294
7274
  this.log({
6295
7275
  level: "warn",
6296
- 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`,
7276
+ 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`,
6297
7277
  conversation_id: conv.id,
6298
7278
  message_id: message.id
6299
7279
  });
6300
- return { ok: false, signature: "non-array message body" };
7280
+ return {
7281
+ ok: false,
7282
+ signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
7283
+ };
6301
7284
  }
6302
- return { ok: true, messages: body };
7285
+ return { ok: true, messages: polledV1.messages };
6303
7286
  } catch (err) {
6304
7287
  this.log({
6305
7288
  level: "warn",
@@ -6358,11 +7341,11 @@ var ChannelDriver = class _ChannelDriver {
6358
7341
  }
6359
7342
  const state = messageRunState(messages, ocId ?? "");
6360
7343
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
6361
- const ongoing = await isSessionOngoing(this.port, sessionId);
7344
+ const ongoing = await this.isSessionOngoing(sessionId);
6362
7345
  if (ongoing === false) {
6363
7346
  this.log({
6364
7347
  level: "info",
6365
- 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`,
7348
+ 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`,
6366
7349
  conversation_id: conv.id,
6367
7350
  message_id: message.id
6368
7351
  });
@@ -6375,7 +7358,7 @@ var ChannelDriver = class _ChannelDriver {
6375
7358
  return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
6376
7359
  }
6377
7360
  if (state === "running" || state === "queued") {
6378
- const ongoing = await isSessionOngoing(this.port, sessionId);
7361
+ const ongoing = await this.isSessionOngoing(sessionId);
6379
7362
  if (ongoing === true) {
6380
7363
  if (state === "queued") {
6381
7364
  const siblingOcIds = this.siblingOpencodeMessageIds(
@@ -6835,7 +7818,7 @@ var ChannelDriver = class _ChannelDriver {
6835
7818
  };
6836
7819
  }
6837
7820
  if (bound) {
6838
- const exists = await sessionExists(this.port, bound);
7821
+ const exists = await this.sessionExists(bound);
6839
7822
  if (exists === false) {
6840
7823
  this.log({
6841
7824
  level: "debug",
@@ -6868,7 +7851,7 @@ var ChannelDriver = class _ChannelDriver {
6868
7851
  */
6869
7852
  async createAndBindSession(conversationId) {
6870
7853
  const directory = await this.resolveOpenCodeDirectory();
6871
- const sessionId = await createOpenCodeSession(this.port, directory);
7854
+ const sessionId = await this.createOpenCodeSession(directory);
6872
7855
  this.sessions.set(conversationId, sessionId);
6873
7856
  await this.persistSession(conversationId, sessionId).catch((err) => {
6874
7857
  this.log({
@@ -6880,17 +7863,18 @@ var ChannelDriver = class _ChannelDriver {
6880
7863
  return sessionId;
6881
7864
  }
6882
7865
  /**
6883
- * Lazily resolve (and cache) opencode's root directory via `GET /path`.
7866
+ * Lazily resolve (and cache) opencode's root directory via the selected client's
7867
+ * location lookup.
6884
7868
  * Resolved once per driver: `undefined` until first lookup, then the directory
6885
- * string or `null` if unavailable (we don't keep retrying a missing `/path`).
7869
+ * string or `null` if unavailable (we don't keep retrying a failed lookup).
6886
7870
  */
6887
7871
  async resolveOpenCodeDirectory() {
6888
7872
  if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
6889
- this.opencodeDirectory = await getOpenCodeDirectory(this.port);
7873
+ this.opencodeDirectory = await this.getOpenCodeDirectory();
6890
7874
  if (!this.opencodeDirectory) {
6891
7875
  this.log({
6892
7876
  level: "warn",
6893
- message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
7877
+ message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
6894
7878
  });
6895
7879
  }
6896
7880
  return this.opencodeDirectory;
@@ -7324,7 +8308,7 @@ var ChannelDriver = class _ChannelDriver {
7324
8308
  while (!this.stopped && !signal.aborted) {
7325
8309
  const openedAt = this.now();
7326
8310
  try {
7327
- const outcome = await readSessionErrorStream(this.port, {
8311
+ const outcome = await this.readOpenCodeSessionErrorStream({
7328
8312
  signal,
7329
8313
  onSessionError: (event) => this.handleSessionError(event)
7330
8314
  });
@@ -7415,7 +8399,7 @@ var ChannelDriver = class _ChannelDriver {
7415
8399
  }
7416
8400
  async failFromSessionError(watcher, event, inFlight) {
7417
8401
  try {
7418
- const messages = await getSessionMessages(this.port, event.sessionId);
8402
+ const messages = await this.getSessionMessages(event.sessionId);
7419
8403
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7420
8404
  if (state !== "queued") {
7421
8405
  this.log({
@@ -7467,7 +8451,8 @@ var ChannelDriver = class _ChannelDriver {
7467
8451
  * markDone (done) exactly once per transition;
7468
8452
  * 2. applies the idle-path re-dispatch guard (a dispatched message that never
7469
8453
  * APPEARS → re-dispatch — D1 obligation 2);
7470
- * 3. polls `/question` + `/permission` (scoped to the session) and surfaces
8454
+ * 3. polls V1's global `/question` + `/permission`, or V2's
8455
+ * `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
7471
8456
  * NEW ones via `reportInteraction`, carrying the PAUSED message's own
7472
8457
  * `source_message_id`;
7473
8458
  * 4. drops messages that completed or timed out from the in-flight set.
@@ -7493,11 +8478,7 @@ var ChannelDriver = class _ChannelDriver {
7493
8478
  if (watcher.generation !== generation) return;
7494
8479
  let messages = null;
7495
8480
  try {
7496
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
7497
- if (res.ok) {
7498
- const body = await res.json();
7499
- messages = Array.isArray(body) ? body : null;
7500
- }
8481
+ messages = await this.getSessionMessages(sessionId);
7501
8482
  } catch {
7502
8483
  }
7503
8484
  if (messages != null && messages.length > 0) {
@@ -7730,7 +8711,7 @@ var ChannelDriver = class _ChannelDriver {
7730
8711
  inFlight.b2LastDescendantCheckMs = this.now();
7731
8712
  const [descendantOngoing, rootOngoing] = await Promise.all([
7732
8713
  this.isAnyDescendantSessionOngoing(sessionId),
7733
- isSessionOngoing(this.port, sessionId)
8714
+ this.isSessionOngoing(sessionId)
7734
8715
  ]);
7735
8716
  if (isB2AbandonmentConfirmed({
7736
8717
  pinnedForMs,
@@ -7790,7 +8771,7 @@ var ChannelDriver = class _ChannelDriver {
7790
8771
  });
7791
8772
  }
7792
8773
  const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
7793
- const ongoing = await isSessionOngoing(this.port, sessionId);
8774
+ const ongoing = await this.isSessionOngoing(sessionId);
7794
8775
  if (isAmbiguousFinishResolved({
7795
8776
  pinnedForMs,
7796
8777
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
@@ -7973,7 +8954,7 @@ var ChannelDriver = class _ChannelDriver {
7973
8954
  */
7974
8955
  async readoptProcessing() {
7975
8956
  const rows = await this.getProcessingMessages();
7976
- if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
8957
+ if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
7977
8958
  const stillProcessing = new Set(rows.map((r) => r.id));
7978
8959
  for (const id of [
7979
8960
  ...this.dontRedispatch,
@@ -7993,6 +8974,16 @@ var ChannelDriver = class _ChannelDriver {
7993
8974
  }
7994
8975
  }
7995
8976
  }
8977
+ for (const id of [...this.untrackableAck]) {
8978
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
8979
+ this.untrackableAck.delete(id);
8980
+ this.log({
8981
+ level: "debug",
8982
+ message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
8983
+ message_id: id
8984
+ });
8985
+ }
8986
+ }
7996
8987
  }
7997
8988
  if (rows.length === 0) return;
7998
8989
  const bySession = /* @__PURE__ */ new Map();
@@ -8013,23 +9004,32 @@ var ChannelDriver = class _ChannelDriver {
8013
9004
  for (const [sessionId, sessionRows] of bySession) {
8014
9005
  let messages;
8015
9006
  try {
8016
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8017
- if (!res.ok) {
8018
- this.log({
8019
- level: "warn",
8020
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
8021
- });
8022
- continue;
8023
- }
8024
- const body = await res.json();
8025
- if (!Array.isArray(body)) {
8026
- this.log({
8027
- level: "warn",
8028
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
8029
- });
8030
- continue;
9007
+ if (this.isV2) {
9008
+ const snapshot = await this.getSessionMessages(sessionId);
9009
+ if (snapshot === null) {
9010
+ this.log({
9011
+ level: "warn",
9012
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
9013
+ });
9014
+ continue;
9015
+ }
9016
+ messages = snapshot;
9017
+ } else {
9018
+ const polled = await pollSessionMessagesForRedrive(
9019
+ this.port,
9020
+ sessionId,
9021
+ this.openCodeClient
9022
+ );
9023
+ if (!polled.ok) {
9024
+ const normalized = normalizeRedrivePollFailureBody(polled.body);
9025
+ this.log({
9026
+ level: "warn",
9027
+ 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`
9028
+ });
9029
+ continue;
9030
+ }
9031
+ messages = polled.messages;
8031
9032
  }
8032
- messages = body;
8033
9033
  } catch (err) {
8034
9034
  this.log({
8035
9035
  level: "warn",
@@ -8038,7 +9038,7 @@ var ChannelDriver = class _ChannelDriver {
8038
9038
  continue;
8039
9039
  }
8040
9040
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
8041
- const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
9041
+ const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
8042
9042
  for (const row of sessionRows) {
8043
9043
  await this.readoptOne(sessionId, row, messages, sessionOngoing);
8044
9044
  }
@@ -8085,7 +9085,7 @@ var ChannelDriver = class _ChannelDriver {
8085
9085
  if (restartAborted) {
8086
9086
  this.log({
8087
9087
  level: "info",
8088
- 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`,
9088
+ 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`,
8089
9089
  conversation_id: row.conversation_id,
8090
9090
  message_id: row.id
8091
9091
  });
@@ -8140,10 +9140,11 @@ var ChannelDriver = class _ChannelDriver {
8140
9140
  }
8141
9141
  await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
8142
9142
  this.dontRedispatch.delete(row.id);
9143
+ this.untrackableAck.delete(row.id);
8143
9144
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
8144
9145
  return;
8145
9146
  }
8146
- if (this.dontRedispatch.has(row.id)) {
9147
+ if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
8147
9148
  this.log({
8148
9149
  level: "debug",
8149
9150
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
@@ -8163,7 +9164,7 @@ var ChannelDriver = class _ChannelDriver {
8163
9164
  const finish = reply?.info?.finish ?? reply?.finish;
8164
9165
  this.log({
8165
9166
  level: "info",
8166
- 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`,
9167
+ 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`,
8167
9168
  conversation_id: row.conversation_id,
8168
9169
  message_id: row.id
8169
9170
  });
@@ -8172,7 +9173,7 @@ var ChannelDriver = class _ChannelDriver {
8172
9173
  }
8173
9174
  this.log({
8174
9175
  level: "info",
8175
- 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)`,
9176
+ 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)`,
8176
9177
  conversation_id: row.conversation_id,
8177
9178
  message_id: row.id
8178
9179
  });
@@ -8182,7 +9183,7 @@ var ChannelDriver = class _ChannelDriver {
8182
9183
  if (ongoing === true) {
8183
9184
  this.log({
8184
9185
  level: "debug",
8185
- 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)`,
9186
+ 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)`,
8186
9187
  conversation_id: row.conversation_id,
8187
9188
  message_id: row.id
8188
9189
  });
@@ -8190,7 +9191,7 @@ var ChannelDriver = class _ChannelDriver {
8190
9191
  if (shape === "b1") {
8191
9192
  this.log({
8192
9193
  level: "debug",
8193
- 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`,
9194
+ 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`,
8194
9195
  conversation_id: row.conversation_id,
8195
9196
  message_id: row.id
8196
9197
  });
@@ -8202,7 +9203,7 @@ var ChannelDriver = class _ChannelDriver {
8202
9203
  }
8203
9204
  this.log({
8204
9205
  level: "debug",
8205
- 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`,
9206
+ 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`,
8206
9207
  conversation_id: row.conversation_id,
8207
9208
  message_id: row.id
8208
9209
  });
@@ -8252,7 +9253,7 @@ var ChannelDriver = class _ChannelDriver {
8252
9253
  * extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
8253
9254
  * SAME delivery instead of duplicating it.
8254
9255
  *
8255
- * EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops
9256
+ * EVEN IF the row was previously parked by either recovery fence (a give-up stops
8256
9257
  * re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
8257
9258
  * `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
8258
9259
  * leave for cron; transient → log + leave for the next drain (the still-
@@ -8319,6 +9320,7 @@ var ChannelDriver = class _ChannelDriver {
8319
9320
  await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
8320
9321
  }
8321
9322
  this.dontRedispatch.delete(row.id);
9323
+ this.untrackableAck.delete(row.id);
8322
9324
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
8323
9325
  }
8324
9326
  /**
@@ -8382,19 +9384,90 @@ var ChannelDriver = class _ChannelDriver {
8382
9384
  conversation_id: row.conversation_id,
8383
9385
  message_id: row.id
8384
9386
  });
8385
- this.awaitingReadopt.add(row.id);
9387
+ if (!this.isV2) this.awaitingReadopt.add(row.id);
8386
9388
  const readoptConv = this.convForRow(sessionId, row);
8387
9389
  const readoptMessage = this.queuedMessageForRow(row);
8388
- const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
9390
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
9391
+ if (this.isV2 && row.attachments && row.attachments.length > 0) {
9392
+ this.signalAttachmentsSkipped(
9393
+ row.conversation_id,
9394
+ row.id,
9395
+ row.attachments.map((attachment, index) => ({
9396
+ index,
9397
+ mime: attachment.mime,
9398
+ ...attachment.filename ? { filename: attachment.filename } : {},
9399
+ status: "skipped"
9400
+ })),
9401
+ false
9402
+ );
9403
+ }
8389
9404
  let ocId;
8390
9405
  try {
8391
- ocId = await this.dispatchLocked(
9406
+ ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
8392
9407
  sessionId,
8393
- () => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
9408
+ () => sendPromptAsync(
9409
+ this.port,
9410
+ sessionId,
9411
+ row.content,
9412
+ options,
9413
+ sendAttachments,
9414
+ this.openCodeClient
9415
+ )
8394
9416
  );
8395
9417
  } catch (err) {
8396
9418
  this.awaitingReadopt.delete(row.id);
8397
9419
  if (err instanceof ChannelAuthError) throw err;
9420
+ if (this.isV2) {
9421
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
9422
+ const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
9423
+ if (!invalidPromptAck) {
9424
+ const exists = await this.sessionExists(sessionId);
9425
+ if (exists === false) {
9426
+ this.log({
9427
+ level: "warn",
9428
+ 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}`,
9429
+ conversation_id: row.conversation_id,
9430
+ message_id: row.id
9431
+ });
9432
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9433
+ return;
9434
+ }
9435
+ if (exists === null) {
9436
+ this.log({
9437
+ level: "warn",
9438
+ 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}`,
9439
+ conversation_id: row.conversation_id,
9440
+ message_id: row.id
9441
+ });
9442
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9443
+ return;
9444
+ }
9445
+ } else {
9446
+ this.untrackableAck.add(row.id);
9447
+ }
9448
+ this.log({
9449
+ level: "error",
9450
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
9451
+ conversation_id: row.conversation_id,
9452
+ message_id: row.id
9453
+ });
9454
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
9455
+ (markErr) => {
9456
+ this.log({
9457
+ level: "warn",
9458
+ message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9459
+ conversation_id: row.conversation_id,
9460
+ message_id: row.id
9461
+ });
9462
+ if (invalidPromptAck) {
9463
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9464
+ } else {
9465
+ this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
9466
+ }
9467
+ }
9468
+ );
9469
+ return;
9470
+ }
8398
9471
  this.log({
8399
9472
  level: "warn",
8400
9473
  message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
@@ -8526,12 +9599,13 @@ var ChannelDriver = class _ChannelDriver {
8526
9599
  this.dispatched.delete(evidentMessageId);
8527
9600
  }
8528
9601
  /**
8529
- * Poll `/question` + `/permission` (scoped to the session) and surface NEW ones
8530
- * via `reportInteraction` (Task 3.5), carrying the PAUSED message's own
8531
- * `source_message_id` so the server @mentions the correct person under
8532
- * concurrency. Dedups by interaction id across ticks (reused per-session sets).
9602
+ * Poll V1's global `/question` + `/permission`, or V2's watched-session form and
9603
+ * permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
9604
+ * carrying the PAUSED message's own `source_message_id` so the server @mentions
9605
+ * the correct person under concurrency. Dedups by interaction id across ticks
9606
+ * (reused per-session sets).
8533
9607
  *
8534
- * The interaction is attributed to the in-flight message it paused on. opencode
9608
+ * The interaction is attributed to the in-flight message it paused on. OpenCode
8535
9609
  * stamps a `messageID` on a permission (and `tool.messageID` on a question) =
8536
9610
  * the assistant message id, whose `parentID` is the user message id — but the
8537
9611
  * simplest robust attribution here is: the single in-flight message that is
@@ -8554,16 +9628,14 @@ var ChannelDriver = class _ChannelDriver {
8554
9628
  let permissionsPolledOk = true;
8555
9629
  let questions = [];
8556
9630
  try {
8557
- const res = await this.fetchImpl(`${this.opencodeBase}/question`);
8558
- if (res.ok) {
8559
- const body = await res.json();
8560
- if (Array.isArray(body)) {
8561
- questions = body;
8562
- } else {
8563
- questionsPolledOk = false;
8564
- }
9631
+ if (this.isV2) {
9632
+ const forms = await listV2Forms(this.openCodeClient, sessionId);
9633
+ if (forms === null) questionsPolledOk = false;
9634
+ else questions = forms;
8565
9635
  } else {
8566
- questionsPolledOk = false;
9636
+ const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
9637
+ if (listed === null) questionsPolledOk = false;
9638
+ else questions = listed;
8567
9639
  }
8568
9640
  } catch {
8569
9641
  questionsPolledOk = false;
@@ -8583,16 +9655,14 @@ var ChannelDriver = class _ChannelDriver {
8583
9655
  }
8584
9656
  let permissions = [];
8585
9657
  try {
8586
- const res = await this.fetchImpl(`${this.opencodeBase}/permission`);
8587
- if (res.ok) {
8588
- const body = await res.json();
8589
- if (Array.isArray(body)) {
8590
- permissions = body;
8591
- } else {
8592
- permissionsPolledOk = false;
8593
- }
9658
+ if (this.isV2) {
9659
+ const listed = await listV2Permissions(this.openCodeClient, sessionId);
9660
+ if (listed === null) permissionsPolledOk = false;
9661
+ else permissions = listed;
8594
9662
  } else {
8595
- permissionsPolledOk = false;
9663
+ const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
9664
+ if (listed === null) permissionsPolledOk = false;
9665
+ else permissions = listed;
8596
9666
  }
8597
9667
  } catch {
8598
9668
  permissionsPolledOk = false;
@@ -8686,10 +9756,14 @@ var ChannelDriver = class _ChannelDriver {
8686
9756
  if (cached !== void 0) return cached;
8687
9757
  let parent = void 0;
8688
9758
  try {
8689
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8690
- if (res.ok) {
8691
- const body = await res.json();
8692
- parent = body && typeof body.parentID === "string" ? body.parentID : null;
9759
+ if (this.isV2) {
9760
+ const session = await getV2Session(this.openCodeClient, sessionId);
9761
+ parent = null;
9762
+ const candidate = session.parentID;
9763
+ if (typeof candidate === "string") parent = candidate;
9764
+ } else {
9765
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9766
+ parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
8693
9767
  }
8694
9768
  } catch {
8695
9769
  parent = void 0;
@@ -8743,18 +9817,16 @@ var ChannelDriver = class _ChannelDriver {
8743
9817
  if (cached) return cached;
8744
9818
  const pending = (async () => {
8745
9819
  try {
8746
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8747
- if (!res.ok) {
9820
+ const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
9821
+ if (messages2 === null) {
8748
9822
  this.log({
8749
9823
  level: "warn",
8750
- 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`,
9824
+ 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`,
8751
9825
  message_id: messageId
8752
9826
  });
8753
9827
  return null;
8754
9828
  }
8755
- const body = await res.json();
8756
- if (!Array.isArray(body)) throw new Error("response body was not a message array");
8757
- return body;
9829
+ return messages2;
8758
9830
  } catch (err) {
8759
9831
  this.log({
8760
9832
  level: "warn",
@@ -8895,21 +9967,18 @@ var ChannelDriver = class _ChannelDriver {
8895
9967
  const cached = this.sessionTitles.get(sessionId);
8896
9968
  if (cached != null) return cached;
8897
9969
  try {
8898
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8899
- if (res.ok) {
8900
- const body = await res.json();
8901
- const title = body && typeof body.title === "string" ? body.title.trim() : "";
8902
- if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
8903
- this.sessionTitles.set(sessionId, title);
8904
- return title;
8905
- }
8906
- return null;
9970
+ let title = "";
9971
+ if (this.isV2) {
9972
+ title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
9973
+ } else {
9974
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9975
+ title = typeof body?.title === "string" ? body.title.trim() : "";
8907
9976
  }
8908
- this.log({
8909
- level: "debug",
8910
- message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
8911
- conversation_id: conversationId
8912
- });
9977
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
9978
+ this.sessionTitles.set(sessionId, title);
9979
+ return title;
9980
+ }
9981
+ return null;
8913
9982
  } catch (err) {
8914
9983
  this.log({
8915
9984
  level: "debug",
@@ -9007,7 +10076,7 @@ var ChannelDriver = class _ChannelDriver {
9007
10076
  * `SessionStatus` only.
9008
10077
  */
9009
10078
  async isAnyDescendantSessionAlive(rootSessionId) {
9010
- const sessions = await listSessions(this.port);
10079
+ const sessions = await this.listSessions();
9011
10080
  if (!sessions) {
9012
10081
  this.log({
9013
10082
  level: "warn",
@@ -9018,7 +10087,7 @@ var ChannelDriver = class _ChannelDriver {
9018
10087
  for (const candidate of sessions) {
9019
10088
  if (!candidate?.id || candidate.id === rootSessionId) continue;
9020
10089
  if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
9021
- const childMsgs = await getSessionMessages(this.port, candidate.id);
10090
+ const childMsgs = await this.getSubagentSessionMessages(candidate.id);
9022
10091
  if (isSessionActivelyGenerating(childMsgs)) {
9023
10092
  return true;
9024
10093
  }
@@ -9080,7 +10149,7 @@ var ChannelDriver = class _ChannelDriver {
9080
10149
  * `isB2AbandonmentConfirmed`.
9081
10150
  */
9082
10151
  async isAnyDescendantSessionOngoing(rootSessionId) {
9083
- const sessions = await listSessions(this.port);
10152
+ const sessions = await this.listSessions();
9084
10153
  if (!sessions) {
9085
10154
  this.log({
9086
10155
  level: "warn",
@@ -9097,7 +10166,7 @@ var ChannelDriver = class _ChannelDriver {
9097
10166
  continue;
9098
10167
  }
9099
10168
  if (membership === false) continue;
9100
- const ongoing = await isSessionOngoing(this.port, candidate.id);
10169
+ const ongoing = await this.isSessionOngoing(candidate.id);
9101
10170
  if (ongoing === true) return true;
9102
10171
  if (ongoing === null) indeterminate = true;
9103
10172
  }
@@ -9437,7 +10506,7 @@ var ChannelDriver = class _ChannelDriver {
9437
10506
  const classified = messageFailure(messages, userMessageId);
9438
10507
  if (classified != null) return classified;
9439
10508
  const reply = findLastAssistantReplyFor(messages, userMessageId);
9440
- const hasProvider = await hasAnyConfiguredProvider(this.port);
10509
+ const hasProvider = await this.hasAnyConfiguredProvider();
9441
10510
  return applyZeroProviderFallback(
9442
10511
  classified,
9443
10512
  hasProvider,
@@ -9510,7 +10579,7 @@ var ChannelDriver = class _ChannelDriver {
9510
10579
  const succeededProviders = /* @__PURE__ */ new Set();
9511
10580
  for (const ref of refs) {
9512
10581
  try {
9513
- const childMessages = await getSessionMessages(this.port, ref.sessionId);
10582
+ const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
9514
10583
  if (childMessages === null) {
9515
10584
  this.log({
9516
10585
  level: "debug",
@@ -9841,6 +10910,7 @@ Port ${port} is already in use.`));
9841
10910
 
9842
10911
  // src/commands/ensure-opencode-v2.ts
9843
10912
  import chalk6 from "chalk";
10913
+ import ora3 from "ora";
9844
10914
  import { select as select3 } from "@inquirer/prompts";
9845
10915
  async function probeOpenCode2WithoutPassword(port) {
9846
10916
  try {
@@ -9866,11 +10936,7 @@ function unknownPasswordError(port) {
9866
10936
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
9867
10937
  );
9868
10938
  }
9869
- function v2SessionSupportIncompleteError() {
9870
- return new Error(
9871
- "OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
9872
- );
9873
- }
10939
+ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
9874
10940
  async function ensureOpenCode2Running(ctx) {
9875
10941
  const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
9876
10942
  if (initialHealth.authFailed) {
@@ -9913,13 +10979,37 @@ Port ${port} is already in use.`));
9913
10979
  }
9914
10980
  }
9915
10981
  if (!ctx.interactive) {
9916
- throw v2SessionSupportIncompleteError();
10982
+ ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
10983
+ const { child: proc, password } = await startOpenCode2(port, {
10984
+ inheritStdio: ctx.inheritStdio
10985
+ });
10986
+ const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
10987
+ if (!health.healthy) {
10988
+ return {
10989
+ port,
10990
+ process: proc,
10991
+ version: null,
10992
+ notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
10993
+ password
10994
+ };
10995
+ }
10996
+ ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
10997
+ return {
10998
+ port,
10999
+ process: proc,
11000
+ version: health.version ?? null,
11001
+ notReadyReason: null,
11002
+ password
11003
+ };
9917
11004
  }
9918
- console.log(chalk6.yellow(`
9919
- ${v2SessionSupportIncompleteError().message}`));
9920
11005
  const action = await select3({
9921
11006
  message: "OpenCode V2 is not running. What would you like to do?",
9922
11007
  choices: [
11008
+ {
11009
+ name: "Start OpenCode V2 for me",
11010
+ value: "start",
11011
+ description: `Run 'opencode2 serve --port ${port}'`
11012
+ },
9923
11013
  {
9924
11014
  name: "Show me the command",
9925
11015
  value: "manual",
@@ -9940,6 +11030,25 @@ ${v2SessionSupportIncompleteError().message}`));
9940
11030
  blank();
9941
11031
  throw new Error("Please start OpenCode V2 manually");
9942
11032
  }
11033
+ if (action === "start") {
11034
+ const spinner = ora3("Starting OpenCode V2...").start();
11035
+ const { child: proc, password } = await startOpenCode2(port, {
11036
+ inheritStdio: ctx.inheritStdio
11037
+ });
11038
+ const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
11039
+ if (!health.healthy) {
11040
+ spinner.fail("Failed to start OpenCode V2");
11041
+ throw new Error("OpenCode V2 failed to start");
11042
+ }
11043
+ spinner.stop();
11044
+ return {
11045
+ port,
11046
+ process: proc,
11047
+ version: health.version ?? null,
11048
+ notReadyReason: null,
11049
+ password
11050
+ };
11051
+ }
9943
11052
  return {
9944
11053
  port,
9945
11054
  process: null,
@@ -10833,7 +11942,7 @@ async function driveChannels(state, driver) {
10833
11942
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
10834
11943
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
10835
11944
  if (claudeCredentialApplied || opencodeAuthApplied) {
10836
- void reloadProviderCache(state.port).catch(
11945
+ void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
10837
11946
  (error2) => logActivity(state, {
10838
11947
  type: "error",
10839
11948
  error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
@@ -10958,7 +12067,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
10958
12067
  async function runSweep(state, driver, config) {
10959
12068
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
10960
12069
  try {
10961
- const sessions = await listSessions(state.port);
12070
+ const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
10962
12071
  if (sessions === null) {
10963
12072
  logActivity(state, {
10964
12073
  type: "info",
@@ -10988,7 +12097,7 @@ async function runSweep(state, driver, config) {
10988
12097
  });
10989
12098
  continue;
10990
12099
  }
10991
- if (await deleteSession(state.port, id)) deleted++;
12100
+ if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
10992
12101
  else failed++;
10993
12102
  }
10994
12103
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
@@ -11496,6 +12605,8 @@ async function run(options) {
11496
12605
  opencodeConnected: false,
11497
12606
  opencodeVersion: null,
11498
12607
  opencodeApiVersion: "v1",
12608
+ opencodePassword: null,
12609
+ opencodeClient: null,
11499
12610
  sessionDbProvenanceAnomaly: false,
11500
12611
  opencodeProcess: null,
11501
12612
  stopOpenCodeLogTail: null,
@@ -11650,7 +12761,7 @@ async function run(options) {
11650
12761
  console.log(chalk7.bold("Evident Run"));
11651
12762
  console.log(chalk7.dim("-".repeat(40)));
11652
12763
  }
11653
- const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
12764
+ const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
11654
12765
  let validation = await getAgentInfo(state.agentId, state.authHeader);
11655
12766
  if (!validation.valid && validation.authFailed && interactive) {
11656
12767
  spinner?.fail("Authentication failed");
@@ -11771,7 +12882,7 @@ async function run(options) {
11771
12882
  logActivity(state, { type: "info", level: "warn", message: warning2 });
11772
12883
  }
11773
12884
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
11774
- const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
12885
+ const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
11775
12886
  try {
11776
12887
  const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
11777
12888
  port: state.port,
@@ -11792,6 +12903,18 @@ async function run(options) {
11792
12903
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
11793
12904
  state.opencodeVersion = oc.version;
11794
12905
  state.opencodeApiVersion = opencodeVersion;
12906
+ let opencodePassword = null;
12907
+ if (opencodeVersion === "v2" && "password" in oc) {
12908
+ const value = oc.password;
12909
+ if (typeof value === "string" || value === null) opencodePassword = value;
12910
+ }
12911
+ state.opencodePassword = opencodePassword;
12912
+ const openCodeClient = createOpenCodeClient({
12913
+ port: state.port,
12914
+ version: state.opencodeApiVersion,
12915
+ password: state.opencodePassword
12916
+ });
12917
+ state.opencodeClient = openCodeClient;
11795
12918
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
11796
12919
  try {
11797
12920
  writeFileSync6(options.opencodePidFile, `${oc.process.pid}
@@ -11838,9 +12961,9 @@ async function run(options) {
11838
12961
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
11839
12962
  }
11840
12963
  }
11841
- await reloadProviderCache(state.port);
12964
+ await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
11842
12965
  const noProviderWarning = buildNoProviderWarning(
11843
- await hasAnyConfiguredProvider(state.port)
12966
+ await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
11844
12967
  );
11845
12968
  if (noProviderWarning) {
11846
12969
  log2(state, noProviderWarning, "warn");
@@ -11963,11 +13086,12 @@ async function run(options) {
11963
13086
  });
11964
13087
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
11965
13088
  }
11966
- const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
13089
+ const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
11967
13090
  const channelDriver = new ChannelDriver({
11968
13091
  agentId: state.agentId,
11969
13092
  port: state.port,
11970
13093
  apiUrl: getApiUrlConfig(),
13094
+ openCodeClient: state.opencodeClient ?? void 0,
11971
13095
  getAuthHeader: () => state.authHeader,
11972
13096
  conversationFilter: state.conversationFilter,
11973
13097
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
@@ -11993,6 +13117,7 @@ async function run(options) {
11993
13117
  agentId: state.agentId,
11994
13118
  getAuthHeader: () => state.authHeader,
11995
13119
  port: state.port,
13120
+ openCodePassword: state.opencodePassword,
11996
13121
  isRunning: () => state.running,
11997
13122
  events: {
11998
13123
  onConnected: (agentId, isReconnect) => {
@@ -12137,7 +13262,7 @@ async function run(options) {
12137
13262
  state.openaiUsageTimer = timer;
12138
13263
  },
12139
13264
  fetchUsage: async () => {
12140
- const usage = await getOpenAiUsage(state.port);
13265
+ const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
12141
13266
  if (usage.subscription === null) {
12142
13267
  logActivity(state, {
12143
13268
  type: "info",