@evident-ai/cli 3.4.1-dev.69d24ff → 3.4.1-dev.6e623a8

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
@@ -1178,11 +1178,11 @@ function stripQuery(url) {
1178
1178
  }
1179
1179
 
1180
1180
  // src/commands/run.ts
1181
- import ora3 from "ora";
1181
+ import ora4 from "ora";
1182
1182
  import { select as select4 } from "@inquirer/prompts";
1183
1183
 
1184
1184
  // src/lib/telemetry.ts
1185
- var CLI_VERSION = (true ? "3.4.1-dev.69d24ff" : void 0) ?? process.env.npm_package_version ?? "unknown";
1185
+ var CLI_VERSION = (true ? "3.4.1-dev.6e623a8" : void 0) ?? process.env.npm_package_version ?? "unknown";
1186
1186
  function getCliVersion() {
1187
1187
  return CLI_VERSION;
1188
1188
  }
@@ -1656,6 +1656,11 @@ function isSessionDbRecoveryRecord(value) {
1656
1656
  );
1657
1657
  }
1658
1658
 
1659
+ // src/lib/opencode/auth.ts
1660
+ function buildOpenCodeBasicAuthHeader(password) {
1661
+ return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
1662
+ }
1663
+
1659
1664
  // src/lib/opencode/health.ts
1660
1665
  async function checkOpenCodeHealth(port) {
1661
1666
  try {
@@ -1673,6 +1678,27 @@ async function checkOpenCodeHealth(port) {
1673
1678
  return { healthy: false, error: message };
1674
1679
  }
1675
1680
  }
1681
+ async function checkOpenCode2Health(port, password) {
1682
+ try {
1683
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
1684
+ headers: {
1685
+ Authorization: buildOpenCodeBasicAuthHeader(password)
1686
+ },
1687
+ signal: AbortSignal.timeout(2e3)
1688
+ });
1689
+ if (response.status === 401) {
1690
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
1691
+ }
1692
+ if (!response.ok) {
1693
+ return { healthy: false, error: `HTTP ${response.status}` };
1694
+ }
1695
+ const data = await response.json().catch(() => ({}));
1696
+ return { healthy: true, version: data.version };
1697
+ } catch (error2) {
1698
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1699
+ return { healthy: false, error: message };
1700
+ }
1701
+ }
1676
1702
  async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1677
1703
  const startTime = Date.now();
1678
1704
  while (Date.now() - startTime < timeoutMs) {
@@ -1684,6 +1710,61 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1684
1710
  }
1685
1711
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1686
1712
  }
1713
+ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1714
+ const startTime = Date.now();
1715
+ while (Date.now() - startTime < timeoutMs) {
1716
+ const health = await checkOpenCode2Health(port, password);
1717
+ if (health.healthy || health.authFailed) {
1718
+ return health;
1719
+ }
1720
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
1721
+ }
1722
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1723
+ }
1724
+
1725
+ // src/lib/http-timeout.ts
1726
+ var REQUEST_TIMEOUT_MS = 6e4;
1727
+ function withRequestTimeout(fetchImpl, timeoutMs) {
1728
+ return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
1729
+ }
1730
+
1731
+ // src/lib/opencode/client.ts
1732
+ function redactPassword(message, password) {
1733
+ return message.replaceAll(password, "[redacted]");
1734
+ }
1735
+ function createOpenCodeClient(options) {
1736
+ const password = options.password ?? null;
1737
+ const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
1738
+ const baseUrl = `http://127.0.0.1:${options.port}`;
1739
+ return {
1740
+ port: options.port,
1741
+ version: options.version,
1742
+ password,
1743
+ async request(path, init, requestOptions) {
1744
+ const requestInit = options.version === "v2" && password !== null ? (() => {
1745
+ const headers = new Headers(init?.headers);
1746
+ headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
1747
+ return { ...init, headers };
1748
+ })() : init;
1749
+ try {
1750
+ const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
1751
+ if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
1752
+ const body = await response.text();
1753
+ throw new Error(
1754
+ `OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
1755
+ );
1756
+ }
1757
+ return response;
1758
+ } catch (error2) {
1759
+ if (options.version === "v2" && password !== null) {
1760
+ const message = error2 instanceof Error ? error2.message : String(error2);
1761
+ throw new Error(redactPassword(message, password));
1762
+ }
1763
+ throw error2;
1764
+ }
1765
+ }
1766
+ };
1767
+ }
1687
1768
 
1688
1769
  // src/lib/opencode/session-db-boot.ts
1689
1770
  import { spawn as spawn2 } from "node:child_process";
@@ -2363,6 +2444,7 @@ function reportedOpenCodeVersion(input) {
2363
2444
 
2364
2445
  // src/lib/opencode/process.ts
2365
2446
  import { execSync, spawn as spawn3 } from "child_process";
2447
+ import { randomBytes } from "node:crypto";
2366
2448
 
2367
2449
  // src/lib/process-stop.ts
2368
2450
  async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
@@ -2421,6 +2503,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2421
2503
  // src/lib/opencode/process.ts
2422
2504
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2423
2505
  var VALID_OPENCODE_LOG_LEVELS = /* @__PURE__ */ new Set(["DEBUG", "INFO", "WARN", "ERROR"]);
2506
+ var VALID_OPENCODE2_LOG_LEVELS = /* @__PURE__ */ new Set([
2507
+ "all",
2508
+ "trace",
2509
+ "debug",
2510
+ "info",
2511
+ "warn",
2512
+ "warning",
2513
+ "error",
2514
+ "fatal",
2515
+ "none"
2516
+ ]);
2424
2517
  function resolveOpenCodeLogLevel(env) {
2425
2518
  const raw = env.OPENCODE_LOG_LEVEL;
2426
2519
  if (!raw) return "INFO";
@@ -2431,6 +2524,16 @@ function resolveOpenCodeLogLevel(env) {
2431
2524
  );
2432
2525
  return "INFO";
2433
2526
  }
2527
+ function resolveOpenCode2LogLevel(env) {
2528
+ const raw = env.OPENCODE_LOG_LEVEL;
2529
+ if (!raw) return "info";
2530
+ const lower = raw.toLowerCase();
2531
+ if (VALID_OPENCODE2_LOG_LEVELS.has(lower)) return lower;
2532
+ console.warn(
2533
+ `startOpenCode2: ignoring invalid OPENCODE_LOG_LEVEL "${raw}" (expected all|trace|debug|info|warn|warning|error|fatal|none) \u2014 using info`
2534
+ );
2535
+ return "info";
2536
+ }
2434
2537
  function getProcessCwd(pid) {
2435
2538
  const platform = process.platform;
2436
2539
  try {
@@ -2613,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
2613
2716
  });
2614
2717
  return child;
2615
2718
  }
2719
+ async function startOpenCode2(port, options = {}) {
2720
+ const password = randomBytes(24).toString("hex");
2721
+ let command = "opencode2";
2722
+ const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
2723
+ let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
2724
+ try {
2725
+ execSync("which opencode2", { stdio: "ignore" });
2726
+ } catch {
2727
+ command = "npx";
2728
+ args = [
2729
+ "-y",
2730
+ "-p",
2731
+ "@opencode-ai/cli@beta",
2732
+ "--",
2733
+ "opencode2",
2734
+ "serve",
2735
+ "--port",
2736
+ port.toString(),
2737
+ "--hostname",
2738
+ "127.0.0.1",
2739
+ ...logLevel
2740
+ ];
2741
+ }
2742
+ const child = spawn3(command, args, {
2743
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
2744
+ detached: true,
2745
+ stdio: options.inheritStdio ? "inherit" : "ignore",
2746
+ cwd: process.cwd()
2747
+ });
2748
+ return { child, password };
2749
+ }
2616
2750
  function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
2617
2751
  const sendSignal = (signal) => {
2618
2752
  if (process.platform === "win32") {
@@ -2760,27 +2894,500 @@ function buildNoProviderWarning(hasProvider) {
2760
2894
  return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
2761
2895
  }
2762
2896
 
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) }));
2897
+ // src/lib/opencode/session-v2.ts
2898
+ function isRecord(value) {
2899
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2900
+ }
2901
+ function finiteNumber(value) {
2902
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2903
+ }
2904
+ function adaptTime(value) {
2905
+ if (!isRecord(value)) return void 0;
2906
+ const created = finiteNumber(value.created);
2907
+ const completed = finiteNumber(value.completed);
2908
+ if (created === void 0 && completed === void 0) return void 0;
2909
+ return {
2910
+ ...created !== void 0 ? { created } : {},
2911
+ ...completed !== void 0 ? { completed } : {}
2912
+ };
2913
+ }
2914
+ function adaptTokens(value) {
2915
+ if (!isRecord(value)) return void 0;
2916
+ const input = finiteNumber(value.input);
2917
+ const output = finiteNumber(value.output);
2918
+ const reasoning = finiteNumber(value.reasoning);
2919
+ const cache = isRecord(value.cache) ? {
2920
+ ...finiteNumber(value.cache.read) !== void 0 ? { read: finiteNumber(value.cache.read) } : {},
2921
+ ...finiteNumber(value.cache.write) !== void 0 ? { write: finiteNumber(value.cache.write) } : {}
2922
+ } : void 0;
2923
+ if (input === void 0 && output === void 0 && reasoning === void 0 && !cache) {
2924
+ return void 0;
2925
+ }
2926
+ return {
2927
+ ...input !== void 0 ? { input } : {},
2928
+ ...output !== void 0 ? { output } : {},
2929
+ ...reasoning !== void 0 ? { reasoning } : {},
2930
+ ...cache ? { cache } : {}
2931
+ };
2932
+ }
2933
+ function adaptMessageInfo(value, role) {
2934
+ const info = {
2935
+ id: value.id,
2936
+ role
2937
+ };
2938
+ const time = adaptTime(value.time);
2939
+ if (time) info.time = time;
2940
+ if (typeof value.finish === "string") info.finish = value.finish;
2941
+ if ("error" in value) info.error = value.error;
2942
+ if (typeof value.agent === "string") info.agent = value.agent;
2943
+ if (isRecord(value.model)) {
2944
+ if (typeof value.model.id === "string") info.modelID = value.model.id;
2945
+ if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
2946
+ }
2947
+ if (typeof value.cost === "number" && Number.isFinite(value.cost)) info.cost = value.cost;
2948
+ const tokens = adaptTokens(value.tokens);
2949
+ if (tokens) info.tokens = tokens;
2950
+ return info;
2951
+ }
2952
+ function adaptV2Message(value) {
2953
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
2954
+ return null;
2955
+ }
2956
+ if (value.type === "user") {
2957
+ if (typeof value.text !== "string") return null;
2958
+ return {
2959
+ info: adaptMessageInfo(value, "user"),
2960
+ parts: [{ type: "text", text: value.text }]
2961
+ };
2962
+ }
2963
+ if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
2964
+ const parts = [];
2965
+ for (const content of value.content) {
2966
+ if (!isRecord(content) || typeof content.type !== "string") return null;
2967
+ if (content.type === "text") {
2968
+ if (typeof content.text !== "string") return null;
2969
+ parts.push({ type: "text", text: content.text });
2970
+ } else {
2971
+ parts.push({ type: content.type });
2972
+ }
2973
+ }
2974
+ return {
2975
+ info: adaptMessageInfo(value, "assistant"),
2976
+ parts
2977
+ };
2978
+ }
2979
+ function adaptFormTool(value) {
2980
+ if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
2981
+ return void 0;
2982
+ }
2983
+ return { messageID: value.messageID, callID: value.id };
2984
+ }
2985
+ function adaptV2FormWire(value) {
2986
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
2987
+ return null;
2988
+ }
2989
+ return value;
2990
+ }
2991
+ function adaptV2FormField(value, header) {
2992
+ if (!isRecord(value)) return null;
2993
+ const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
2994
+ if (!question) return null;
2995
+ const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
2996
+ if (!isRecord(option)) return [];
2997
+ const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
2998
+ if (!label) return [];
2999
+ return [
3000
+ {
3001
+ label,
3002
+ description: typeof option.description === "string" ? option.description : ""
3003
+ }
3004
+ ];
3005
+ }) : [];
3006
+ return { question, header, options };
3007
+ }
3008
+ function adaptV2Form(value) {
3009
+ const form = adaptV2FormWire(value);
3010
+ if (!form || !Array.isArray(form.fields)) return null;
3011
+ const header = typeof form.title === "string" ? form.title : "";
3012
+ const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
3013
+ if (questions.length === 0) return null;
3014
+ const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
3015
+ return {
3016
+ id: form.id,
3017
+ sessionID: form.sessionID,
3018
+ questions,
3019
+ ...tool ? { tool } : {},
3020
+ raw: form
3021
+ };
3022
+ }
3023
+ function adaptV2FormList(value) {
3024
+ if (!isRecord(value) || !Array.isArray(value.data)) return null;
3025
+ return value.data.map(adaptV2Form).filter((form) => form !== null);
3026
+ }
3027
+ function adaptPattern(value) {
3028
+ if (typeof value === "string" && value.length > 0) return value;
3029
+ if (Array.isArray(value) && value.every((pattern) => typeof pattern === "string")) {
3030
+ return value;
3031
+ }
3032
+ return void 0;
3033
+ }
3034
+ function adaptV2PermissionWire(value) {
3035
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
3036
+ return null;
3037
+ }
3038
+ if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
3039
+ return value;
3040
+ }
3041
+ function adaptV2Permission(value) {
3042
+ const permission = adaptV2PermissionWire(value);
3043
+ if (!permission) return null;
3044
+ const type = permission.permission ?? permission.action;
3045
+ if (!type) return null;
3046
+ const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
3047
+ const time = isRecord(permission.time) ? finiteNumber(permission.time.created) !== void 0 ? { created: finiteNumber(permission.time.created) } : void 0 : void 0;
3048
+ return {
3049
+ id: permission.id,
3050
+ type,
3051
+ sessionID: permission.sessionID,
3052
+ metadata: isRecord(permission.metadata) ? permission.metadata : {},
3053
+ raw: permission,
3054
+ ...pattern !== void 0 ? { pattern } : {},
3055
+ ...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
3056
+ ...typeof permission.callID === "string" ? { callID: permission.callID } : {},
3057
+ ...typeof permission.title === "string" ? { title: permission.title } : {},
3058
+ ...time ? { time } : {}
3059
+ };
3060
+ }
3061
+ function adaptV2PermissionList(value) {
3062
+ if (!isRecord(value) || !Array.isArray(value.data)) return null;
3063
+ return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
3064
+ }
3065
+ function adaptV2Session(value) {
3066
+ if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
3067
+ const time = isRecord(value.time) ? {
3068
+ ...finiteNumber(value.time.created) !== void 0 ? { created: finiteNumber(value.time.created) } : {},
3069
+ ...finiteNumber(value.time.updated) !== void 0 ? { updated: finiteNumber(value.time.updated) } : {}
3070
+ } : void 0;
3071
+ return {
3072
+ id: value.id,
3073
+ ...typeof value.title === "string" ? { title: value.title } : {},
3074
+ ...typeof value.parentID === "string" ? { parentID: value.parentID } : {},
3075
+ ...time && Object.keys(time).length > 0 ? { time } : {}
3076
+ };
3077
+ }
3078
+ function adaptV2SessionList(value) {
3079
+ if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
3080
+ return {
3081
+ data: value.data.map(adaptV2Session).filter((session) => session !== null),
3082
+ cursor: value.cursor
3083
+ };
3084
+ }
3085
+ function adaptV2Location(value) {
3086
+ const candidates = [
3087
+ value,
3088
+ isRecord(value) ? value.data : void 0,
3089
+ isRecord(value) ? value.location : void 0
3090
+ ];
3091
+ for (const candidate of candidates) {
3092
+ if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
3093
+ const directory = candidate.directory.trim();
3094
+ if (directory) return directory;
3095
+ }
3096
+ return null;
3097
+ }
3098
+ function adaptV2Messages(value) {
3099
+ if (!isRecord(value) || !Array.isArray(value.data)) return [];
3100
+ return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
3101
+ }
3102
+ async function readJson(response) {
3103
+ try {
3104
+ return await response.json();
3105
+ } catch (error2) {
3106
+ throw new Error(
3107
+ `OpenCode V2 response was not valid JSON: ${error2 instanceof Error ? error2.message : String(error2)}`
3108
+ );
3109
+ }
3110
+ }
3111
+ async function readData(client, path, init) {
3112
+ const response = await client.request(path, init);
3113
+ const body = await readJson(response);
3114
+ if (!isRecord(body) || !("data" in body)) {
3115
+ throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
3116
+ }
3117
+ return body.data;
3118
+ }
3119
+ var OpenCodeV2PromptAckError = class extends Error {
3120
+ constructor(message) {
3121
+ super(message);
3122
+ this.name = "OpenCodeV2PromptAckError";
3123
+ }
3124
+ };
3125
+ async function getOpenCodeDirectoryV2(client) {
3126
+ try {
3127
+ return adaptV2Location(await readJson(await client.request("/api/location")));
3128
+ } catch (error2) {
3129
+ console.error(
3130
+ `[getOpenCodeDirectoryV2] GET /api/location failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3131
+ );
3132
+ return null;
3133
+ }
3134
+ }
3135
+ async function createV2Session(client, directory) {
3136
+ const data = await readData(client, "/api/session", {
3137
+ method: "POST",
3138
+ headers: { "Content-Type": "application/json" },
3139
+ body: JSON.stringify({ location: { directory } })
3140
+ });
3141
+ if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
3142
+ throw new Error("OpenCode V2 create session response was missing data.id");
3143
+ }
3144
+ return data.id;
3145
+ }
3146
+ async function getV2Session(client, sessionId) {
3147
+ const data = await readData(client, `/api/session/${encodeURIComponent(sessionId)}`);
3148
+ const session = adaptV2Session(data);
3149
+ if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
3150
+ return session;
3151
+ }
3152
+ async function listV2SessionPage(client, cursor) {
3153
+ const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
3154
+ try {
3155
+ return adaptV2SessionList(await readJson(await client.request(path)));
3156
+ } catch (error2) {
3157
+ console.error(
3158
+ `[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3159
+ );
3160
+ return null;
3161
+ }
3162
+ }
3163
+ async function listV2Sessions(client) {
3164
+ const sessions = [];
3165
+ const seenCursors = /* @__PURE__ */ new Set();
3166
+ let cursor;
3167
+ let hasNextPage = true;
3168
+ try {
3169
+ while (hasNextPage) {
3170
+ const page = await listV2SessionPage(client, cursor);
3171
+ if (!page) return null;
3172
+ sessions.push(...page.data);
3173
+ const next = page.cursor.next;
3174
+ if (next === void 0 || next === null) {
3175
+ hasNextPage = false;
3176
+ continue;
3177
+ }
3178
+ if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
3179
+ throw new Error("OpenCode V2 session list contained an invalid next cursor");
3180
+ }
3181
+ seenCursors.add(next);
3182
+ cursor = next;
3183
+ }
3184
+ return sessions;
3185
+ } catch (error2) {
3186
+ console.error(
3187
+ `[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3188
+ );
3189
+ return null;
3190
+ }
3191
+ }
3192
+ async function deleteV2Session(client, sessionId) {
3193
+ try {
3194
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3195
+ return true;
3196
+ } catch (error2) {
3197
+ console.error(
3198
+ `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3199
+ );
3200
+ return false;
3201
+ }
3202
+ }
3203
+ async function v2SessionExists(client, sessionId) {
3204
+ try {
3205
+ const response = await client.request(
3206
+ `/api/session/${encodeURIComponent(sessionId)}`,
3207
+ void 0,
3208
+ { allowStatuses: [404] }
3209
+ );
3210
+ return response.status === 404 ? false : true;
3211
+ } catch (error2) {
3212
+ console.error(
3213
+ `[v2SessionExists] GET /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3214
+ );
3215
+ return null;
3216
+ }
3217
+ }
3218
+ async function sendV2Prompt(client, sessionId, text) {
3219
+ const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
3220
+ const response = await client.request(path, {
3221
+ method: "POST",
3222
+ headers: { "Content-Type": "application/json" },
3223
+ body: JSON.stringify({ text, delivery: "queue" })
3224
+ });
3225
+ let body;
3226
+ try {
3227
+ body = await readJson(response);
3228
+ } catch (error2) {
3229
+ throw new OpenCodeV2PromptAckError(
3230
+ `OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
3231
+ );
3232
+ }
3233
+ const data = isRecord(body) && "data" in body ? body.data : void 0;
3234
+ if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
3235
+ throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
3236
+ }
3237
+ return data.id;
3238
+ }
3239
+ async function getV2SessionMessages(client, sessionId) {
3240
+ const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
3241
+ try {
3242
+ const body = await readJson(await client.request(path));
3243
+ if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
3244
+ return adaptV2Messages(body);
3245
+ } catch (error2) {
3246
+ console.error(
3247
+ `[getV2SessionMessages] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3248
+ );
3249
+ return null;
3250
+ }
3251
+ }
3252
+ async function listV2Forms(client, sessionId) {
3253
+ const path = `/api/session/${encodeURIComponent(sessionId)}/form`;
3254
+ try {
3255
+ return adaptV2FormList(await readJson(await client.request(path)));
3256
+ } catch (error2) {
3257
+ console.error(
3258
+ `[listV2Forms] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3259
+ );
3260
+ return null;
3261
+ }
3262
+ }
3263
+ async function listV2Permissions(client, sessionId) {
3264
+ const path = `/api/session/${encodeURIComponent(sessionId)}/permission`;
3265
+ try {
3266
+ return adaptV2PermissionList(await readJson(await client.request(path)));
3267
+ } catch (error2) {
3268
+ console.error(
3269
+ `[listV2Permissions] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3270
+ );
3271
+ return null;
3272
+ }
3273
+ }
3274
+ async function getV2ActiveSessions(client) {
3275
+ try {
3276
+ const body = await readJson(await client.request("/api/session/active"));
3277
+ if (!isRecord(body) || !isRecord(body.data)) return null;
3278
+ return body.data;
3279
+ } catch (error2) {
3280
+ console.error(
3281
+ `[getV2ActiveSessions] GET /api/session/active failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3282
+ );
3283
+ return null;
3284
+ }
3285
+ }
3286
+ async function isV2SessionOngoing(client, sessionId) {
3287
+ const activeSessions = await getV2ActiveSessions(client);
3288
+ if (activeSessions === null) return null;
3289
+ return Object.prototype.hasOwnProperty.call(activeSessions, sessionId);
3290
+ }
3291
+ function sessionErrorReason(value) {
3292
+ if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
3293
+ if (isRecord(value)) {
3294
+ const data = isRecord(value.data) ? value.data : void 0;
3295
+ const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
3296
+ if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
3297
+ }
3298
+ return "OpenCode reported a session error with no details";
3299
+ }
3300
+ function adaptV2SessionErrorEvent(value) {
3301
+ let parsed = value;
3302
+ if (typeof value === "string") {
3303
+ try {
3304
+ parsed = JSON.parse(value);
3305
+ } catch (error2) {
3306
+ void error2;
3307
+ return null;
3308
+ }
3309
+ }
3310
+ if (!isRecord(parsed)) return null;
3311
+ try {
3312
+ const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
3313
+ if (establishedShape) return establishedShape;
3314
+ } catch (error2) {
3315
+ void error2;
3316
+ }
3317
+ const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
3318
+ for (const event of candidates) {
3319
+ if (event.type !== "session.error") continue;
3320
+ const properties = [event.properties, event.data, event].find(isRecord);
3321
+ if (!properties) continue;
3322
+ const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
3323
+ if (!sessionId) continue;
3324
+ return {
3325
+ sessionId,
3326
+ reason: sessionErrorReason(properties.error ?? properties)
3327
+ };
3328
+ }
3329
+ return null;
3330
+ }
3331
+ async function readV2SessionErrorStream(client, options) {
3332
+ let reader = null;
3333
+ try {
3334
+ const response = await client.request("/api/event", {
3335
+ headers: { accept: "text/event-stream" },
3336
+ signal: options.signal
3337
+ });
3338
+ if (!response.ok || !response.body) {
3339
+ return { reason: "unavailable", detail: `HTTP ${response.status}` };
3340
+ }
3341
+ reader = response.body.getReader();
3342
+ const decoder = new TextDecoder();
3343
+ let buffer = "";
3344
+ const processLine = (line) => {
3345
+ const trimmed = line.trimEnd();
3346
+ if (!trimmed.startsWith("data:")) return;
3347
+ const event = adaptV2SessionErrorEvent(trimmed.slice("data:".length).replace(/^ /, ""));
3348
+ if (event) options.onSessionError(event);
3349
+ };
3350
+ while (true) {
3351
+ const { done, value } = await reader.read();
3352
+ if (done) return { reason: "ended" };
3353
+ buffer += decoder.decode(value, { stream: true });
3354
+ const lines = buffer.split("\n");
3355
+ buffer = lines.pop() ?? "";
3356
+ for (const line of lines) processLine(line);
3357
+ }
3358
+ } catch (error2) {
3359
+ if (options.signal.aborted) return { reason: "aborted" };
3360
+ return {
3361
+ reason: "unavailable",
3362
+ detail: error2 instanceof Error ? error2.message : String(error2)
3363
+ };
3364
+ } finally {
3365
+ if (reader) void reader.cancel().catch(() => void 0);
3366
+ }
2767
3367
  }
2768
3368
 
2769
3369
  // src/lib/opencode/session.ts
3370
+ var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
2770
3371
  function timedFetch(input, init) {
2771
3372
  return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
2772
3373
  }
3374
+ function requestWithClient(port, client, path, init, options) {
3375
+ return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
3376
+ }
2773
3377
  function opencodeBase(port) {
2774
3378
  return `http://127.0.0.1:${port}`;
2775
3379
  }
2776
- async function getOpenCodeDirectory(port) {
3380
+ async function getOpenCodeDirectory(port, client) {
2777
3381
  try {
2778
- const res = await timedFetch(`${opencodeBase(port)}/path`);
3382
+ const res = await requestWithClient(port, client, "/path");
2779
3383
  if (!res.ok) return null;
2780
3384
  const body = await res.json();
2781
3385
  const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
2782
3386
  return dir && dir.trim() ? dir.trim() : null;
2783
- } catch {
3387
+ } catch (error2) {
3388
+ console.error(
3389
+ `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3390
+ );
2784
3391
  return null;
2785
3392
  }
2786
3393
  }
@@ -2824,16 +3431,48 @@ function isAssistantInFlight(m) {
2824
3431
  if (completedOf(m) == null) return true;
2825
3432
  return finishOf(m) === "tool-calls";
2826
3433
  }
2827
- async function getSessionMessages(port, sessionId) {
3434
+ async function getSessionMessages(port, sessionId, client) {
2828
3435
  try {
2829
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/message`);
3436
+ const path = `/session/${sessionId}/message`;
3437
+ const res = await requestWithClient(port, client, path);
2830
3438
  if (!res.ok) return null;
2831
3439
  const body = await res.json();
2832
3440
  return Array.isArray(body) ? body : null;
2833
- } catch {
3441
+ } catch (error2) {
3442
+ console.error(
3443
+ `[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3444
+ );
2834
3445
  return null;
2835
3446
  }
2836
3447
  }
3448
+ async function fetchSessionMessages(port, sessionId, client) {
3449
+ const response = await requestWithClient(port, client, `/session/${sessionId}/message`);
3450
+ if (!response.ok) return null;
3451
+ const body = await response.json();
3452
+ return Array.isArray(body) ? body : null;
3453
+ }
3454
+ async function pollSessionMessagesForRedrive(port, sessionId, client) {
3455
+ try {
3456
+ const response = await requestWithClient(
3457
+ port,
3458
+ client,
3459
+ `/session/${sessionId}/message`,
3460
+ void 0,
3461
+ { allowStatuses: ALL_HTTP_STATUSES }
3462
+ );
3463
+ if (!response.ok) {
3464
+ return { ok: false, status: response.status, body: await response.text(), malformed: false };
3465
+ }
3466
+ const body = await response.json();
3467
+ if (!Array.isArray(body)) return { ok: false, status: null, body: "", malformed: true };
3468
+ return { ok: true, messages: body };
3469
+ } catch (error2) {
3470
+ console.error(
3471
+ `[pollSessionMessagesForRedrive] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3472
+ );
3473
+ return { ok: false, status: null, body: "", malformed: false };
3474
+ }
3475
+ }
2837
3476
  function isSessionActivelyGenerating(messages) {
2838
3477
  if (!messages || messages.length === 0) return false;
2839
3478
  const last = messages[messages.length - 1];
@@ -2854,27 +3493,37 @@ function sessionLastActivityMs(session) {
2854
3493
  }
2855
3494
  return null;
2856
3495
  }
2857
- async function listSessions(port) {
3496
+ async function listSessions(port, client) {
3497
+ if (client?.version === "v2") return listV2Sessions(client);
2858
3498
  try {
2859
- const res = await timedFetch(`${opencodeBase(port)}/session`);
3499
+ const res = await requestWithClient(port, client, "/session");
2860
3500
  if (!res.ok) return null;
2861
3501
  const body = await res.json();
2862
3502
  return Array.isArray(body) ? body : null;
2863
- } catch {
3503
+ } catch (error2) {
3504
+ console.error(
3505
+ `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3506
+ );
2864
3507
  return null;
2865
3508
  }
2866
3509
  }
2867
- async function deleteSession(port, id) {
3510
+ async function deleteSession(port, id, client) {
3511
+ if (client?.version === "v2") return deleteV2Session(client, id);
2868
3512
  try {
2869
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`, { method: "DELETE" });
3513
+ const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2870
3514
  return res.status >= 200 && res.status < 300;
2871
- } catch {
3515
+ } catch (error2) {
3516
+ console.error(
3517
+ `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3518
+ );
2872
3519
  return false;
2873
3520
  }
2874
3521
  }
2875
- async function sessionExists(port, id) {
3522
+ async function sessionExists(port, id, client) {
2876
3523
  try {
2877
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`);
3524
+ const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
3525
+ allowStatuses: [404]
3526
+ });
2878
3527
  if (res.status >= 200 && res.status < 300) return true;
2879
3528
  if (res.status === 404) return false;
2880
3529
  return null;
@@ -2882,9 +3531,22 @@ async function sessionExists(port, id) {
2882
3531
  return null;
2883
3532
  }
2884
3533
  }
2885
- async function getSessionStatuses(port) {
3534
+ async function getOpenCodeSession(port, id, client) {
2886
3535
  try {
2887
- const res = await timedFetch(`${opencodeBase(port)}/session/status`);
3536
+ const response = await requestWithClient(port, client, `/session/${id}`);
3537
+ const body = await response.json();
3538
+ return body && typeof body === "object" && !Array.isArray(body) ? body : null;
3539
+ } catch (error2) {
3540
+ console.error(
3541
+ `[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3542
+ );
3543
+ return null;
3544
+ }
3545
+ }
3546
+ async function getSessionStatuses(port, client) {
3547
+ if (client?.version === "v2") return null;
3548
+ try {
3549
+ const res = await requestWithClient(port, client, "/session/status");
2888
3550
  if (!res.ok) {
2889
3551
  console.error(
2890
3552
  `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
@@ -2906,22 +3568,28 @@ async function getSessionStatuses(port) {
2906
3568
  return null;
2907
3569
  }
2908
3570
  }
2909
- async function isSessionOngoing(port, id) {
2910
- const map = await getSessionStatuses(port);
3571
+ async function isSessionOngoing(port, id, client) {
3572
+ if (client?.version === "v2") return isV2SessionOngoing(client, id);
3573
+ const map = await getSessionStatuses(port, client);
2911
3574
  if (map == null) return null;
2912
3575
  const entry = map[id];
2913
3576
  return entry != null && entry.type !== "idle";
2914
3577
  }
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
- });
3578
+ async function createOpenCodeSession(port, directory, client) {
3579
+ const path = new URL(`${opencodeBase(port)}/session`);
3580
+ if (directory && directory.trim()) path.searchParams.set("directory", directory.trim());
3581
+ const requestPath = `${path.pathname}${path.search}`;
3582
+ const response = await requestWithClient(
3583
+ port,
3584
+ client,
3585
+ requestPath,
3586
+ {
3587
+ method: "POST",
3588
+ headers: { "Content-Type": "application/json" },
3589
+ body: JSON.stringify({})
3590
+ },
3591
+ { allowStatuses: ALL_HTTP_STATUSES }
3592
+ );
2925
3593
  if (!response.ok) {
2926
3594
  const text = await response.text().catch(() => "");
2927
3595
  throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
@@ -2929,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
2929
3597
  const data = await response.json();
2930
3598
  return data.id;
2931
3599
  }
2932
- async function getModelAttachmentCapability(port, model) {
3600
+ async function getModelAttachmentCapability(port, model, client) {
2933
3601
  const { model: baseModel } = splitModelVariant(model);
3602
+ if (client?.version === "v2") {
3603
+ console.error(
3604
+ `[getModelAttachmentCapability] V2 provider capabilities are unavailable; using text-only fallback (port ${port})`
3605
+ );
3606
+ return null;
3607
+ }
2934
3608
  try {
2935
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
3609
+ const res = await requestWithClient(port, client, "/config/providers");
2936
3610
  if (!res.ok) {
2937
3611
  console.error(
2938
3612
  `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3052,19 +3726,43 @@ function applyModelOptions(body, options) {
3052
3726
  }
3053
3727
  if (variant) body.variant = variant;
3054
3728
  }
3729
+ async function listOpenCodeQuestions(port, client) {
3730
+ try {
3731
+ const response = await requestWithClient(port, client, "/question");
3732
+ const body = await response.json();
3733
+ return Array.isArray(body) ? body : null;
3734
+ } catch (error2) {
3735
+ console.error(
3736
+ `[listOpenCodeQuestions] GET /question failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3737
+ );
3738
+ return null;
3739
+ }
3740
+ }
3741
+ async function listOpenCodePermissions(port, client) {
3742
+ try {
3743
+ const response = await requestWithClient(port, client, "/permission");
3744
+ const body = await response.json();
3745
+ return Array.isArray(body) ? body : null;
3746
+ } catch (error2) {
3747
+ console.error(
3748
+ `[listOpenCodePermissions] GET /permission failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3749
+ );
3750
+ return null;
3751
+ }
3752
+ }
3055
3753
  function messageText(m) {
3056
3754
  if (!m || !Array.isArray(m.parts)) return "";
3057
3755
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
3058
3756
  }
3059
- async function sendPromptAsync(port, sessionId, content, options, attachments) {
3060
- const before = await getSessionMessages(port, sessionId);
3757
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3758
+ const before = await getSessionMessages(port, sessionId, client);
3061
3759
  const knownUserIds = new Set(
3062
3760
  (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3063
3761
  );
3064
3762
  const parts = [{ type: "text", text: content }];
3065
3763
  let pendingOutcomes = null;
3066
3764
  if (attachments && attachments.inputs.length > 0) {
3067
- const capable = await getModelAttachmentCapability(port, options?.model);
3765
+ const capable = await getModelAttachmentCapability(port, options?.model, client);
3068
3766
  const {
3069
3767
  parts: fileParts,
3070
3768
  outcomes,
@@ -3077,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3077
3775
  parts
3078
3776
  };
3079
3777
  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
- });
3778
+ const res = await requestWithClient(
3779
+ port,
3780
+ client,
3781
+ `/session/${sessionId}/prompt_async`,
3782
+ {
3783
+ method: "POST",
3784
+ headers: { "Content-Type": "application/json" },
3785
+ body: JSON.stringify(body)
3786
+ },
3787
+ { allowStatuses: ALL_HTTP_STATUSES }
3788
+ );
3085
3789
  if (res.status < 200 || res.status >= 300) {
3086
3790
  const text = await res.text().catch(() => "");
3087
3791
  const { variant } = splitModelVariant(options?.model);
@@ -3092,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3092
3796
  const READ_BACK_ATTEMPTS = 5;
3093
3797
  const READ_BACK_DELAY_MS = 150;
3094
3798
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
3095
- const after = await getSessionMessages(port, sessionId);
3799
+ const after = await getSessionMessages(port, sessionId, client);
3096
3800
  if (after) {
3097
3801
  let best = null;
3098
3802
  for (const m of after) {
@@ -3195,7 +3899,7 @@ function collectSubagentSessions(messages, userMessageId) {
3195
3899
  }
3196
3900
  return refs;
3197
3901
  }
3198
- function finiteNumber(value) {
3902
+ function finiteNumber2(value) {
3199
3903
  return typeof value === "number" && Number.isFinite(value) ? value : null;
3200
3904
  }
3201
3905
  function taskCallModel(value) {
@@ -3224,8 +3928,8 @@ function collectTaskCalls(messages, userMessageId) {
3224
3928
  parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3225
3929
  model: taskCallModel(metadata?.model),
3226
3930
  status: part.state.status ?? "unknown",
3227
- timeStart: finiteNumber(part.state.time?.start),
3228
- timeEnd: finiteNumber(part.state.time?.end)
3931
+ timeStart: finiteNumber2(part.state.time?.start),
3932
+ timeEnd: finiteNumber2(part.state.time?.end)
3229
3933
  });
3230
3934
  }
3231
3935
  }
@@ -3240,7 +3944,7 @@ function attributeTaskCallUsage(messages, windows) {
3240
3944
  const unattributed = [];
3241
3945
  for (const message of messages ?? []) {
3242
3946
  if (roleOf(message) !== "assistant") continue;
3243
- const created = finiteNumber(createdOf(message));
3947
+ const created = finiteNumber2(createdOf(message));
3244
3948
  const matching = created === null ? [] : eligibleWindows.filter(
3245
3949
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3246
3950
  );
@@ -3481,9 +4185,51 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
3481
4185
  }
3482
4186
  return hasLaterUser && hasStartedLaterUser;
3483
4187
  }
3484
- async function hasAnyConfiguredProvider(port) {
4188
+ async function hasAnyConfiguredProvider(port, client) {
4189
+ if (client?.version === "v2") {
4190
+ const directory = await getOpenCodeDirectoryV2(client);
4191
+ if (!directory) {
4192
+ console.error(
4193
+ `[hasAnyConfiguredProvider] V2 working directory was unavailable (port ${port})`
4194
+ );
4195
+ return null;
4196
+ }
4197
+ const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4198
+ try {
4199
+ const res = await client.request(path);
4200
+ if (!res.ok) {
4201
+ console.error(
4202
+ `[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
4203
+ );
4204
+ return null;
4205
+ }
4206
+ const body = await res.json();
4207
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
4208
+ console.error(
4209
+ `[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
4210
+ );
4211
+ return null;
4212
+ }
4213
+ for (const integration of body.data) {
4214
+ if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
4215
+ console.error(
4216
+ `[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
4217
+ );
4218
+ return null;
4219
+ }
4220
+ }
4221
+ return body.data.some(
4222
+ (integration) => Array.isArray(integration.connections) && integration.connections.length > 0
4223
+ );
4224
+ } catch (error2) {
4225
+ console.error(
4226
+ `[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
4227
+ );
4228
+ return null;
4229
+ }
4230
+ }
3485
4231
  try {
3486
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
4232
+ const res = await requestWithClient(port, client, "/config/providers");
3487
4233
  if (!res.ok) {
3488
4234
  console.error(
3489
4235
  `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3512,7 +4258,7 @@ async function hasAnyConfiguredProvider(port) {
3512
4258
  return null;
3513
4259
  }
3514
4260
  }
3515
- function sessionErrorReason(error2) {
4261
+ function sessionErrorReason2(error2) {
3516
4262
  const record = typeof error2 === "object" && error2 !== null ? error2 : null;
3517
4263
  const data = record?.data;
3518
4264
  const dataRecord = typeof data === "object" && data !== null ? data : null;
@@ -3542,16 +4288,20 @@ function parseSessionErrorFrame(data) {
3542
4288
  if (typeof sessionId !== "string" || sessionId.length === 0) return null;
3543
4289
  return {
3544
4290
  sessionId,
3545
- reason: sessionErrorReason(propertiesRecord.error)
4291
+ reason: sessionErrorReason2(propertiesRecord.error)
3546
4292
  };
3547
4293
  }
3548
- async function readSessionErrorStream(port, options) {
4294
+ async function readSessionErrorStream(port, options, client) {
4295
+ if (client?.version === "v2") return readV2SessionErrorStream(client, options);
3549
4296
  let reader = null;
3550
4297
  try {
3551
- const response = await fetch(`${opencodeBase(port)}/event`, {
4298
+ const response = await (client?.request("/event", {
3552
4299
  headers: { accept: "text/event-stream" },
3553
4300
  signal: options.signal
3554
- });
4301
+ }) ?? fetch(`${opencodeBase(port)}/event`, {
4302
+ headers: { accept: "text/event-stream" },
4303
+ signal: options.signal
4304
+ }));
3555
4305
  if (!response.ok || !response.body) {
3556
4306
  return { reason: "unavailable", detail: `HTTP ${response.status}` };
3557
4307
  }
@@ -3582,9 +4332,10 @@ async function readSessionErrorStream(port, options) {
3582
4332
  if (reader) void reader.cancel().catch(() => void 0);
3583
4333
  }
3584
4334
  }
3585
- async function reloadProviderCache(port) {
4335
+ async function reloadProviderCache(port, client) {
4336
+ if (client?.version === "v2") return;
3586
4337
  try {
3587
- const res = await timedFetch(`${opencodeBase(port)}/config`, {
4338
+ const res = await requestWithClient(port, client, "/config", {
3588
4339
  method: "PATCH",
3589
4340
  headers: { "Content-Type": "application/json" },
3590
4341
  body: JSON.stringify({})
@@ -3962,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
3962
4713
  "content-length"
3963
4714
  ]);
3964
4715
  var StreamForwarder = class {
3965
- constructor(ws, port, callbacks = {}) {
4716
+ constructor(ws, port, callbacks = {}, options = {}) {
3966
4717
  this.ws = ws;
3967
4718
  this.port = port;
3968
4719
  this.callbacks = callbacks;
4720
+ this.options = options;
3969
4721
  }
3970
4722
  inflight = /* @__PURE__ */ new Map();
3971
4723
  /**
@@ -4049,7 +4801,15 @@ var StreamForwarder = class {
4049
4801
  }
4050
4802
  const fwdHeaders = {};
4051
4803
  for (const [k, v] of Object.entries(headers ?? {})) {
4052
- if (!STRIP_REQ.has(k.toLowerCase())) fwdHeaders[k] = v;
4804
+ const lower = k.toLowerCase();
4805
+ if (STRIP_REQ.has(lower)) continue;
4806
+ if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
4807
+ if (lower === "authorization") continue;
4808
+ }
4809
+ fwdHeaders[k] = v;
4810
+ }
4811
+ if (this.options.openCodePassword !== void 0 && this.options.openCodePassword !== null) {
4812
+ fwdHeaders.Authorization = buildOpenCodeBasicAuthHeader(this.options.openCodePassword);
4053
4813
  }
4054
4814
  this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
4055
4815
  const body = bodyPromise ? await bodyPromise : void 0;
@@ -4162,6 +4922,7 @@ function connectTunnel(options) {
4162
4922
  agentId,
4163
4923
  authHeader,
4164
4924
  port,
4925
+ openCodePassword,
4165
4926
  onConnected,
4166
4927
  onDisconnected,
4167
4928
  onError,
@@ -4179,11 +4940,16 @@ function connectTunnel(options) {
4179
4940
  Authorization: authHeader
4180
4941
  }
4181
4942
  });
4182
- const forwarder = new StreamForwarder(ws, port, {
4183
- onHead: () => onResponse?.(),
4184
- onDrainPing: () => onDrainPing?.(),
4185
- onUsageRearmPing: () => onUsageRearmPing?.()
4186
- });
4943
+ const forwarder = new StreamForwarder(
4944
+ ws,
4945
+ port,
4946
+ {
4947
+ onHead: () => onResponse?.(),
4948
+ onDrainPing: () => onDrainPing?.(),
4949
+ onUsageRearmPing: () => onUsageRearmPing?.()
4950
+ },
4951
+ { openCodePassword }
4952
+ );
4187
4953
  const connectionTimeout = setTimeout(() => {
4188
4954
  ws.close();
4189
4955
  reject(new Error("Connection timeout"));
@@ -4326,6 +5092,7 @@ var RunnerConnection = class {
4326
5092
  agentId: this.resolvedAgentId,
4327
5093
  authHeader: this.opts.getAuthHeader(),
4328
5094
  port: this.opts.port,
5095
+ openCodePassword: this.opts.openCodePassword,
4329
5096
  onConnected: (agentId) => {
4330
5097
  this.reconnectAttempt = 0;
4331
5098
  this.reconnecting = false;
@@ -4506,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
4506
5273
  function normalizeProbeModel(model) {
4507
5274
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4508
5275
  }
4509
- async function resolveProbeModels(port) {
5276
+ function isRecord2(value) {
5277
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5278
+ }
5279
+ function unsupportedProbeModels(reason, port) {
5280
+ console.error(`[resolveProbeModels] ${reason} (port ${port})`);
5281
+ return { status: "unsupported", reason };
5282
+ }
5283
+ async function resolveV1ProbeModels(client, port) {
4510
5284
  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 [];
5285
+ const response = await client.request("/config/providers");
5286
+ const body = await response.json();
5287
+ if (!isRecord2(body) || !Array.isArray(body.providers)) {
5288
+ return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
4520
5289
  }
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 [];
5290
+ const provider = body.providers.find(
5291
+ (candidate) => isRecord2(candidate) && candidate.id === "openai"
5292
+ );
5293
+ if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
5294
+ const defaults2 = isRecord2(body.default) ? body.default : void 0;
4524
5295
  const candidates = [
4525
- ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
5296
+ ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4526
5297
  ...Object.keys(provider.models)
4527
5298
  ].map(normalizeProbeModel);
4528
- return [...new Set(candidates)].slice(0, 4);
5299
+ return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
4529
5300
  } catch (err) {
4530
- console.error(
4531
- `[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
5301
+ return unsupportedProbeModels(
5302
+ `V1 GET /config/providers failed: ${err instanceof Error ? err.message : String(err)}`,
5303
+ port
5304
+ );
5305
+ }
5306
+ }
5307
+ async function resolveV2ProbeModels(client, port) {
5308
+ const directory = await getOpenCodeDirectoryV2(client);
5309
+ if (!directory) {
5310
+ return unsupportedProbeModels("V2 working directory could not be verified", port);
5311
+ }
5312
+ const path = `/api/provider?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
5313
+ try {
5314
+ const response = await client.request(path);
5315
+ const body = await response.json();
5316
+ if (!isRecord2(body) || !Array.isArray(body.data)) {
5317
+ return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
5318
+ }
5319
+ const provider = body.data.find(
5320
+ (candidate) => isRecord2(candidate) && candidate.id === "openai"
5321
+ );
5322
+ if (!provider) return { status: "supported", models: [] };
5323
+ if (!isRecord2(provider.models)) {
5324
+ return unsupportedProbeModels(
5325
+ "V2 provider response has no safe OpenAI model catalogue",
5326
+ port
5327
+ );
5328
+ }
5329
+ return {
5330
+ status: "supported",
5331
+ models: [...new Set(Object.keys(provider.models).map(normalizeProbeModel))].slice(0, 4)
5332
+ };
5333
+ } catch (err) {
5334
+ return unsupportedProbeModels(
5335
+ `V2 GET ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
5336
+ port
4532
5337
  );
4533
- return [];
4534
5338
  }
4535
5339
  }
5340
+ async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
5341
+ return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
5342
+ }
4536
5343
  function hasPrimaryHeaders(headers) {
4537
5344
  return [
4538
5345
  "x-codex-primary-used-percent",
@@ -4540,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
4540
5347
  "x-codex-primary-reset-at"
4541
5348
  ].some((name) => headers.has(name));
4542
5349
  }
4543
- async function getOpenAiUsage(port) {
5350
+ async function getOpenAiUsage(port, client) {
4544
5351
  const credentials2 = readOpenCodeChatGptCredentials();
4545
5352
  if (!credentials2) {
4546
5353
  throw new OpenAiUsageError(
@@ -4555,12 +5362,16 @@ async function getOpenAiUsage(port) {
4555
5362
  );
4556
5363
  }
4557
5364
  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");
5365
+ const lookup = await resolveProbeModels(port, client);
5366
+ if (lookup.status === "unsupported" || lookup.models.length === 0) {
5367
+ const detail = lookup.status === "unsupported" ? ` ${lookup.reason}.` : "";
5368
+ throw new OpenAiUsageError(
5369
+ `No supported OpenAI probe model is available.${detail}`,
5370
+ "no_probe_model"
5371
+ );
4561
5372
  }
4562
5373
  let lastStatus;
4563
- for (const model of models) {
5374
+ for (const model of lookup.models) {
4564
5375
  let res;
4565
5376
  try {
4566
5377
  res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
@@ -5379,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
5379
6190
  maxActiveSessions;
5380
6191
  watcherStallMs;
5381
6192
  wedgeWarningIntervalMs;
6193
+ openCodeClient;
5382
6194
  /** Cache of conversationId → opencode sessionId. */
5383
6195
  sessions = /* @__PURE__ */ new Map();
5384
6196
  /**
@@ -5497,10 +6309,15 @@ var ChannelDriver = class _ChannelDriver {
5497
6309
  * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
5498
6310
  * in opencode must still be delivered via `markDone` on the next drain — so
5499
6311
  * `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.
6312
+ * non-done path. It is cleared once the row leaves both processing and pending
6313
+ * lists, so it can never leak. A V2 prompt
6314
+ * acknowledgement with no usable id also uses this fence: OpenCode accepted the
6315
+ * turn, but there is no safe id to watch, so a failed `markFailed` report must not
6316
+ * allow the pending row to post the prompt again.
5502
6317
  */
5503
6318
  dontRedispatch = /* @__PURE__ */ new Set();
6319
+ /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6320
+ pendingMessageIds = /* @__PURE__ */ new Set();
5504
6321
  /**
5505
6322
  * "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
5506
6323
  * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
@@ -5623,7 +6440,8 @@ var ChannelDriver = class _ChannelDriver {
5623
6440
  */
5624
6441
  attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
5625
6442
  /**
5626
- * Cache of the opencode root directory (from `GET /path`). Resolved lazily on
6443
+ * Cache of the opencode root directory from the selected client's location lookup.
6444
+ * Resolved lazily on
5627
6445
  * first session creation so drain-created sessions are rooted at the project
5628
6446
  * directory and thus visible in `opencode web`'s session list. `undefined` =
5629
6447
  * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
@@ -5717,6 +6535,11 @@ var ChannelDriver = class _ChannelDriver {
5717
6535
  config.fetchImpl ?? fetch,
5718
6536
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
5719
6537
  );
6538
+ this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
6539
+ port: config.port,
6540
+ version: "v1",
6541
+ fetchImpl: config.fetchImpl
6542
+ });
5720
6543
  this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5721
6544
  this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
5722
6545
  this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
@@ -5728,9 +6551,39 @@ var ChannelDriver = class _ChannelDriver {
5728
6551
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5729
6552
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
5730
6553
  }
5731
- /** The IPv4-loopback base URL for the local `opencode serve`. */
5732
- get opencodeBase() {
5733
- return `http://127.0.0.1:${this.port}`;
6554
+ get isV2() {
6555
+ return this.openCodeClient.version === "v2";
6556
+ }
6557
+ async getSessionMessages(sessionId) {
6558
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6559
+ }
6560
+ async getSubagentSessionMessages(sessionId) {
6561
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
6562
+ }
6563
+ async getTelemetrySubagentSessionMessages(sessionId) {
6564
+ if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
6565
+ return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6566
+ }
6567
+ async listSessions() {
6568
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
6569
+ }
6570
+ async sessionExists(sessionId) {
6571
+ return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
6572
+ }
6573
+ async isSessionOngoing(sessionId) {
6574
+ return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
6575
+ }
6576
+ async getOpenCodeDirectory() {
6577
+ return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
6578
+ }
6579
+ async createOpenCodeSession(directory) {
6580
+ return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
6581
+ }
6582
+ async hasAnyConfiguredProvider() {
6583
+ return hasAnyConfiguredProvider(this.port, this.openCodeClient);
6584
+ }
6585
+ async readOpenCodeSessionErrorStream(options) {
6586
+ return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
5734
6587
  }
5735
6588
  /**
5736
6589
  * Drain all pending channel conversations once: poll → dispatch → register.
@@ -5808,6 +6661,7 @@ var ChannelDriver = class _ChannelDriver {
5808
6661
  async runDrain() {
5809
6662
  let dispatched = 0;
5810
6663
  try {
6664
+ this.pendingMessageIds.clear();
5811
6665
  const conversations = await this.getPendingConversations();
5812
6666
  if (this.recycleRequestedFlag) {
5813
6667
  this.stop();
@@ -6032,6 +6886,7 @@ var ChannelDriver = class _ChannelDriver {
6032
6886
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6033
6887
  this.ensureSessionErrorStream();
6034
6888
  const messages = await this.getPendingMessages(conv.id);
6889
+ for (const message of messages) this.pendingMessageIds.add(message.id);
6035
6890
  let dispatched = 0;
6036
6891
  let skippedAlreadyDispatched = 0;
6037
6892
  if (refusedSessionId && messages.length > 0) {
@@ -6045,6 +6900,15 @@ var ChannelDriver = class _ChannelDriver {
6045
6900
  skippedAlreadyDispatched += 1;
6046
6901
  continue;
6047
6902
  }
6903
+ if (this.dontRedispatch.has(message.id)) {
6904
+ this.log({
6905
+ level: "warn",
6906
+ message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
6907
+ conversation_id: conv.id,
6908
+ message_id: message.id
6909
+ });
6910
+ break;
6911
+ }
6048
6912
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
6049
6913
  if (effectiveOpencodeMessageId) {
6050
6914
  const outcome = await this.resolveRedrive(
@@ -6073,15 +6937,55 @@ var ChannelDriver = class _ChannelDriver {
6073
6937
  conversation_id: conv.id,
6074
6938
  message_id: message.id
6075
6939
  });
6076
- const sendAttachments = this.buildSendAttachments(conv, message);
6077
- opencodeMessageId = await this.dispatchLocked(
6940
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
6941
+ if (this.isV2 && message.attachments && message.attachments.length > 0) {
6942
+ this.signalAttachmentsSkipped(
6943
+ conv.id,
6944
+ message.id,
6945
+ message.attachments.map((attachment, index) => ({
6946
+ index,
6947
+ mime: attachment.mime,
6948
+ ...attachment.filename ? { filename: attachment.filename } : {},
6949
+ status: "skipped"
6950
+ })),
6951
+ false
6952
+ );
6953
+ }
6954
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
6078
6955
  sessionId,
6079
- () => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
6956
+ () => sendPromptAsync(
6957
+ this.port,
6958
+ sessionId,
6959
+ message.content,
6960
+ options,
6961
+ sendAttachments,
6962
+ this.openCodeClient
6963
+ )
6080
6964
  );
6081
6965
  } catch (err) {
6082
6966
  if (err instanceof ChannelAuthError) throw err;
6967
+ if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
6968
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
6969
+ this.dontRedispatch.add(message.id);
6970
+ this.log({
6971
+ level: "error",
6972
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
6973
+ conversation_id: conv.id,
6974
+ message_id: message.id
6975
+ });
6976
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
6977
+ this.log({
6978
+ level: "warn",
6979
+ message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
6980
+ conversation_id: conv.id,
6981
+ message_id: message.id
6982
+ });
6983
+ void this.postSignal(conv.id, message.id, "ack_untrackable");
6984
+ });
6985
+ break;
6986
+ }
6083
6987
  this.dispatched.delete(message.id);
6084
- const exists = await sessionExists(this.port, sessionId);
6988
+ const exists = await this.sessionExists(sessionId);
6085
6989
  if (exists === false) {
6086
6990
  this.sessions.delete(conv.id);
6087
6991
  this.log({
@@ -6130,6 +7034,9 @@ var ChannelDriver = class _ChannelDriver {
6130
7034
  break;
6131
7035
  }
6132
7036
  if (opencodeMessageId === null) {
7037
+ if (this.isV2) {
7038
+ throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7039
+ }
6133
7040
  const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
6134
7041
  if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
6135
7042
  this.log({
@@ -6277,29 +7184,38 @@ var ChannelDriver = class _ChannelDriver {
6277
7184
  */
6278
7185
  async pollSessionMessagesForRedrive(conv, message, sessionId) {
6279
7186
  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}` : ""}` };
7187
+ if (this.isV2) {
7188
+ const messages = await this.getSessionMessages(sessionId);
7189
+ if (messages === null) {
7190
+ this.log({
7191
+ level: "warn",
7192
+ message: `Re-drive: failed to poll V2 session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} \u2014 treating as unreadable this tick`,
7193
+ conversation_id: conv.id,
7194
+ message_id: message.id
7195
+ });
7196
+ return { ok: false, signature: null };
7197
+ }
7198
+ return { ok: true, messages };
6291
7199
  }
6292
- const body = await res.json();
6293
- if (!Array.isArray(body)) {
7200
+ const polledV1 = await pollSessionMessagesForRedrive(
7201
+ this.port,
7202
+ sessionId,
7203
+ this.openCodeClient
7204
+ );
7205
+ if (!polledV1.ok) {
7206
+ const normalized = normalizeRedrivePollFailureBody(polledV1.body);
6294
7207
  this.log({
6295
7208
  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`,
7209
+ message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned ${polledV1.malformed ? "a non-array message body" : `HTTP ${polledV1.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 treating as unreadable this tick`,
6297
7210
  conversation_id: conv.id,
6298
7211
  message_id: message.id
6299
7212
  });
6300
- return { ok: false, signature: "non-array message body" };
7213
+ return {
7214
+ ok: false,
7215
+ signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
7216
+ };
6301
7217
  }
6302
- return { ok: true, messages: body };
7218
+ return { ok: true, messages: polledV1.messages };
6303
7219
  } catch (err) {
6304
7220
  this.log({
6305
7221
  level: "warn",
@@ -6358,11 +7274,11 @@ var ChannelDriver = class _ChannelDriver {
6358
7274
  }
6359
7275
  const state = messageRunState(messages, ocId ?? "");
6360
7276
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
6361
- const ongoing = await isSessionOngoing(this.port, sessionId);
7277
+ const ongoing = await this.isSessionOngoing(sessionId);
6362
7278
  if (ongoing === false) {
6363
7279
  this.log({
6364
7280
  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`,
7281
+ message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
6366
7282
  conversation_id: conv.id,
6367
7283
  message_id: message.id
6368
7284
  });
@@ -6375,7 +7291,7 @@ var ChannelDriver = class _ChannelDriver {
6375
7291
  return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
6376
7292
  }
6377
7293
  if (state === "running" || state === "queued") {
6378
- const ongoing = await isSessionOngoing(this.port, sessionId);
7294
+ const ongoing = await this.isSessionOngoing(sessionId);
6379
7295
  if (ongoing === true) {
6380
7296
  if (state === "queued") {
6381
7297
  const siblingOcIds = this.siblingOpencodeMessageIds(
@@ -6835,7 +7751,7 @@ var ChannelDriver = class _ChannelDriver {
6835
7751
  };
6836
7752
  }
6837
7753
  if (bound) {
6838
- const exists = await sessionExists(this.port, bound);
7754
+ const exists = await this.sessionExists(bound);
6839
7755
  if (exists === false) {
6840
7756
  this.log({
6841
7757
  level: "debug",
@@ -6868,7 +7784,7 @@ var ChannelDriver = class _ChannelDriver {
6868
7784
  */
6869
7785
  async createAndBindSession(conversationId) {
6870
7786
  const directory = await this.resolveOpenCodeDirectory();
6871
- const sessionId = await createOpenCodeSession(this.port, directory);
7787
+ const sessionId = await this.createOpenCodeSession(directory);
6872
7788
  this.sessions.set(conversationId, sessionId);
6873
7789
  await this.persistSession(conversationId, sessionId).catch((err) => {
6874
7790
  this.log({
@@ -6880,17 +7796,18 @@ var ChannelDriver = class _ChannelDriver {
6880
7796
  return sessionId;
6881
7797
  }
6882
7798
  /**
6883
- * Lazily resolve (and cache) opencode's root directory via `GET /path`.
7799
+ * Lazily resolve (and cache) opencode's root directory via the selected client's
7800
+ * location lookup.
6884
7801
  * 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`).
7802
+ * string or `null` if unavailable (we don't keep retrying a failed lookup).
6886
7803
  */
6887
7804
  async resolveOpenCodeDirectory() {
6888
7805
  if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
6889
- this.opencodeDirectory = await getOpenCodeDirectory(this.port);
7806
+ this.opencodeDirectory = await this.getOpenCodeDirectory();
6890
7807
  if (!this.opencodeDirectory) {
6891
7808
  this.log({
6892
7809
  level: "warn",
6893
- message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
7810
+ message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
6894
7811
  });
6895
7812
  }
6896
7813
  return this.opencodeDirectory;
@@ -7324,7 +8241,7 @@ var ChannelDriver = class _ChannelDriver {
7324
8241
  while (!this.stopped && !signal.aborted) {
7325
8242
  const openedAt = this.now();
7326
8243
  try {
7327
- const outcome = await readSessionErrorStream(this.port, {
8244
+ const outcome = await this.readOpenCodeSessionErrorStream({
7328
8245
  signal,
7329
8246
  onSessionError: (event) => this.handleSessionError(event)
7330
8247
  });
@@ -7415,7 +8332,7 @@ var ChannelDriver = class _ChannelDriver {
7415
8332
  }
7416
8333
  async failFromSessionError(watcher, event, inFlight) {
7417
8334
  try {
7418
- const messages = await getSessionMessages(this.port, event.sessionId);
8335
+ const messages = await this.getSessionMessages(event.sessionId);
7419
8336
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7420
8337
  if (state !== "queued") {
7421
8338
  this.log({
@@ -7467,7 +8384,8 @@ var ChannelDriver = class _ChannelDriver {
7467
8384
  * markDone (done) exactly once per transition;
7468
8385
  * 2. applies the idle-path re-dispatch guard (a dispatched message that never
7469
8386
  * APPEARS → re-dispatch — D1 obligation 2);
7470
- * 3. polls `/question` + `/permission` (scoped to the session) and surfaces
8387
+ * 3. polls V1's global `/question` + `/permission`, or V2's
8388
+ * `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
7471
8389
  * NEW ones via `reportInteraction`, carrying the PAUSED message's own
7472
8390
  * `source_message_id`;
7473
8391
  * 4. drops messages that completed or timed out from the in-flight set.
@@ -7493,11 +8411,7 @@ var ChannelDriver = class _ChannelDriver {
7493
8411
  if (watcher.generation !== generation) return;
7494
8412
  let messages = null;
7495
8413
  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
- }
8414
+ messages = await this.getSessionMessages(sessionId);
7501
8415
  } catch {
7502
8416
  }
7503
8417
  if (messages != null && messages.length > 0) {
@@ -7730,7 +8644,7 @@ var ChannelDriver = class _ChannelDriver {
7730
8644
  inFlight.b2LastDescendantCheckMs = this.now();
7731
8645
  const [descendantOngoing, rootOngoing] = await Promise.all([
7732
8646
  this.isAnyDescendantSessionOngoing(sessionId),
7733
- isSessionOngoing(this.port, sessionId)
8647
+ this.isSessionOngoing(sessionId)
7734
8648
  ]);
7735
8649
  if (isB2AbandonmentConfirmed({
7736
8650
  pinnedForMs,
@@ -7790,7 +8704,7 @@ var ChannelDriver = class _ChannelDriver {
7790
8704
  });
7791
8705
  }
7792
8706
  const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
7793
- const ongoing = await isSessionOngoing(this.port, sessionId);
8707
+ const ongoing = await this.isSessionOngoing(sessionId);
7794
8708
  if (isAmbiguousFinishResolved({
7795
8709
  pinnedForMs,
7796
8710
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
@@ -7980,7 +8894,7 @@ var ChannelDriver = class _ChannelDriver {
7980
8894
  ...this.doneUndeliverable,
7981
8895
  ...this.readoptPollUnresolvedSignalled
7982
8896
  ]) {
7983
- if (!stillProcessing.has(id)) {
8897
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
7984
8898
  const cleared = this.dontRedispatch.delete(id);
7985
8899
  const clearedUndeliverable = this.doneUndeliverable.delete(id);
7986
8900
  this.readoptPollUnresolvedSignalled.delete(id);
@@ -8013,23 +8927,32 @@ var ChannelDriver = class _ChannelDriver {
8013
8927
  for (const [sessionId, sessionRows] of bySession) {
8014
8928
  let messages;
8015
8929
  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;
8930
+ if (this.isV2) {
8931
+ const snapshot = await this.getSessionMessages(sessionId);
8932
+ if (snapshot === null) {
8933
+ this.log({
8934
+ level: "warn",
8935
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
8936
+ });
8937
+ continue;
8938
+ }
8939
+ messages = snapshot;
8940
+ } else {
8941
+ const polled = await pollSessionMessagesForRedrive(
8942
+ this.port,
8943
+ sessionId,
8944
+ this.openCodeClient
8945
+ );
8946
+ if (!polled.ok) {
8947
+ const normalized = normalizeRedrivePollFailureBody(polled.body);
8948
+ this.log({
8949
+ level: "warn",
8950
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned ${polled.malformed ? "a non-array message body" : `HTTP ${polled.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 skipping this session this tick`
8951
+ });
8952
+ continue;
8953
+ }
8954
+ messages = polled.messages;
8031
8955
  }
8032
- messages = body;
8033
8956
  } catch (err) {
8034
8957
  this.log({
8035
8958
  level: "warn",
@@ -8038,7 +8961,7 @@ var ChannelDriver = class _ChannelDriver {
8038
8961
  continue;
8039
8962
  }
8040
8963
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
8041
- const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
8964
+ const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
8042
8965
  for (const row of sessionRows) {
8043
8966
  await this.readoptOne(sessionId, row, messages, sessionOngoing);
8044
8967
  }
@@ -8085,7 +9008,7 @@ var ChannelDriver = class _ChannelDriver {
8085
9008
  if (restartAborted) {
8086
9009
  this.log({
8087
9010
  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`,
9011
+ message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
8089
9012
  conversation_id: row.conversation_id,
8090
9013
  message_id: row.id
8091
9014
  });
@@ -8163,7 +9086,7 @@ var ChannelDriver = class _ChannelDriver {
8163
9086
  const finish = reply?.info?.finish ?? reply?.finish;
8164
9087
  this.log({
8165
9088
  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`,
9089
+ message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per the active-session status check \u2014 delivering the existing reply instead of re-dispatching`,
8167
9090
  conversation_id: row.conversation_id,
8168
9091
  message_id: row.id
8169
9092
  });
@@ -8172,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
8172
9095
  }
8173
9096
  this.log({
8174
9097
  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)`,
9098
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
8176
9099
  conversation_id: row.conversation_id,
8177
9100
  message_id: row.id
8178
9101
  });
@@ -8182,7 +9105,7 @@ var ChannelDriver = class _ChannelDriver {
8182
9105
  if (ongoing === true) {
8183
9106
  this.log({
8184
9107
  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)`,
9108
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per the active-session status check (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
8186
9109
  conversation_id: row.conversation_id,
8187
9110
  message_id: row.id
8188
9111
  });
@@ -8190,7 +9113,7 @@ var ChannelDriver = class _ChannelDriver {
8190
9113
  if (shape === "b1") {
8191
9114
  this.log({
8192
9115
  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`,
9116
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
8194
9117
  conversation_id: row.conversation_id,
8195
9118
  message_id: row.id
8196
9119
  });
@@ -8202,7 +9125,7 @@ var ChannelDriver = class _ChannelDriver {
8202
9125
  }
8203
9126
  this.log({
8204
9127
  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`,
9128
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
8206
9129
  conversation_id: row.conversation_id,
8207
9130
  message_id: row.id
8208
9131
  });
@@ -8382,19 +9305,90 @@ var ChannelDriver = class _ChannelDriver {
8382
9305
  conversation_id: row.conversation_id,
8383
9306
  message_id: row.id
8384
9307
  });
8385
- this.awaitingReadopt.add(row.id);
9308
+ if (!this.isV2) this.awaitingReadopt.add(row.id);
8386
9309
  const readoptConv = this.convForRow(sessionId, row);
8387
9310
  const readoptMessage = this.queuedMessageForRow(row);
8388
- const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
9311
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
9312
+ if (this.isV2 && row.attachments && row.attachments.length > 0) {
9313
+ this.signalAttachmentsSkipped(
9314
+ row.conversation_id,
9315
+ row.id,
9316
+ row.attachments.map((attachment, index) => ({
9317
+ index,
9318
+ mime: attachment.mime,
9319
+ ...attachment.filename ? { filename: attachment.filename } : {},
9320
+ status: "skipped"
9321
+ })),
9322
+ false
9323
+ );
9324
+ }
8389
9325
  let ocId;
8390
9326
  try {
8391
- ocId = await this.dispatchLocked(
9327
+ ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
8392
9328
  sessionId,
8393
- () => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
9329
+ () => sendPromptAsync(
9330
+ this.port,
9331
+ sessionId,
9332
+ row.content,
9333
+ options,
9334
+ sendAttachments,
9335
+ this.openCodeClient
9336
+ )
8394
9337
  );
8395
9338
  } catch (err) {
8396
9339
  this.awaitingReadopt.delete(row.id);
8397
9340
  if (err instanceof ChannelAuthError) throw err;
9341
+ if (this.isV2) {
9342
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
9343
+ const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
9344
+ if (!invalidPromptAck) {
9345
+ const exists = await this.sessionExists(sessionId);
9346
+ if (exists === false) {
9347
+ this.log({
9348
+ level: "warn",
9349
+ message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage3}`,
9350
+ conversation_id: row.conversation_id,
9351
+ message_id: row.id
9352
+ });
9353
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9354
+ return;
9355
+ }
9356
+ if (exists === null) {
9357
+ this.log({
9358
+ level: "warn",
9359
+ message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage3}`,
9360
+ conversation_id: row.conversation_id,
9361
+ message_id: row.id
9362
+ });
9363
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9364
+ return;
9365
+ }
9366
+ } else {
9367
+ this.dontRedispatch.add(row.id);
9368
+ }
9369
+ this.log({
9370
+ level: "error",
9371
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
9372
+ conversation_id: row.conversation_id,
9373
+ message_id: row.id
9374
+ });
9375
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
9376
+ (markErr) => {
9377
+ this.log({
9378
+ level: "warn",
9379
+ message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9380
+ conversation_id: row.conversation_id,
9381
+ message_id: row.id
9382
+ });
9383
+ if (invalidPromptAck) {
9384
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9385
+ } else {
9386
+ this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
9387
+ }
9388
+ }
9389
+ );
9390
+ return;
9391
+ }
8398
9392
  this.log({
8399
9393
  level: "warn",
8400
9394
  message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
@@ -8405,6 +9399,17 @@ var ChannelDriver = class _ChannelDriver {
8405
9399
  return;
8406
9400
  }
8407
9401
  if (ocId === null) {
9402
+ if (this.isV2) {
9403
+ this.dontRedispatch.add(row.id);
9404
+ this.log({
9405
+ level: "error",
9406
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} completed without an acknowledged message id`,
9407
+ conversation_id: row.conversation_id,
9408
+ message_id: row.id
9409
+ });
9410
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9411
+ return;
9412
+ }
8408
9413
  this.awaitingReadopt.delete(row.id);
8409
9414
  const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
8410
9415
  if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
@@ -8526,12 +9531,13 @@ var ChannelDriver = class _ChannelDriver {
8526
9531
  this.dispatched.delete(evidentMessageId);
8527
9532
  }
8528
9533
  /**
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).
9534
+ * Poll V1's global `/question` + `/permission`, or V2's watched-session form and
9535
+ * permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
9536
+ * carrying the PAUSED message's own `source_message_id` so the server @mentions
9537
+ * the correct person under concurrency. Dedups by interaction id across ticks
9538
+ * (reused per-session sets).
8533
9539
  *
8534
- * The interaction is attributed to the in-flight message it paused on. opencode
9540
+ * The interaction is attributed to the in-flight message it paused on. OpenCode
8535
9541
  * stamps a `messageID` on a permission (and `tool.messageID` on a question) =
8536
9542
  * the assistant message id, whose `parentID` is the user message id — but the
8537
9543
  * simplest robust attribution here is: the single in-flight message that is
@@ -8554,16 +9560,14 @@ var ChannelDriver = class _ChannelDriver {
8554
9560
  let permissionsPolledOk = true;
8555
9561
  let questions = [];
8556
9562
  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
- }
9563
+ if (this.isV2) {
9564
+ const forms = await listV2Forms(this.openCodeClient, sessionId);
9565
+ if (forms === null) questionsPolledOk = false;
9566
+ else questions = forms;
8565
9567
  } else {
8566
- questionsPolledOk = false;
9568
+ const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
9569
+ if (listed === null) questionsPolledOk = false;
9570
+ else questions = listed;
8567
9571
  }
8568
9572
  } catch {
8569
9573
  questionsPolledOk = false;
@@ -8583,16 +9587,14 @@ var ChannelDriver = class _ChannelDriver {
8583
9587
  }
8584
9588
  let permissions = [];
8585
9589
  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
- }
9590
+ if (this.isV2) {
9591
+ const listed = await listV2Permissions(this.openCodeClient, sessionId);
9592
+ if (listed === null) permissionsPolledOk = false;
9593
+ else permissions = listed;
8594
9594
  } else {
8595
- permissionsPolledOk = false;
9595
+ const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
9596
+ if (listed === null) permissionsPolledOk = false;
9597
+ else permissions = listed;
8596
9598
  }
8597
9599
  } catch {
8598
9600
  permissionsPolledOk = false;
@@ -8686,10 +9688,14 @@ var ChannelDriver = class _ChannelDriver {
8686
9688
  if (cached !== void 0) return cached;
8687
9689
  let parent = void 0;
8688
9690
  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;
9691
+ if (this.isV2) {
9692
+ const session = await getV2Session(this.openCodeClient, sessionId);
9693
+ parent = null;
9694
+ const candidate = session.parentID;
9695
+ if (typeof candidate === "string") parent = candidate;
9696
+ } else {
9697
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9698
+ parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
8693
9699
  }
8694
9700
  } catch {
8695
9701
  parent = void 0;
@@ -8743,18 +9749,16 @@ var ChannelDriver = class _ChannelDriver {
8743
9749
  if (cached) return cached;
8744
9750
  const pending = (async () => {
8745
9751
  try {
8746
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8747
- if (!res.ok) {
9752
+ const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
9753
+ if (messages2 === null) {
8748
9754
  this.log({
8749
9755
  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`,
9756
+ message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was unreadable \u2014 omitting invocation telemetry`,
8751
9757
  message_id: messageId
8752
9758
  });
8753
9759
  return null;
8754
9760
  }
8755
- const body = await res.json();
8756
- if (!Array.isArray(body)) throw new Error("response body was not a message array");
8757
- return body;
9761
+ return messages2;
8758
9762
  } catch (err) {
8759
9763
  this.log({
8760
9764
  level: "warn",
@@ -8895,21 +9899,18 @@ var ChannelDriver = class _ChannelDriver {
8895
9899
  const cached = this.sessionTitles.get(sessionId);
8896
9900
  if (cached != null) return cached;
8897
9901
  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;
9902
+ let title = "";
9903
+ if (this.isV2) {
9904
+ title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
9905
+ } else {
9906
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9907
+ title = typeof body?.title === "string" ? body.title.trim() : "";
8907
9908
  }
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
- });
9909
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
9910
+ this.sessionTitles.set(sessionId, title);
9911
+ return title;
9912
+ }
9913
+ return null;
8913
9914
  } catch (err) {
8914
9915
  this.log({
8915
9916
  level: "debug",
@@ -9007,7 +10008,7 @@ var ChannelDriver = class _ChannelDriver {
9007
10008
  * `SessionStatus` only.
9008
10009
  */
9009
10010
  async isAnyDescendantSessionAlive(rootSessionId) {
9010
- const sessions = await listSessions(this.port);
10011
+ const sessions = await this.listSessions();
9011
10012
  if (!sessions) {
9012
10013
  this.log({
9013
10014
  level: "warn",
@@ -9018,7 +10019,7 @@ var ChannelDriver = class _ChannelDriver {
9018
10019
  for (const candidate of sessions) {
9019
10020
  if (!candidate?.id || candidate.id === rootSessionId) continue;
9020
10021
  if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
9021
- const childMsgs = await getSessionMessages(this.port, candidate.id);
10022
+ const childMsgs = await this.getSubagentSessionMessages(candidate.id);
9022
10023
  if (isSessionActivelyGenerating(childMsgs)) {
9023
10024
  return true;
9024
10025
  }
@@ -9080,7 +10081,7 @@ var ChannelDriver = class _ChannelDriver {
9080
10081
  * `isB2AbandonmentConfirmed`.
9081
10082
  */
9082
10083
  async isAnyDescendantSessionOngoing(rootSessionId) {
9083
- const sessions = await listSessions(this.port);
10084
+ const sessions = await this.listSessions();
9084
10085
  if (!sessions) {
9085
10086
  this.log({
9086
10087
  level: "warn",
@@ -9097,7 +10098,7 @@ var ChannelDriver = class _ChannelDriver {
9097
10098
  continue;
9098
10099
  }
9099
10100
  if (membership === false) continue;
9100
- const ongoing = await isSessionOngoing(this.port, candidate.id);
10101
+ const ongoing = await this.isSessionOngoing(candidate.id);
9101
10102
  if (ongoing === true) return true;
9102
10103
  if (ongoing === null) indeterminate = true;
9103
10104
  }
@@ -9437,7 +10438,7 @@ var ChannelDriver = class _ChannelDriver {
9437
10438
  const classified = messageFailure(messages, userMessageId);
9438
10439
  if (classified != null) return classified;
9439
10440
  const reply = findLastAssistantReplyFor(messages, userMessageId);
9440
- const hasProvider = await hasAnyConfiguredProvider(this.port);
10441
+ const hasProvider = await this.hasAnyConfiguredProvider();
9441
10442
  return applyZeroProviderFallback(
9442
10443
  classified,
9443
10444
  hasProvider,
@@ -9510,7 +10511,7 @@ var ChannelDriver = class _ChannelDriver {
9510
10511
  const succeededProviders = /* @__PURE__ */ new Set();
9511
10512
  for (const ref of refs) {
9512
10513
  try {
9513
- const childMessages = await getSessionMessages(this.port, ref.sessionId);
10514
+ const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
9514
10515
  if (childMessages === null) {
9515
10516
  this.log({
9516
10517
  level: "debug",
@@ -9841,6 +10842,7 @@ Port ${port} is already in use.`));
9841
10842
 
9842
10843
  // src/commands/ensure-opencode-v2.ts
9843
10844
  import chalk6 from "chalk";
10845
+ import ora3 from "ora";
9844
10846
  import { select as select3 } from "@inquirer/prompts";
9845
10847
  async function probeOpenCode2WithoutPassword(port) {
9846
10848
  try {
@@ -9866,11 +10868,7 @@ function unknownPasswordError(port) {
9866
10868
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
9867
10869
  );
9868
10870
  }
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
- }
10871
+ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
9874
10872
  async function ensureOpenCode2Running(ctx) {
9875
10873
  const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
9876
10874
  if (initialHealth.authFailed) {
@@ -9913,13 +10911,37 @@ Port ${port} is already in use.`));
9913
10911
  }
9914
10912
  }
9915
10913
  if (!ctx.interactive) {
9916
- throw v2SessionSupportIncompleteError();
10914
+ ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
10915
+ const { child: proc, password } = await startOpenCode2(port, {
10916
+ inheritStdio: ctx.inheritStdio
10917
+ });
10918
+ const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
10919
+ if (!health.healthy) {
10920
+ return {
10921
+ port,
10922
+ process: proc,
10923
+ version: null,
10924
+ notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
10925
+ password
10926
+ };
10927
+ }
10928
+ ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
10929
+ return {
10930
+ port,
10931
+ process: proc,
10932
+ version: health.version ?? null,
10933
+ notReadyReason: null,
10934
+ password
10935
+ };
9917
10936
  }
9918
- console.log(chalk6.yellow(`
9919
- ${v2SessionSupportIncompleteError().message}`));
9920
10937
  const action = await select3({
9921
10938
  message: "OpenCode V2 is not running. What would you like to do?",
9922
10939
  choices: [
10940
+ {
10941
+ name: "Start OpenCode V2 for me",
10942
+ value: "start",
10943
+ description: `Run 'opencode2 serve --port ${port}'`
10944
+ },
9923
10945
  {
9924
10946
  name: "Show me the command",
9925
10947
  value: "manual",
@@ -9940,6 +10962,25 @@ ${v2SessionSupportIncompleteError().message}`));
9940
10962
  blank();
9941
10963
  throw new Error("Please start OpenCode V2 manually");
9942
10964
  }
10965
+ if (action === "start") {
10966
+ const spinner = ora3("Starting OpenCode V2...").start();
10967
+ const { child: proc, password } = await startOpenCode2(port, {
10968
+ inheritStdio: ctx.inheritStdio
10969
+ });
10970
+ const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
10971
+ if (!health.healthy) {
10972
+ spinner.fail("Failed to start OpenCode V2");
10973
+ throw new Error("OpenCode V2 failed to start");
10974
+ }
10975
+ spinner.stop();
10976
+ return {
10977
+ port,
10978
+ process: proc,
10979
+ version: health.version ?? null,
10980
+ notReadyReason: null,
10981
+ password
10982
+ };
10983
+ }
9943
10984
  return {
9944
10985
  port,
9945
10986
  process: null,
@@ -10833,7 +11874,7 @@ async function driveChannels(state, driver) {
10833
11874
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
10834
11875
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
10835
11876
  if (claudeCredentialApplied || opencodeAuthApplied) {
10836
- void reloadProviderCache(state.port).catch(
11877
+ void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
10837
11878
  (error2) => logActivity(state, {
10838
11879
  type: "error",
10839
11880
  error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
@@ -10958,7 +11999,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
10958
11999
  async function runSweep(state, driver, config) {
10959
12000
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
10960
12001
  try {
10961
- const sessions = await listSessions(state.port);
12002
+ const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
10962
12003
  if (sessions === null) {
10963
12004
  logActivity(state, {
10964
12005
  type: "info",
@@ -10988,7 +12029,7 @@ async function runSweep(state, driver, config) {
10988
12029
  });
10989
12030
  continue;
10990
12031
  }
10991
- if (await deleteSession(state.port, id)) deleted++;
12032
+ if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
10992
12033
  else failed++;
10993
12034
  }
10994
12035
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
@@ -11496,6 +12537,8 @@ async function run(options) {
11496
12537
  opencodeConnected: false,
11497
12538
  opencodeVersion: null,
11498
12539
  opencodeApiVersion: "v1",
12540
+ opencodePassword: null,
12541
+ opencodeClient: null,
11499
12542
  sessionDbProvenanceAnomaly: false,
11500
12543
  opencodeProcess: null,
11501
12544
  stopOpenCodeLogTail: null,
@@ -11650,7 +12693,7 @@ async function run(options) {
11650
12693
  console.log(chalk7.bold("Evident Run"));
11651
12694
  console.log(chalk7.dim("-".repeat(40)));
11652
12695
  }
11653
- const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
12696
+ const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
11654
12697
  let validation = await getAgentInfo(state.agentId, state.authHeader);
11655
12698
  if (!validation.valid && validation.authFailed && interactive) {
11656
12699
  spinner?.fail("Authentication failed");
@@ -11771,7 +12814,7 @@ async function run(options) {
11771
12814
  logActivity(state, { type: "info", level: "warn", message: warning2 });
11772
12815
  }
11773
12816
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
11774
- const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
12817
+ const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
11775
12818
  try {
11776
12819
  const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
11777
12820
  port: state.port,
@@ -11792,6 +12835,18 @@ async function run(options) {
11792
12835
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
11793
12836
  state.opencodeVersion = oc.version;
11794
12837
  state.opencodeApiVersion = opencodeVersion;
12838
+ let opencodePassword = null;
12839
+ if (opencodeVersion === "v2" && "password" in oc) {
12840
+ const value = oc.password;
12841
+ if (typeof value === "string" || value === null) opencodePassword = value;
12842
+ }
12843
+ state.opencodePassword = opencodePassword;
12844
+ const openCodeClient = createOpenCodeClient({
12845
+ port: state.port,
12846
+ version: state.opencodeApiVersion,
12847
+ password: state.opencodePassword
12848
+ });
12849
+ state.opencodeClient = openCodeClient;
11795
12850
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
11796
12851
  try {
11797
12852
  writeFileSync6(options.opencodePidFile, `${oc.process.pid}
@@ -11838,9 +12893,9 @@ async function run(options) {
11838
12893
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
11839
12894
  }
11840
12895
  }
11841
- await reloadProviderCache(state.port);
12896
+ await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
11842
12897
  const noProviderWarning = buildNoProviderWarning(
11843
- await hasAnyConfiguredProvider(state.port)
12898
+ await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
11844
12899
  );
11845
12900
  if (noProviderWarning) {
11846
12901
  log2(state, noProviderWarning, "warn");
@@ -11963,11 +13018,12 @@ async function run(options) {
11963
13018
  });
11964
13019
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
11965
13020
  }
11966
- const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
13021
+ const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
11967
13022
  const channelDriver = new ChannelDriver({
11968
13023
  agentId: state.agentId,
11969
13024
  port: state.port,
11970
13025
  apiUrl: getApiUrlConfig(),
13026
+ openCodeClient: state.opencodeClient ?? void 0,
11971
13027
  getAuthHeader: () => state.authHeader,
11972
13028
  conversationFilter: state.conversationFilter,
11973
13029
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
@@ -11993,6 +13049,7 @@ async function run(options) {
11993
13049
  agentId: state.agentId,
11994
13050
  getAuthHeader: () => state.authHeader,
11995
13051
  port: state.port,
13052
+ openCodePassword: state.opencodePassword,
11996
13053
  isRunning: () => state.running,
11997
13054
  events: {
11998
13055
  onConnected: (agentId, isReconnect) => {
@@ -12137,7 +13194,7 @@ async function run(options) {
12137
13194
  state.openaiUsageTimer = timer;
12138
13195
  },
12139
13196
  fetchUsage: async () => {
12140
- const usage = await getOpenAiUsage(state.port);
13197
+ const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
12141
13198
  if (usage.subscription === null) {
12142
13199
  logActivity(state, {
12143
13200
  type: "info",