@evident-ai/cli 3.4.1-dev.38181ff → 3.4.1-dev.388b76b

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.38181ff" : void 0) ?? process.env.npm_package_version ?? "unknown";
1185
+ var CLI_VERSION = (true ? "3.4.1-dev.388b76b" : 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";
@@ -2347,15 +2428,23 @@ function isQueueValidatedVersion(version2) {
2347
2428
  if (!version2) return false;
2348
2429
  return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
2349
2430
  }
2350
- function buildOpenCodeVersionWarning(version2) {
2351
- if (isQueueValidatedVersion(version2)) return null;
2352
- const detected = version2 ? `v${version2}` : "unknown";
2431
+ function buildOpenCodeVersionWarning(version2, major) {
2432
+ if (major === "v2") return null;
2353
2433
  const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
2354
- return `Warning: opencode ${detected} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
2434
+ if (!version2) {
2435
+ return `Warning: the running opencode's version could not be determined from its health response, so queue validation could not be checked (validated: ${validated}). Compare against \`opencode --version\`; continuing anyway.`;
2436
+ }
2437
+ if (isQueueValidatedVersion(version2)) return null;
2438
+ return `Warning: opencode v${version2} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
2439
+ }
2440
+ function reportedOpenCodeVersion(input) {
2441
+ if (!input.connected) return null;
2442
+ return input.version || `${input.major}-unknown`;
2355
2443
  }
2356
2444
 
2357
2445
  // src/lib/opencode/process.ts
2358
2446
  import { execSync, spawn as spawn3 } from "child_process";
2447
+ import { randomBytes } from "node:crypto";
2359
2448
 
2360
2449
  // src/lib/process-stop.ts
2361
2450
  async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
@@ -2414,6 +2503,17 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
2414
2503
  // src/lib/opencode/process.ts
2415
2504
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
2416
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
+ ]);
2417
2517
  function resolveOpenCodeLogLevel(env) {
2418
2518
  const raw = env.OPENCODE_LOG_LEVEL;
2419
2519
  if (!raw) return "INFO";
@@ -2424,6 +2524,16 @@ function resolveOpenCodeLogLevel(env) {
2424
2524
  );
2425
2525
  return "INFO";
2426
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
+ }
2427
2537
  function getProcessCwd(pid) {
2428
2538
  const platform = process.platform;
2429
2539
  try {
@@ -2606,6 +2716,37 @@ async function startOpenCode(port, options = {}) {
2606
2716
  });
2607
2717
  return child;
2608
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
+ }
2609
2750
  function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
2610
2751
  const sendSignal = (signal) => {
2611
2752
  if (process.platform === "win32") {
@@ -2753,27 +2894,500 @@ function buildNoProviderWarning(hasProvider) {
2753
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).";
2754
2895
  }
2755
2896
 
2756
- // src/lib/http-timeout.ts
2757
- var REQUEST_TIMEOUT_MS = 6e4;
2758
- function withRequestTimeout(fetchImpl, timeoutMs) {
2759
- return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
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
+ }
2760
3367
  }
2761
3368
 
2762
3369
  // src/lib/opencode/session.ts
3370
+ var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
2763
3371
  function timedFetch(input, init) {
2764
3372
  return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
2765
3373
  }
3374
+ function requestWithClient(port, client, path, init, options) {
3375
+ return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
3376
+ }
2766
3377
  function opencodeBase(port) {
2767
3378
  return `http://127.0.0.1:${port}`;
2768
3379
  }
2769
- async function getOpenCodeDirectory(port) {
3380
+ async function getOpenCodeDirectory(port, client) {
2770
3381
  try {
2771
- const res = await timedFetch(`${opencodeBase(port)}/path`);
3382
+ const res = await requestWithClient(port, client, "/path");
2772
3383
  if (!res.ok) return null;
2773
3384
  const body = await res.json();
2774
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;
2775
3386
  return dir && dir.trim() ? dir.trim() : null;
2776
- } catch {
3387
+ } catch (error2) {
3388
+ console.error(
3389
+ `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3390
+ );
2777
3391
  return null;
2778
3392
  }
2779
3393
  }
@@ -2817,16 +3431,48 @@ function isAssistantInFlight(m) {
2817
3431
  if (completedOf(m) == null) return true;
2818
3432
  return finishOf(m) === "tool-calls";
2819
3433
  }
2820
- async function getSessionMessages(port, sessionId) {
3434
+ async function getSessionMessages(port, sessionId, client) {
2821
3435
  try {
2822
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/message`);
3436
+ const path = `/session/${sessionId}/message`;
3437
+ const res = await requestWithClient(port, client, path);
2823
3438
  if (!res.ok) return null;
2824
3439
  const body = await res.json();
2825
3440
  return Array.isArray(body) ? body : null;
2826
- } catch {
3441
+ } catch (error2) {
3442
+ console.error(
3443
+ `[getSessionMessages] GET /session/${sessionId}/message failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3444
+ );
2827
3445
  return null;
2828
3446
  }
2829
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
+ }
2830
3476
  function isSessionActivelyGenerating(messages) {
2831
3477
  if (!messages || messages.length === 0) return false;
2832
3478
  const last = messages[messages.length - 1];
@@ -2847,27 +3493,37 @@ function sessionLastActivityMs(session) {
2847
3493
  }
2848
3494
  return null;
2849
3495
  }
2850
- async function listSessions(port) {
3496
+ async function listSessions(port, client) {
3497
+ if (client?.version === "v2") return listV2Sessions(client);
2851
3498
  try {
2852
- const res = await timedFetch(`${opencodeBase(port)}/session`);
3499
+ const res = await requestWithClient(port, client, "/session");
2853
3500
  if (!res.ok) return null;
2854
3501
  const body = await res.json();
2855
3502
  return Array.isArray(body) ? body : null;
2856
- } catch {
3503
+ } catch (error2) {
3504
+ console.error(
3505
+ `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3506
+ );
2857
3507
  return null;
2858
3508
  }
2859
3509
  }
2860
- async function deleteSession(port, id) {
3510
+ async function deleteSession(port, id, client) {
3511
+ if (client?.version === "v2") return deleteV2Session(client, id);
2861
3512
  try {
2862
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`, { method: "DELETE" });
3513
+ const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2863
3514
  return res.status >= 200 && res.status < 300;
2864
- } catch {
3515
+ } catch (error2) {
3516
+ console.error(
3517
+ `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3518
+ );
2865
3519
  return false;
2866
3520
  }
2867
3521
  }
2868
- async function sessionExists(port, id) {
3522
+ async function sessionExists(port, id, client) {
2869
3523
  try {
2870
- const res = await timedFetch(`${opencodeBase(port)}/session/${id}`);
3524
+ const res = await requestWithClient(port, client, `/session/${id}`, void 0, {
3525
+ allowStatuses: [404]
3526
+ });
2871
3527
  if (res.status >= 200 && res.status < 300) return true;
2872
3528
  if (res.status === 404) return false;
2873
3529
  return null;
@@ -2875,9 +3531,22 @@ async function sessionExists(port, id) {
2875
3531
  return null;
2876
3532
  }
2877
3533
  }
2878
- async function getSessionStatuses(port) {
3534
+ async function getOpenCodeSession(port, id, client) {
3535
+ try {
3536
+ const response = await requestWithClient(port, client, `/session/${id}`);
3537
+ const body = await response.json();
3538
+ return body && typeof body === "object" && !Array.isArray(body) ? body : null;
3539
+ } catch (error2) {
3540
+ console.error(
3541
+ `[getOpenCodeSession] GET /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3542
+ );
3543
+ return null;
3544
+ }
3545
+ }
3546
+ async function getSessionStatuses(port, client) {
3547
+ if (client?.version === "v2") return null;
2879
3548
  try {
2880
- const res = await timedFetch(`${opencodeBase(port)}/session/status`);
3549
+ const res = await requestWithClient(port, client, "/session/status");
2881
3550
  if (!res.ok) {
2882
3551
  console.error(
2883
3552
  `[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
@@ -2899,22 +3568,28 @@ async function getSessionStatuses(port) {
2899
3568
  return null;
2900
3569
  }
2901
3570
  }
2902
- async function isSessionOngoing(port, id) {
2903
- 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);
2904
3574
  if (map == null) return null;
2905
3575
  const entry = map[id];
2906
3576
  return entry != null && entry.type !== "idle";
2907
3577
  }
2908
- async function createOpenCodeSession(port, directory) {
2909
- const url = new URL(`${opencodeBase(port)}/session`);
2910
- if (directory && directory.trim()) {
2911
- url.searchParams.set("directory", directory.trim());
2912
- }
2913
- const response = await timedFetch(url, {
2914
- method: "POST",
2915
- headers: { "Content-Type": "application/json" },
2916
- body: JSON.stringify({})
2917
- });
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
+ );
2918
3593
  if (!response.ok) {
2919
3594
  const text = await response.text().catch(() => "");
2920
3595
  throw new Error(`Failed to create session: HTTP ${response.status}${text ? `: ${text}` : ""}`);
@@ -2922,10 +3597,16 @@ async function createOpenCodeSession(port, directory) {
2922
3597
  const data = await response.json();
2923
3598
  return data.id;
2924
3599
  }
2925
- async function getModelAttachmentCapability(port, model) {
3600
+ async function getModelAttachmentCapability(port, model, client) {
2926
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
+ }
2927
3608
  try {
2928
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
3609
+ const res = await requestWithClient(port, client, "/config/providers");
2929
3610
  if (!res.ok) {
2930
3611
  console.error(
2931
3612
  `[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3043,21 +3724,45 @@ function applyModelOptions(body, options) {
3043
3724
  };
3044
3725
  }
3045
3726
  }
3046
- if (variant) body.variant = variant;
3727
+ if (variant) body.variant = variant;
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
+ }
3047
3752
  }
3048
3753
  function messageText(m) {
3049
3754
  if (!m || !Array.isArray(m.parts)) return "";
3050
3755
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
3051
3756
  }
3052
- async function sendPromptAsync(port, sessionId, content, options, attachments) {
3053
- const before = await getSessionMessages(port, sessionId);
3757
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3758
+ const before = await getSessionMessages(port, sessionId, client);
3054
3759
  const knownUserIds = new Set(
3055
3760
  (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3056
3761
  );
3057
3762
  const parts = [{ type: "text", text: content }];
3058
3763
  let pendingOutcomes = null;
3059
3764
  if (attachments && attachments.inputs.length > 0) {
3060
- const capable = await getModelAttachmentCapability(port, options?.model);
3765
+ const capable = await getModelAttachmentCapability(port, options?.model, client);
3061
3766
  const {
3062
3767
  parts: fileParts,
3063
3768
  outcomes,
@@ -3070,11 +3775,17 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3070
3775
  parts
3071
3776
  };
3072
3777
  applyModelOptions(body, options);
3073
- const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
3074
- method: "POST",
3075
- headers: { "Content-Type": "application/json" },
3076
- body: JSON.stringify(body)
3077
- });
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
+ );
3078
3789
  if (res.status < 200 || res.status >= 300) {
3079
3790
  const text = await res.text().catch(() => "");
3080
3791
  const { variant } = splitModelVariant(options?.model);
@@ -3085,7 +3796,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
3085
3796
  const READ_BACK_ATTEMPTS = 5;
3086
3797
  const READ_BACK_DELAY_MS = 150;
3087
3798
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
3088
- const after = await getSessionMessages(port, sessionId);
3799
+ const after = await getSessionMessages(port, sessionId, client);
3089
3800
  if (after) {
3090
3801
  let best = null;
3091
3802
  for (const m of after) {
@@ -3188,7 +3899,7 @@ function collectSubagentSessions(messages, userMessageId) {
3188
3899
  }
3189
3900
  return refs;
3190
3901
  }
3191
- function finiteNumber(value) {
3902
+ function finiteNumber2(value) {
3192
3903
  return typeof value === "number" && Number.isFinite(value) ? value : null;
3193
3904
  }
3194
3905
  function taskCallModel(value) {
@@ -3217,8 +3928,8 @@ function collectTaskCalls(messages, userMessageId) {
3217
3928
  parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
3218
3929
  model: taskCallModel(metadata?.model),
3219
3930
  status: part.state.status ?? "unknown",
3220
- timeStart: finiteNumber(part.state.time?.start),
3221
- timeEnd: finiteNumber(part.state.time?.end)
3931
+ timeStart: finiteNumber2(part.state.time?.start),
3932
+ timeEnd: finiteNumber2(part.state.time?.end)
3222
3933
  });
3223
3934
  }
3224
3935
  }
@@ -3233,7 +3944,7 @@ function attributeTaskCallUsage(messages, windows) {
3233
3944
  const unattributed = [];
3234
3945
  for (const message of messages ?? []) {
3235
3946
  if (roleOf(message) !== "assistant") continue;
3236
- const created = finiteNumber(createdOf(message));
3947
+ const created = finiteNumber2(createdOf(message));
3237
3948
  const matching = created === null ? [] : eligibleWindows.filter(
3238
3949
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
3239
3950
  );
@@ -3474,9 +4185,51 @@ function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageI
3474
4185
  }
3475
4186
  return hasLaterUser && hasStartedLaterUser;
3476
4187
  }
3477
- 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
+ }
3478
4231
  try {
3479
- const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
4232
+ const res = await requestWithClient(port, client, "/config/providers");
3480
4233
  if (!res.ok) {
3481
4234
  console.error(
3482
4235
  `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
@@ -3505,7 +4258,7 @@ async function hasAnyConfiguredProvider(port) {
3505
4258
  return null;
3506
4259
  }
3507
4260
  }
3508
- function sessionErrorReason(error2) {
4261
+ function sessionErrorReason2(error2) {
3509
4262
  const record = typeof error2 === "object" && error2 !== null ? error2 : null;
3510
4263
  const data = record?.data;
3511
4264
  const dataRecord = typeof data === "object" && data !== null ? data : null;
@@ -3535,16 +4288,20 @@ function parseSessionErrorFrame(data) {
3535
4288
  if (typeof sessionId !== "string" || sessionId.length === 0) return null;
3536
4289
  return {
3537
4290
  sessionId,
3538
- reason: sessionErrorReason(propertiesRecord.error)
4291
+ reason: sessionErrorReason2(propertiesRecord.error)
3539
4292
  };
3540
4293
  }
3541
- async function readSessionErrorStream(port, options) {
4294
+ async function readSessionErrorStream(port, options, client) {
4295
+ if (client?.version === "v2") return readV2SessionErrorStream(client, options);
3542
4296
  let reader = null;
3543
4297
  try {
3544
- const response = await fetch(`${opencodeBase(port)}/event`, {
4298
+ const response = await (client?.request("/event", {
3545
4299
  headers: { accept: "text/event-stream" },
3546
4300
  signal: options.signal
3547
- });
4301
+ }) ?? fetch(`${opencodeBase(port)}/event`, {
4302
+ headers: { accept: "text/event-stream" },
4303
+ signal: options.signal
4304
+ }));
3548
4305
  if (!response.ok || !response.body) {
3549
4306
  return { reason: "unavailable", detail: `HTTP ${response.status}` };
3550
4307
  }
@@ -3575,9 +4332,10 @@ async function readSessionErrorStream(port, options) {
3575
4332
  if (reader) void reader.cancel().catch(() => void 0);
3576
4333
  }
3577
4334
  }
3578
- async function reloadProviderCache(port) {
4335
+ async function reloadProviderCache(port, client) {
4336
+ if (client?.version === "v2") return;
3579
4337
  try {
3580
- const res = await timedFetch(`${opencodeBase(port)}/config`, {
4338
+ const res = await requestWithClient(port, client, "/config", {
3581
4339
  method: "PATCH",
3582
4340
  headers: { "Content-Type": "application/json" },
3583
4341
  body: JSON.stringify({})
@@ -3955,10 +4713,11 @@ var STRIP_RES = /* @__PURE__ */ new Set([
3955
4713
  "content-length"
3956
4714
  ]);
3957
4715
  var StreamForwarder = class {
3958
- constructor(ws, port, callbacks = {}) {
4716
+ constructor(ws, port, callbacks = {}, options = {}) {
3959
4717
  this.ws = ws;
3960
4718
  this.port = port;
3961
4719
  this.callbacks = callbacks;
4720
+ this.options = options;
3962
4721
  }
3963
4722
  inflight = /* @__PURE__ */ new Map();
3964
4723
  /**
@@ -4042,7 +4801,15 @@ var StreamForwarder = class {
4042
4801
  }
4043
4802
  const fwdHeaders = {};
4044
4803
  for (const [k, v] of Object.entries(headers ?? {})) {
4045
- 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);
4046
4813
  }
4047
4814
  this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
4048
4815
  const body = bodyPromise ? await bodyPromise : void 0;
@@ -4155,6 +4922,7 @@ function connectTunnel(options) {
4155
4922
  agentId,
4156
4923
  authHeader,
4157
4924
  port,
4925
+ openCodePassword,
4158
4926
  onConnected,
4159
4927
  onDisconnected,
4160
4928
  onError,
@@ -4172,11 +4940,16 @@ function connectTunnel(options) {
4172
4940
  Authorization: authHeader
4173
4941
  }
4174
4942
  });
4175
- const forwarder = new StreamForwarder(ws, port, {
4176
- onHead: () => onResponse?.(),
4177
- onDrainPing: () => onDrainPing?.(),
4178
- onUsageRearmPing: () => onUsageRearmPing?.()
4179
- });
4943
+ const forwarder = new StreamForwarder(
4944
+ ws,
4945
+ port,
4946
+ {
4947
+ onHead: () => onResponse?.(),
4948
+ onDrainPing: () => onDrainPing?.(),
4949
+ onUsageRearmPing: () => onUsageRearmPing?.()
4950
+ },
4951
+ { openCodePassword }
4952
+ );
4180
4953
  const connectionTimeout = setTimeout(() => {
4181
4954
  ws.close();
4182
4955
  reject(new Error("Connection timeout"));
@@ -4319,6 +5092,7 @@ var RunnerConnection = class {
4319
5092
  agentId: this.resolvedAgentId,
4320
5093
  authHeader: this.opts.getAuthHeader(),
4321
5094
  port: this.opts.port,
5095
+ openCodePassword: this.opts.openCodePassword,
4322
5096
  onConnected: (agentId) => {
4323
5097
  this.reconnectAttempt = 0;
4324
5098
  this.reconnecting = false;
@@ -4499,33 +5273,73 @@ function parseCodexUsageHeaders(headers) {
4499
5273
  function normalizeProbeModel(model) {
4500
5274
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4501
5275
  }
4502
- 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) {
4503
5284
  try {
4504
- const res = await withRequestTimeout(
4505
- fetch,
4506
- REQUEST_TIMEOUT_MS
4507
- )(`${opencodeBase(port)}/config/providers`);
4508
- if (!res.ok) {
4509
- console.error(
4510
- `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
4511
- );
4512
- return [];
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);
4513
5289
  }
4514
- const body = await res.json();
4515
- const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
4516
- if (!provider || !provider.models || typeof provider.models !== "object") return [];
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;
4517
5295
  const candidates = [
4518
- ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
5296
+ ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4519
5297
  ...Object.keys(provider.models)
4520
5298
  ].map(normalizeProbeModel);
4521
- return [...new Set(candidates)].slice(0, 4);
5299
+ return { status: "supported", models: [...new Set(candidates)].slice(0, 4) };
4522
5300
  } catch (err) {
4523
- console.error(
4524
- `[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
4525
5337
  );
4526
- return [];
4527
5338
  }
4528
5339
  }
5340
+ async function resolveProbeModels(port, client = createOpenCodeClient({ port, version: "v1" })) {
5341
+ return client.version === "v2" ? resolveV2ProbeModels(client, port) : resolveV1ProbeModels(client, port);
5342
+ }
4529
5343
  function hasPrimaryHeaders(headers) {
4530
5344
  return [
4531
5345
  "x-codex-primary-used-percent",
@@ -4533,7 +5347,7 @@ function hasPrimaryHeaders(headers) {
4533
5347
  "x-codex-primary-reset-at"
4534
5348
  ].some((name) => headers.has(name));
4535
5349
  }
4536
- async function getOpenAiUsage(port) {
5350
+ async function getOpenAiUsage(port, client) {
4537
5351
  const credentials2 = readOpenCodeChatGptCredentials();
4538
5352
  if (!credentials2) {
4539
5353
  throw new OpenAiUsageError(
@@ -4548,12 +5362,16 @@ async function getOpenAiUsage(port) {
4548
5362
  );
4549
5363
  }
4550
5364
  const subscription = parseChatGptIdentity(credentials2.accessToken);
4551
- const models = await resolveProbeModels(port);
4552
- if (models.length === 0) {
4553
- throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
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
+ );
4554
5372
  }
4555
5373
  let lastStatus;
4556
- for (const model of models) {
5374
+ for (const model of lookup.models) {
4557
5375
  let res;
4558
5376
  try {
4559
5377
  res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
@@ -5372,6 +6190,7 @@ var ChannelDriver = class _ChannelDriver {
5372
6190
  maxActiveSessions;
5373
6191
  watcherStallMs;
5374
6192
  wedgeWarningIntervalMs;
6193
+ openCodeClient;
5375
6194
  /** Cache of conversationId → opencode sessionId. */
5376
6195
  sessions = /* @__PURE__ */ new Map();
5377
6196
  /**
@@ -5480,20 +6299,23 @@ var ChannelDriver = class _ChannelDriver {
5480
6299
  */
5481
6300
  readopted = /* @__PURE__ */ new Set();
5482
6301
  /**
5483
- * "Don't re-DISPATCH / re-attach this orphan again" (Bug 2/5). Set when a
5484
- * re-adopted running/orphan row's watcher hit its `processed_at`-anchored
5485
- * deadline (or an orphan whose window already elapsed): the still-`processing`
5486
- * server row would otherwise be re-adopted (and re-dispatched) on EVERY ~2s
5487
- * drain until the 15-min cron resets it spamming new turns.
5488
- *
5489
- * CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
5490
- * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
5491
- * in opencode must still be delivered via `markDone` on the next drain — so
5492
- * `readoptOne` computes `state` FIRST and this set is checked only on the
5493
- * non-done path. It is cleared once the row leaves the processing list (cron
5494
- * reset → it drains normally as `pending`), so it can never leak.
6302
+ * Readopt give-up fence. Set when recovery declines to start or continue a turn
6303
+ * for a row that is still `processing`, so the next drain does not re-dispatch or
6304
+ * re-attach it before the cron safety net acts. It suppresses only non-done
6305
+ * recovery paths; DONE delivery still runs. Clear it when
6306
+ * `!stillProcessing.has(id)`, because leaving `processing` hands the row back to
6307
+ * normal processing.
5495
6308
  */
5496
6309
  dontRedispatch = /* @__PURE__ */ new Set();
6310
+ /**
6311
+ * Untrackable-ack fence. Set after OpenCode accepts a prompt without returning a
6312
+ * usable message id, because another POST could create a duplicate turn. Keep it
6313
+ * fenced while the row is `processing` or `pending`; clear it only when the row
6314
+ * is absent from both lists.
6315
+ */
6316
+ untrackableAck = /* @__PURE__ */ new Set();
6317
+ /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6318
+ pendingMessageIds = /* @__PURE__ */ new Set();
5497
6319
  /**
5498
6320
  * "markDone for this row is TERMINALLY undeliverable" (Bug 4). Set ONLY when a
5499
6321
  * re-adopted DONE row's `markDone` returned a terminal 4xx (a status that will
@@ -5616,7 +6438,8 @@ var ChannelDriver = class _ChannelDriver {
5616
6438
  */
5617
6439
  attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
5618
6440
  /**
5619
- * Cache of the opencode root directory (from `GET /path`). Resolved lazily on
6441
+ * Cache of the opencode root directory from the selected client's location lookup.
6442
+ * Resolved lazily on
5620
6443
  * first session creation so drain-created sessions are rooted at the project
5621
6444
  * directory and thus visible in `opencode web`'s session list. `undefined` =
5622
6445
  * not yet resolved; `null` = resolved-but-unavailable (don't keep retrying).
@@ -5710,6 +6533,11 @@ var ChannelDriver = class _ChannelDriver {
5710
6533
  config.fetchImpl ?? fetch,
5711
6534
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
5712
6535
  );
6536
+ this.openCodeClient = config.openCodeClient ?? createOpenCodeClient({
6537
+ port: config.port,
6538
+ version: "v1",
6539
+ fetchImpl: config.fetchImpl
6540
+ });
5713
6541
  this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5714
6542
  this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
5715
6543
  this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
@@ -5721,9 +6549,39 @@ var ChannelDriver = class _ChannelDriver {
5721
6549
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
5722
6550
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
5723
6551
  }
5724
- /** The IPv4-loopback base URL for the local `opencode serve`. */
5725
- get opencodeBase() {
5726
- return `http://127.0.0.1:${this.port}`;
6552
+ get isV2() {
6553
+ return this.openCodeClient.version === "v2";
6554
+ }
6555
+ async getSessionMessages(sessionId) {
6556
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6557
+ }
6558
+ async getSubagentSessionMessages(sessionId) {
6559
+ return this.isV2 ? getV2SessionMessages(this.openCodeClient, sessionId) : getSessionMessages(this.port, sessionId, this.openCodeClient);
6560
+ }
6561
+ async getTelemetrySubagentSessionMessages(sessionId) {
6562
+ if (this.isV2) return getV2SessionMessages(this.openCodeClient, sessionId);
6563
+ return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6564
+ }
6565
+ async listSessions() {
6566
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
6567
+ }
6568
+ async sessionExists(sessionId) {
6569
+ return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
6570
+ }
6571
+ async isSessionOngoing(sessionId) {
6572
+ return this.isV2 ? isV2SessionOngoing(this.openCodeClient, sessionId) : isSessionOngoing(this.port, sessionId, this.openCodeClient);
6573
+ }
6574
+ async getOpenCodeDirectory() {
6575
+ return this.isV2 ? getOpenCodeDirectoryV2(this.openCodeClient) : getOpenCodeDirectory(this.port, this.openCodeClient);
6576
+ }
6577
+ async createOpenCodeSession(directory) {
6578
+ return this.isV2 ? createV2Session(this.openCodeClient, directory) : createOpenCodeSession(this.port, directory, this.openCodeClient);
6579
+ }
6580
+ async hasAnyConfiguredProvider() {
6581
+ return hasAnyConfiguredProvider(this.port, this.openCodeClient);
6582
+ }
6583
+ async readOpenCodeSessionErrorStream(options) {
6584
+ return this.isV2 ? readV2SessionErrorStream(this.openCodeClient, options) : readSessionErrorStream(this.port, options, this.openCodeClient);
5727
6585
  }
5728
6586
  /**
5729
6587
  * Drain all pending channel conversations once: poll → dispatch → register.
@@ -5801,6 +6659,7 @@ var ChannelDriver = class _ChannelDriver {
5801
6659
  async runDrain() {
5802
6660
  let dispatched = 0;
5803
6661
  try {
6662
+ this.pendingMessageIds.clear();
5804
6663
  const conversations = await this.getPendingConversations();
5805
6664
  if (this.recycleRequestedFlag) {
5806
6665
  this.stop();
@@ -6025,6 +6884,7 @@ var ChannelDriver = class _ChannelDriver {
6025
6884
  const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
6026
6885
  this.ensureSessionErrorStream();
6027
6886
  const messages = await this.getPendingMessages(conv.id);
6887
+ for (const message of messages) this.pendingMessageIds.add(message.id);
6028
6888
  let dispatched = 0;
6029
6889
  let skippedAlreadyDispatched = 0;
6030
6890
  if (refusedSessionId && messages.length > 0) {
@@ -6038,6 +6898,15 @@ var ChannelDriver = class _ChannelDriver {
6038
6898
  skippedAlreadyDispatched += 1;
6039
6899
  continue;
6040
6900
  }
6901
+ if (this.untrackableAck.has(message.id)) {
6902
+ this.log({
6903
+ level: "warn",
6904
+ message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
6905
+ conversation_id: conv.id,
6906
+ message_id: message.id
6907
+ });
6908
+ break;
6909
+ }
6041
6910
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
6042
6911
  if (effectiveOpencodeMessageId) {
6043
6912
  const outcome = await this.resolveRedrive(
@@ -6066,15 +6935,55 @@ var ChannelDriver = class _ChannelDriver {
6066
6935
  conversation_id: conv.id,
6067
6936
  message_id: message.id
6068
6937
  });
6069
- const sendAttachments = this.buildSendAttachments(conv, message);
6070
- opencodeMessageId = await this.dispatchLocked(
6938
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(conv, message);
6939
+ if (this.isV2 && message.attachments && message.attachments.length > 0) {
6940
+ this.signalAttachmentsSkipped(
6941
+ conv.id,
6942
+ message.id,
6943
+ message.attachments.map((attachment, index) => ({
6944
+ index,
6945
+ mime: attachment.mime,
6946
+ ...attachment.filename ? { filename: attachment.filename } : {},
6947
+ status: "skipped"
6948
+ })),
6949
+ false
6950
+ );
6951
+ }
6952
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
6071
6953
  sessionId,
6072
- () => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
6954
+ () => sendPromptAsync(
6955
+ this.port,
6956
+ sessionId,
6957
+ message.content,
6958
+ options,
6959
+ sendAttachments,
6960
+ this.openCodeClient
6961
+ )
6073
6962
  );
6074
6963
  } catch (err) {
6075
6964
  if (err instanceof ChannelAuthError) throw err;
6965
+ if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
6966
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
6967
+ this.untrackableAck.add(message.id);
6968
+ this.log({
6969
+ level: "error",
6970
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
6971
+ conversation_id: conv.id,
6972
+ message_id: message.id
6973
+ });
6974
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
6975
+ this.log({
6976
+ level: "warn",
6977
+ message: `markFailed PATCH for V2 dispatch failure on message ${message.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
6978
+ conversation_id: conv.id,
6979
+ message_id: message.id
6980
+ });
6981
+ void this.postSignal(conv.id, message.id, "ack_untrackable");
6982
+ });
6983
+ break;
6984
+ }
6076
6985
  this.dispatched.delete(message.id);
6077
- const exists = await sessionExists(this.port, sessionId);
6986
+ const exists = await this.sessionExists(sessionId);
6078
6987
  if (exists === false) {
6079
6988
  this.sessions.delete(conv.id);
6080
6989
  this.log({
@@ -6123,6 +7032,9 @@ var ChannelDriver = class _ChannelDriver {
6123
7032
  break;
6124
7033
  }
6125
7034
  if (opencodeMessageId === null) {
7035
+ if (this.isV2) {
7036
+ throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7037
+ }
6126
7038
  const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
6127
7039
  if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
6128
7040
  this.log({
@@ -6270,29 +7182,38 @@ var ChannelDriver = class _ChannelDriver {
6270
7182
  */
6271
7183
  async pollSessionMessagesForRedrive(conv, message, sessionId) {
6272
7184
  try {
6273
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
6274
- if (!res.ok) {
6275
- const rawBody = await res.text();
6276
- const normalized = normalizeRedrivePollFailureBody(rawBody);
6277
- this.log({
6278
- level: "warn",
6279
- message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned HTTP ${res.status}${normalized ? `: ${normalized}` : ""} \u2014 treating as unreadable this tick`,
6280
- conversation_id: conv.id,
6281
- message_id: message.id
6282
- });
6283
- return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ""}` };
7185
+ if (this.isV2) {
7186
+ const messages = await this.getSessionMessages(sessionId);
7187
+ if (messages === null) {
7188
+ this.log({
7189
+ level: "warn",
7190
+ message: `Re-drive: failed to poll V2 session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} \u2014 treating as unreadable this tick`,
7191
+ conversation_id: conv.id,
7192
+ message_id: message.id
7193
+ });
7194
+ return { ok: false, signature: null };
7195
+ }
7196
+ return { ok: true, messages };
6284
7197
  }
6285
- const body = await res.json();
6286
- if (!Array.isArray(body)) {
7198
+ const polledV1 = await pollSessionMessagesForRedrive(
7199
+ this.port,
7200
+ sessionId,
7201
+ this.openCodeClient
7202
+ );
7203
+ if (!polledV1.ok) {
7204
+ const normalized = normalizeRedrivePollFailureBody(polledV1.body);
6287
7205
  this.log({
6288
7206
  level: "warn",
6289
- message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned a non-array message body \u2014 treating as unreadable this tick`,
7207
+ message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned ${polledV1.malformed ? "a non-array message body" : `HTTP ${polledV1.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 treating as unreadable this tick`,
6290
7208
  conversation_id: conv.id,
6291
7209
  message_id: message.id
6292
7210
  });
6293
- return { ok: false, signature: "non-array message body" };
7211
+ return {
7212
+ ok: false,
7213
+ signature: polledV1.status === null && !polledV1.malformed ? null : polledV1.malformed ? "non-array message body" : `HTTP ${polledV1.status}${normalized ? `: ${normalized}` : ""}`
7214
+ };
6294
7215
  }
6295
- return { ok: true, messages: body };
7216
+ return { ok: true, messages: polledV1.messages };
6296
7217
  } catch (err) {
6297
7218
  this.log({
6298
7219
  level: "warn",
@@ -6351,11 +7272,11 @@ var ChannelDriver = class _ChannelDriver {
6351
7272
  }
6352
7273
  const state = messageRunState(messages, ocId ?? "");
6353
7274
  if (state === "failed" && isAbortedTerminalReply(messages, ocId ?? "")) {
6354
- const ongoing = await isSessionOngoing(this.port, sessionId);
7275
+ const ongoing = await this.isSessionOngoing(sessionId);
6355
7276
  if (ongoing === false) {
6356
7277
  this.log({
6357
7278
  level: "info",
6358
- message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
7279
+ message: `Re-drive: message ${message.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
6359
7280
  conversation_id: conv.id,
6360
7281
  message_id: message.id
6361
7282
  });
@@ -6368,7 +7289,7 @@ var ChannelDriver = class _ChannelDriver {
6368
7289
  return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
6369
7290
  }
6370
7291
  if (state === "running" || state === "queued") {
6371
- const ongoing = await isSessionOngoing(this.port, sessionId);
7292
+ const ongoing = await this.isSessionOngoing(sessionId);
6372
7293
  if (ongoing === true) {
6373
7294
  if (state === "queued") {
6374
7295
  const siblingOcIds = this.siblingOpencodeMessageIds(
@@ -6828,7 +7749,7 @@ var ChannelDriver = class _ChannelDriver {
6828
7749
  };
6829
7750
  }
6830
7751
  if (bound) {
6831
- const exists = await sessionExists(this.port, bound);
7752
+ const exists = await this.sessionExists(bound);
6832
7753
  if (exists === false) {
6833
7754
  this.log({
6834
7755
  level: "debug",
@@ -6861,7 +7782,7 @@ var ChannelDriver = class _ChannelDriver {
6861
7782
  */
6862
7783
  async createAndBindSession(conversationId) {
6863
7784
  const directory = await this.resolveOpenCodeDirectory();
6864
- const sessionId = await createOpenCodeSession(this.port, directory);
7785
+ const sessionId = await this.createOpenCodeSession(directory);
6865
7786
  this.sessions.set(conversationId, sessionId);
6866
7787
  await this.persistSession(conversationId, sessionId).catch((err) => {
6867
7788
  this.log({
@@ -6873,17 +7794,18 @@ var ChannelDriver = class _ChannelDriver {
6873
7794
  return sessionId;
6874
7795
  }
6875
7796
  /**
6876
- * Lazily resolve (and cache) opencode's root directory via `GET /path`.
7797
+ * Lazily resolve (and cache) opencode's root directory via the selected client's
7798
+ * location lookup.
6877
7799
  * Resolved once per driver: `undefined` until first lookup, then the directory
6878
- * string or `null` if unavailable (we don't keep retrying a missing `/path`).
7800
+ * string or `null` if unavailable (we don't keep retrying a failed lookup).
6879
7801
  */
6880
7802
  async resolveOpenCodeDirectory() {
6881
7803
  if (this.opencodeDirectory !== void 0) return this.opencodeDirectory;
6882
- this.opencodeDirectory = await getOpenCodeDirectory(this.port);
7804
+ this.opencodeDirectory = await this.getOpenCodeDirectory();
6883
7805
  if (!this.opencodeDirectory) {
6884
7806
  this.log({
6885
7807
  level: "warn",
6886
- message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
7808
+ message: "Could not determine opencode directory (location lookup failed) \u2014 new sessions may not appear in opencode web"
6887
7809
  });
6888
7810
  }
6889
7811
  return this.opencodeDirectory;
@@ -7317,7 +8239,7 @@ var ChannelDriver = class _ChannelDriver {
7317
8239
  while (!this.stopped && !signal.aborted) {
7318
8240
  const openedAt = this.now();
7319
8241
  try {
7320
- const outcome = await readSessionErrorStream(this.port, {
8242
+ const outcome = await this.readOpenCodeSessionErrorStream({
7321
8243
  signal,
7322
8244
  onSessionError: (event) => this.handleSessionError(event)
7323
8245
  });
@@ -7408,7 +8330,7 @@ var ChannelDriver = class _ChannelDriver {
7408
8330
  }
7409
8331
  async failFromSessionError(watcher, event, inFlight) {
7410
8332
  try {
7411
- const messages = await getSessionMessages(this.port, event.sessionId);
8333
+ const messages = await this.getSessionMessages(event.sessionId);
7412
8334
  const state = messageRunState(messages, inFlight.opencodeMessageId);
7413
8335
  if (state !== "queued") {
7414
8336
  this.log({
@@ -7460,7 +8382,8 @@ var ChannelDriver = class _ChannelDriver {
7460
8382
  * markDone (done) exactly once per transition;
7461
8383
  * 2. applies the idle-path re-dispatch guard (a dispatched message that never
7462
8384
  * APPEARS → re-dispatch — D1 obligation 2);
7463
- * 3. polls `/question` + `/permission` (scoped to the session) and surfaces
8385
+ * 3. polls V1's global `/question` + `/permission`, or V2's
8386
+ * `/api/session/:id/form` + `/api/session/:id/permission`, and surfaces
7464
8387
  * NEW ones via `reportInteraction`, carrying the PAUSED message's own
7465
8388
  * `source_message_id`;
7466
8389
  * 4. drops messages that completed or timed out from the in-flight set.
@@ -7486,11 +8409,7 @@ var ChannelDriver = class _ChannelDriver {
7486
8409
  if (watcher.generation !== generation) return;
7487
8410
  let messages = null;
7488
8411
  try {
7489
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
7490
- if (res.ok) {
7491
- const body = await res.json();
7492
- messages = Array.isArray(body) ? body : null;
7493
- }
8412
+ messages = await this.getSessionMessages(sessionId);
7494
8413
  } catch {
7495
8414
  }
7496
8415
  if (messages != null && messages.length > 0) {
@@ -7723,7 +8642,7 @@ var ChannelDriver = class _ChannelDriver {
7723
8642
  inFlight.b2LastDescendantCheckMs = this.now();
7724
8643
  const [descendantOngoing, rootOngoing] = await Promise.all([
7725
8644
  this.isAnyDescendantSessionOngoing(sessionId),
7726
- isSessionOngoing(this.port, sessionId)
8645
+ this.isSessionOngoing(sessionId)
7727
8646
  ]);
7728
8647
  if (isB2AbandonmentConfirmed({
7729
8648
  pinnedForMs,
@@ -7783,7 +8702,7 @@ var ChannelDriver = class _ChannelDriver {
7783
8702
  });
7784
8703
  }
7785
8704
  const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
7786
- const ongoing = await isSessionOngoing(this.port, sessionId);
8705
+ const ongoing = await this.isSessionOngoing(sessionId);
7787
8706
  if (isAmbiguousFinishResolved({
7788
8707
  pinnedForMs,
7789
8708
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
@@ -7966,7 +8885,7 @@ var ChannelDriver = class _ChannelDriver {
7966
8885
  */
7967
8886
  async readoptProcessing() {
7968
8887
  const rows = await this.getProcessingMessages();
7969
- if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
8888
+ if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
7970
8889
  const stillProcessing = new Set(rows.map((r) => r.id));
7971
8890
  for (const id of [
7972
8891
  ...this.dontRedispatch,
@@ -7986,6 +8905,16 @@ var ChannelDriver = class _ChannelDriver {
7986
8905
  }
7987
8906
  }
7988
8907
  }
8908
+ for (const id of [...this.untrackableAck]) {
8909
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
8910
+ this.untrackableAck.delete(id);
8911
+ this.log({
8912
+ level: "debug",
8913
+ message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
8914
+ message_id: id
8915
+ });
8916
+ }
8917
+ }
7989
8918
  }
7990
8919
  if (rows.length === 0) return;
7991
8920
  const bySession = /* @__PURE__ */ new Map();
@@ -8006,23 +8935,32 @@ var ChannelDriver = class _ChannelDriver {
8006
8935
  for (const [sessionId, sessionRows] of bySession) {
8007
8936
  let messages;
8008
8937
  try {
8009
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8010
- if (!res.ok) {
8011
- this.log({
8012
- level: "warn",
8013
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
8014
- });
8015
- continue;
8016
- }
8017
- const body = await res.json();
8018
- if (!Array.isArray(body)) {
8019
- this.log({
8020
- level: "warn",
8021
- message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
8022
- });
8023
- continue;
8938
+ if (this.isV2) {
8939
+ const snapshot = await this.getSessionMessages(sessionId);
8940
+ if (snapshot === null) {
8941
+ this.log({
8942
+ level: "warn",
8943
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
8944
+ });
8945
+ continue;
8946
+ }
8947
+ messages = snapshot;
8948
+ } else {
8949
+ const polled = await pollSessionMessagesForRedrive(
8950
+ this.port,
8951
+ sessionId,
8952
+ this.openCodeClient
8953
+ );
8954
+ if (!polled.ok) {
8955
+ const normalized = normalizeRedrivePollFailureBody(polled.body);
8956
+ this.log({
8957
+ level: "warn",
8958
+ message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned ${polled.malformed ? "a non-array message body" : `HTTP ${polled.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 skipping this session this tick`
8959
+ });
8960
+ continue;
8961
+ }
8962
+ messages = polled.messages;
8024
8963
  }
8025
- messages = body;
8026
8964
  } catch (err) {
8027
8965
  this.log({
8028
8966
  level: "warn",
@@ -8031,7 +8969,7 @@ var ChannelDriver = class _ChannelDriver {
8031
8969
  continue;
8032
8970
  }
8033
8971
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
8034
- const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
8972
+ const sessionOngoing = anyUntracked ? await this.isSessionOngoing(sessionId) : null;
8035
8973
  for (const row of sessionRows) {
8036
8974
  await this.readoptOne(sessionId, row, messages, sessionOngoing);
8037
8975
  }
@@ -8078,7 +9016,7 @@ var ChannelDriver = class _ChannelDriver {
8078
9016
  if (restartAborted) {
8079
9017
  this.log({
8080
9018
  level: "info",
8081
- message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
9019
+ message: `Re-adopt: message ${row.id.slice(0, 8)} carries an abort-shaped terminal error and session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check \u2014 restart orphan, re-dispatching instead of marking it permanently failed`,
8082
9020
  conversation_id: row.conversation_id,
8083
9021
  message_id: row.id
8084
9022
  });
@@ -8133,10 +9071,11 @@ var ChannelDriver = class _ChannelDriver {
8133
9071
  }
8134
9072
  await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
8135
9073
  this.dontRedispatch.delete(row.id);
9074
+ this.untrackableAck.delete(row.id);
8136
9075
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
8137
9076
  return;
8138
9077
  }
8139
- if (this.dontRedispatch.has(row.id)) {
9078
+ if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
8140
9079
  this.log({
8141
9080
  level: "debug",
8142
9081
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
@@ -8156,7 +9095,7 @@ var ChannelDriver = class _ChannelDriver {
8156
9095
  const finish = reply?.info?.finish ?? reply?.finish;
8157
9096
  this.log({
8158
9097
  level: "info",
8159
- message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per GET /session/status \u2014 delivering the existing reply instead of re-dispatching`,
9098
+ message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per the active-session status check \u2014 delivering the existing reply instead of re-dispatching`,
8160
9099
  conversation_id: row.conversation_id,
8161
9100
  message_id: row.id
8162
9101
  });
@@ -8165,7 +9104,7 @@ var ChannelDriver = class _ChannelDriver {
8165
9104
  }
8166
9105
  this.log({
8167
9106
  level: "info",
8168
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
9107
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per the active-session status check (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
8169
9108
  conversation_id: row.conversation_id,
8170
9109
  message_id: row.id
8171
9110
  });
@@ -8175,7 +9114,7 @@ var ChannelDriver = class _ChannelDriver {
8175
9114
  if (ongoing === true) {
8176
9115
  this.log({
8177
9116
  level: "debug",
8178
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per GET /session/status (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
9117
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per the active-session status check (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
8179
9118
  conversation_id: row.conversation_id,
8180
9119
  message_id: row.id
8181
9120
  });
@@ -8183,7 +9122,7 @@ var ChannelDriver = class _ChannelDriver {
8183
9122
  if (shape === "b1") {
8184
9123
  this.log({
8185
9124
  level: "debug",
8186
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
9125
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
8187
9126
  conversation_id: row.conversation_id,
8188
9127
  message_id: row.id
8189
9128
  });
@@ -8195,7 +9134,7 @@ var ChannelDriver = class _ChannelDriver {
8195
9134
  }
8196
9135
  this.log({
8197
9136
  level: "debug",
8198
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
9137
+ message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but the active-session status check was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
8199
9138
  conversation_id: row.conversation_id,
8200
9139
  message_id: row.id
8201
9140
  });
@@ -8245,7 +9184,7 @@ var ChannelDriver = class _ChannelDriver {
8245
9184
  * extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
8246
9185
  * SAME delivery instead of duplicating it.
8247
9186
  *
8248
- * EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops
9187
+ * EVEN IF the row was previously parked by either recovery fence (a give-up stops
8249
9188
  * re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
8250
9189
  * `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
8251
9190
  * leave for cron; transient → log + leave for the next drain (the still-
@@ -8312,6 +9251,7 @@ var ChannelDriver = class _ChannelDriver {
8312
9251
  await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
8313
9252
  }
8314
9253
  this.dontRedispatch.delete(row.id);
9254
+ this.untrackableAck.delete(row.id);
8315
9255
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
8316
9256
  }
8317
9257
  /**
@@ -8375,19 +9315,90 @@ var ChannelDriver = class _ChannelDriver {
8375
9315
  conversation_id: row.conversation_id,
8376
9316
  message_id: row.id
8377
9317
  });
8378
- this.awaitingReadopt.add(row.id);
9318
+ if (!this.isV2) this.awaitingReadopt.add(row.id);
8379
9319
  const readoptConv = this.convForRow(sessionId, row);
8380
9320
  const readoptMessage = this.queuedMessageForRow(row);
8381
- const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
9321
+ const sendAttachments = this.isV2 ? void 0 : this.buildSendAttachments(readoptConv, readoptMessage);
9322
+ if (this.isV2 && row.attachments && row.attachments.length > 0) {
9323
+ this.signalAttachmentsSkipped(
9324
+ row.conversation_id,
9325
+ row.id,
9326
+ row.attachments.map((attachment, index) => ({
9327
+ index,
9328
+ mime: attachment.mime,
9329
+ ...attachment.filename ? { filename: attachment.filename } : {},
9330
+ status: "skipped"
9331
+ })),
9332
+ false
9333
+ );
9334
+ }
8382
9335
  let ocId;
8383
9336
  try {
8384
- ocId = await this.dispatchLocked(
9337
+ ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
8385
9338
  sessionId,
8386
- () => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
9339
+ () => sendPromptAsync(
9340
+ this.port,
9341
+ sessionId,
9342
+ row.content,
9343
+ options,
9344
+ sendAttachments,
9345
+ this.openCodeClient
9346
+ )
8387
9347
  );
8388
9348
  } catch (err) {
8389
9349
  this.awaitingReadopt.delete(row.id);
8390
9350
  if (err instanceof ChannelAuthError) throw err;
9351
+ if (this.isV2) {
9352
+ const errorMessage3 = err instanceof Error ? err.message : String(err);
9353
+ const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
9354
+ if (!invalidPromptAck) {
9355
+ const exists = await this.sessionExists(sessionId);
9356
+ if (exists === false) {
9357
+ this.log({
9358
+ level: "warn",
9359
+ message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage3}`,
9360
+ conversation_id: row.conversation_id,
9361
+ message_id: row.id
9362
+ });
9363
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9364
+ return;
9365
+ }
9366
+ if (exists === null) {
9367
+ this.log({
9368
+ level: "warn",
9369
+ message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage3}`,
9370
+ conversation_id: row.conversation_id,
9371
+ message_id: row.id
9372
+ });
9373
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
9374
+ return;
9375
+ }
9376
+ } else {
9377
+ this.untrackableAck.add(row.id);
9378
+ }
9379
+ this.log({
9380
+ level: "error",
9381
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
9382
+ conversation_id: row.conversation_id,
9383
+ message_id: row.id
9384
+ });
9385
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
9386
+ (markErr) => {
9387
+ this.log({
9388
+ level: "warn",
9389
+ message: `markFailed PATCH for V2 re-adopt dispatch failure on message ${row.id.slice(0, 8)} failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9390
+ conversation_id: row.conversation_id,
9391
+ message_id: row.id
9392
+ });
9393
+ if (invalidPromptAck) {
9394
+ void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9395
+ } else {
9396
+ this.signalDispatchNotStarted(readoptConv, readoptMessage, "failure_unreported");
9397
+ }
9398
+ }
9399
+ );
9400
+ return;
9401
+ }
8391
9402
  this.log({
8392
9403
  level: "warn",
8393
9404
  message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
@@ -8519,12 +9530,13 @@ var ChannelDriver = class _ChannelDriver {
8519
9530
  this.dispatched.delete(evidentMessageId);
8520
9531
  }
8521
9532
  /**
8522
- * Poll `/question` + `/permission` (scoped to the session) and surface NEW ones
8523
- * via `reportInteraction` (Task 3.5), carrying the PAUSED message's own
8524
- * `source_message_id` so the server @mentions the correct person under
8525
- * concurrency. Dedups by interaction id across ticks (reused per-session sets).
9533
+ * Poll V1's global `/question` + `/permission`, or V2's watched-session form and
9534
+ * permission routes, and surface NEW ones via `reportInteraction` (Task 3.5),
9535
+ * carrying the PAUSED message's own `source_message_id` so the server @mentions
9536
+ * the correct person under concurrency. Dedups by interaction id across ticks
9537
+ * (reused per-session sets).
8526
9538
  *
8527
- * The interaction is attributed to the in-flight message it paused on. opencode
9539
+ * The interaction is attributed to the in-flight message it paused on. OpenCode
8528
9540
  * stamps a `messageID` on a permission (and `tool.messageID` on a question) =
8529
9541
  * the assistant message id, whose `parentID` is the user message id — but the
8530
9542
  * simplest robust attribution here is: the single in-flight message that is
@@ -8547,16 +9559,14 @@ var ChannelDriver = class _ChannelDriver {
8547
9559
  let permissionsPolledOk = true;
8548
9560
  let questions = [];
8549
9561
  try {
8550
- const res = await this.fetchImpl(`${this.opencodeBase}/question`);
8551
- if (res.ok) {
8552
- const body = await res.json();
8553
- if (Array.isArray(body)) {
8554
- questions = body;
8555
- } else {
8556
- questionsPolledOk = false;
8557
- }
9562
+ if (this.isV2) {
9563
+ const forms = await listV2Forms(this.openCodeClient, sessionId);
9564
+ if (forms === null) questionsPolledOk = false;
9565
+ else questions = forms;
8558
9566
  } else {
8559
- questionsPolledOk = false;
9567
+ const listed = await listOpenCodeQuestions(this.port, this.openCodeClient);
9568
+ if (listed === null) questionsPolledOk = false;
9569
+ else questions = listed;
8560
9570
  }
8561
9571
  } catch {
8562
9572
  questionsPolledOk = false;
@@ -8576,16 +9586,14 @@ var ChannelDriver = class _ChannelDriver {
8576
9586
  }
8577
9587
  let permissions = [];
8578
9588
  try {
8579
- const res = await this.fetchImpl(`${this.opencodeBase}/permission`);
8580
- if (res.ok) {
8581
- const body = await res.json();
8582
- if (Array.isArray(body)) {
8583
- permissions = body;
8584
- } else {
8585
- permissionsPolledOk = false;
8586
- }
9589
+ if (this.isV2) {
9590
+ const listed = await listV2Permissions(this.openCodeClient, sessionId);
9591
+ if (listed === null) permissionsPolledOk = false;
9592
+ else permissions = listed;
8587
9593
  } else {
8588
- permissionsPolledOk = false;
9594
+ const listed = await listOpenCodePermissions(this.port, this.openCodeClient);
9595
+ if (listed === null) permissionsPolledOk = false;
9596
+ else permissions = listed;
8589
9597
  }
8590
9598
  } catch {
8591
9599
  permissionsPolledOk = false;
@@ -8679,10 +9687,14 @@ var ChannelDriver = class _ChannelDriver {
8679
9687
  if (cached !== void 0) return cached;
8680
9688
  let parent = void 0;
8681
9689
  try {
8682
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8683
- if (res.ok) {
8684
- const body = await res.json();
8685
- parent = body && typeof body.parentID === "string" ? body.parentID : null;
9690
+ if (this.isV2) {
9691
+ const session = await getV2Session(this.openCodeClient, sessionId);
9692
+ parent = null;
9693
+ const candidate = session.parentID;
9694
+ if (typeof candidate === "string") parent = candidate;
9695
+ } else {
9696
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9697
+ parent = typeof body?.parentID === "string" ? body.parentID : body === null ? void 0 : null;
8686
9698
  }
8687
9699
  } catch {
8688
9700
  parent = void 0;
@@ -8736,18 +9748,16 @@ var ChannelDriver = class _ChannelDriver {
8736
9748
  if (cached) return cached;
8737
9749
  const pending = (async () => {
8738
9750
  try {
8739
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
8740
- if (!res.ok) {
9751
+ const messages2 = await this.getTelemetrySubagentSessionMessages(sessionId);
9752
+ if (messages2 === null) {
8741
9753
  this.log({
8742
9754
  level: "warn",
8743
- message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 omitting invocation telemetry`,
9755
+ message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was unreadable \u2014 omitting invocation telemetry`,
8744
9756
  message_id: messageId
8745
9757
  });
8746
9758
  return null;
8747
9759
  }
8748
- const body = await res.json();
8749
- if (!Array.isArray(body)) throw new Error("response body was not a message array");
8750
- return body;
9760
+ return messages2;
8751
9761
  } catch (err) {
8752
9762
  this.log({
8753
9763
  level: "warn",
@@ -8888,21 +9898,18 @@ var ChannelDriver = class _ChannelDriver {
8888
9898
  const cached = this.sessionTitles.get(sessionId);
8889
9899
  if (cached != null) return cached;
8890
9900
  try {
8891
- const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
8892
- if (res.ok) {
8893
- const body = await res.json();
8894
- const title = body && typeof body.title === "string" ? body.title.trim() : "";
8895
- if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
8896
- this.sessionTitles.set(sessionId, title);
8897
- return title;
8898
- }
8899
- return null;
9901
+ let title = "";
9902
+ if (this.isV2) {
9903
+ title = (await getV2Session(this.openCodeClient, sessionId)).title?.trim() ?? "";
9904
+ } else {
9905
+ const body = await getOpenCodeSession(this.port, sessionId, this.openCodeClient);
9906
+ title = typeof body?.title === "string" ? body.title.trim() : "";
8900
9907
  }
8901
- this.log({
8902
- level: "debug",
8903
- message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
8904
- conversation_id: conversationId
8905
- });
9908
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
9909
+ this.sessionTitles.set(sessionId, title);
9910
+ return title;
9911
+ }
9912
+ return null;
8906
9913
  } catch (err) {
8907
9914
  this.log({
8908
9915
  level: "debug",
@@ -9000,7 +10007,7 @@ var ChannelDriver = class _ChannelDriver {
9000
10007
  * `SessionStatus` only.
9001
10008
  */
9002
10009
  async isAnyDescendantSessionAlive(rootSessionId) {
9003
- const sessions = await listSessions(this.port);
10010
+ const sessions = await this.listSessions();
9004
10011
  if (!sessions) {
9005
10012
  this.log({
9006
10013
  level: "warn",
@@ -9011,7 +10018,7 @@ var ChannelDriver = class _ChannelDriver {
9011
10018
  for (const candidate of sessions) {
9012
10019
  if (!candidate?.id || candidate.id === rootSessionId) continue;
9013
10020
  if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
9014
- const childMsgs = await getSessionMessages(this.port, candidate.id);
10021
+ const childMsgs = await this.getSubagentSessionMessages(candidate.id);
9015
10022
  if (isSessionActivelyGenerating(childMsgs)) {
9016
10023
  return true;
9017
10024
  }
@@ -9073,7 +10080,7 @@ var ChannelDriver = class _ChannelDriver {
9073
10080
  * `isB2AbandonmentConfirmed`.
9074
10081
  */
9075
10082
  async isAnyDescendantSessionOngoing(rootSessionId) {
9076
- const sessions = await listSessions(this.port);
10083
+ const sessions = await this.listSessions();
9077
10084
  if (!sessions) {
9078
10085
  this.log({
9079
10086
  level: "warn",
@@ -9090,7 +10097,7 @@ var ChannelDriver = class _ChannelDriver {
9090
10097
  continue;
9091
10098
  }
9092
10099
  if (membership === false) continue;
9093
- const ongoing = await isSessionOngoing(this.port, candidate.id);
10100
+ const ongoing = await this.isSessionOngoing(candidate.id);
9094
10101
  if (ongoing === true) return true;
9095
10102
  if (ongoing === null) indeterminate = true;
9096
10103
  }
@@ -9430,7 +10437,7 @@ var ChannelDriver = class _ChannelDriver {
9430
10437
  const classified = messageFailure(messages, userMessageId);
9431
10438
  if (classified != null) return classified;
9432
10439
  const reply = findLastAssistantReplyFor(messages, userMessageId);
9433
- const hasProvider = await hasAnyConfiguredProvider(this.port);
10440
+ const hasProvider = await this.hasAnyConfiguredProvider();
9434
10441
  return applyZeroProviderFallback(
9435
10442
  classified,
9436
10443
  hasProvider,
@@ -9503,7 +10510,7 @@ var ChannelDriver = class _ChannelDriver {
9503
10510
  const succeededProviders = /* @__PURE__ */ new Set();
9504
10511
  for (const ref of refs) {
9505
10512
  try {
9506
- const childMessages = await getSessionMessages(this.port, ref.sessionId);
10513
+ const childMessages = await this.getSubagentSessionMessages(ref.sessionId);
9507
10514
  if (childMessages === null) {
9508
10515
  this.log({
9509
10516
  level: "debug",
@@ -9834,6 +10841,7 @@ Port ${port} is already in use.`));
9834
10841
 
9835
10842
  // src/commands/ensure-opencode-v2.ts
9836
10843
  import chalk6 from "chalk";
10844
+ import ora3 from "ora";
9837
10845
  import { select as select3 } from "@inquirer/prompts";
9838
10846
  async function probeOpenCode2WithoutPassword(port) {
9839
10847
  try {
@@ -9859,11 +10867,7 @@ function unknownPasswordError(port) {
9859
10867
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
9860
10868
  );
9861
10869
  }
9862
- function v2SessionSupportIncompleteError() {
9863
- return new Error(
9864
- "OpenCode V2 (opencode2) session support is incomplete \u2014 see https://github.com/sroze/evident/issues/2075. Not starting a new opencode2 process."
9865
- );
9866
- }
10870
+ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
9867
10871
  async function ensureOpenCode2Running(ctx) {
9868
10872
  const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
9869
10873
  if (initialHealth.authFailed) {
@@ -9906,13 +10910,37 @@ Port ${port} is already in use.`));
9906
10910
  }
9907
10911
  }
9908
10912
  if (!ctx.interactive) {
9909
- throw v2SessionSupportIncompleteError();
10913
+ ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
10914
+ const { child: proc, password } = await startOpenCode2(port, {
10915
+ inheritStdio: ctx.inheritStdio
10916
+ });
10917
+ const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
10918
+ if (!health.healthy) {
10919
+ return {
10920
+ port,
10921
+ process: proc,
10922
+ version: null,
10923
+ notReadyReason: `it did not become healthy within ${Math.round(ctx.startTimeoutMs / 1e3)}s`,
10924
+ password
10925
+ };
10926
+ }
10927
+ ctx.log(`OpenCode V2 started on port ${port}${health.version ? ` (v${health.version})` : ""}`);
10928
+ return {
10929
+ port,
10930
+ process: proc,
10931
+ version: health.version ?? null,
10932
+ notReadyReason: null,
10933
+ password
10934
+ };
9910
10935
  }
9911
- console.log(chalk6.yellow(`
9912
- ${v2SessionSupportIncompleteError().message}`));
9913
10936
  const action = await select3({
9914
10937
  message: "OpenCode V2 is not running. What would you like to do?",
9915
10938
  choices: [
10939
+ {
10940
+ name: "Start OpenCode V2 for me",
10941
+ value: "start",
10942
+ description: `Run 'opencode2 serve --port ${port}'`
10943
+ },
9916
10944
  {
9917
10945
  name: "Show me the command",
9918
10946
  value: "manual",
@@ -9933,6 +10961,25 @@ ${v2SessionSupportIncompleteError().message}`));
9933
10961
  blank();
9934
10962
  throw new Error("Please start OpenCode V2 manually");
9935
10963
  }
10964
+ if (action === "start") {
10965
+ const spinner = ora3("Starting OpenCode V2...").start();
10966
+ const { child: proc, password } = await startOpenCode2(port, {
10967
+ inheritStdio: ctx.inheritStdio
10968
+ });
10969
+ const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
10970
+ if (!health.healthy) {
10971
+ spinner.fail("Failed to start OpenCode V2");
10972
+ throw new Error("OpenCode V2 failed to start");
10973
+ }
10974
+ spinner.stop();
10975
+ return {
10976
+ port,
10977
+ process: proc,
10978
+ version: health.version ?? null,
10979
+ notReadyReason: null,
10980
+ password
10981
+ };
10982
+ }
9936
10983
  return {
9937
10984
  port,
9938
10985
  process: null,
@@ -10826,7 +11873,7 @@ async function driveChannels(state, driver) {
10826
11873
  lastSeenOpencodeAuthApplies = opencodeAuthApplies;
10827
11874
  if (opencodeAuthApplied) state.openaiUsageRearm?.();
10828
11875
  if (claudeCredentialApplied || opencodeAuthApplied) {
10829
- void reloadProviderCache(state.port).catch(
11876
+ void reloadProviderCache(state.port, state.opencodeClient ?? void 0).catch(
10830
11877
  (error2) => logActivity(state, {
10831
11878
  type: "error",
10832
11879
  error: `OpenCode provider cache reload failed on port ${state.port}: ${error2 instanceof Error ? error2.message : String(error2)}`
@@ -10951,7 +11998,7 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
10951
11998
  async function runSweep(state, driver, config) {
10952
11999
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
10953
12000
  try {
10954
- const sessions = await listSessions(state.port);
12001
+ const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
10955
12002
  if (sessions === null) {
10956
12003
  logActivity(state, {
10957
12004
  type: "info",
@@ -10981,7 +12028,7 @@ async function runSweep(state, driver, config) {
10981
12028
  });
10982
12029
  continue;
10983
12030
  }
10984
- if (await deleteSession(state.port, id)) deleted++;
12031
+ if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
10985
12032
  else failed++;
10986
12033
  }
10987
12034
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
@@ -11488,6 +12535,9 @@ async function run(options) {
11488
12535
  connected: false,
11489
12536
  opencodeConnected: false,
11490
12537
  opencodeVersion: null,
12538
+ opencodeApiVersion: "v1",
12539
+ opencodePassword: null,
12540
+ opencodeClient: null,
11491
12541
  sessionDbProvenanceAnomaly: false,
11492
12542
  opencodeProcess: null,
11493
12543
  stopOpenCodeLogTail: null,
@@ -11642,7 +12692,7 @@ async function run(options) {
11642
12692
  console.log(chalk7.bold("Evident Run"));
11643
12693
  console.log(chalk7.dim("-".repeat(40)));
11644
12694
  }
11645
- const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
12695
+ const spinner = interactive && !state.json ? ora4("Validating runner...").start() : null;
11646
12696
  let validation = await getAgentInfo(state.agentId, state.authHeader);
11647
12697
  if (!validation.valid && validation.authFailed && interactive) {
11648
12698
  spinner?.fail("Authentication failed");
@@ -11763,7 +12813,7 @@ async function run(options) {
11763
12813
  logActivity(state, { type: "info", level: "warn", message: warning2 });
11764
12814
  }
11765
12815
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
11766
- const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
12816
+ const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
11767
12817
  try {
11768
12818
  const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
11769
12819
  port: state.port,
@@ -11783,6 +12833,19 @@ async function run(options) {
11783
12833
  state.port = oc.port;
11784
12834
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
11785
12835
  state.opencodeVersion = oc.version;
12836
+ state.opencodeApiVersion = opencodeVersion;
12837
+ let opencodePassword = null;
12838
+ if (opencodeVersion === "v2" && "password" in oc) {
12839
+ const value = oc.password;
12840
+ if (typeof value === "string" || value === null) opencodePassword = value;
12841
+ }
12842
+ state.opencodePassword = opencodePassword;
12843
+ const openCodeClient = createOpenCodeClient({
12844
+ port: state.port,
12845
+ version: state.opencodeApiVersion,
12846
+ password: state.opencodePassword
12847
+ });
12848
+ state.opencodeClient = openCodeClient;
11786
12849
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
11787
12850
  try {
11788
12851
  writeFileSync6(options.opencodePidFile, `${oc.process.pid}
@@ -11819,16 +12882,19 @@ async function run(options) {
11819
12882
  const message = `OpenCode is not ready on port ${state.port}: ${oc.notReadyReason}. The runner will still come online, but messages will fail until opencode answers \u2014 raise the wait with --opencode-start-timeout <seconds> (env ${OPENCODE_START_TIMEOUT_ENV}).`;
11820
12883
  logActivity(state, { type: "info", level: "warn", message });
11821
12884
  } else {
11822
- const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
12885
+ const versionWarning = buildOpenCodeVersionWarning(
12886
+ state.opencodeVersion,
12887
+ state.opencodeApiVersion
12888
+ );
11823
12889
  if (versionWarning) {
11824
12890
  log2(state, versionWarning, "warn");
11825
12891
  if (state.interactive && !state.json) {
11826
12892
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
11827
12893
  }
11828
12894
  }
11829
- await reloadProviderCache(state.port);
12895
+ await reloadProviderCache(state.port, state.opencodeClient ?? void 0);
11830
12896
  const noProviderWarning = buildNoProviderWarning(
11831
- await hasAnyConfiguredProvider(state.port)
12897
+ await hasAnyConfiguredProvider(state.port, state.opencodeClient ?? void 0)
11832
12898
  );
11833
12899
  if (noProviderWarning) {
11834
12900
  log2(state, noProviderWarning, "warn");
@@ -11951,11 +13017,12 @@ async function run(options) {
11951
13017
  });
11952
13018
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
11953
13019
  }
11954
- const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
13020
+ const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
11955
13021
  const channelDriver = new ChannelDriver({
11956
13022
  agentId: state.agentId,
11957
13023
  port: state.port,
11958
13024
  apiUrl: getApiUrlConfig(),
13025
+ openCodeClient: state.opencodeClient ?? void 0,
11959
13026
  getAuthHeader: () => state.authHeader,
11960
13027
  conversationFilter: state.conversationFilter,
11961
13028
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
@@ -11981,6 +13048,7 @@ async function run(options) {
11981
13048
  agentId: state.agentId,
11982
13049
  getAuthHeader: () => state.authHeader,
11983
13050
  port: state.port,
13051
+ openCodePassword: state.opencodePassword,
11984
13052
  isRunning: () => state.running,
11985
13053
  events: {
11986
13054
  onConnected: (agentId, isReconnect) => {
@@ -12005,7 +13073,11 @@ async function run(options) {
12005
13073
  emitAgentConnected(state.agentId, {
12006
13074
  port: state.port,
12007
13075
  cli_version: getCliVersion(),
12008
- opencode_version: state.opencodeVersion
13076
+ opencode_version: reportedOpenCodeVersion({
13077
+ version: state.opencodeVersion,
13078
+ major: state.opencodeApiVersion,
13079
+ connected: state.opencodeConnected
13080
+ })
12009
13081
  });
12010
13082
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
12011
13083
  if (state.interactive) displayStatus(state);
@@ -12121,7 +13193,7 @@ async function run(options) {
12121
13193
  state.openaiUsageTimer = timer;
12122
13194
  },
12123
13195
  fetchUsage: async () => {
12124
- const usage = await getOpenAiUsage(state.port);
13196
+ const usage = await getOpenAiUsage(state.port, state.opencodeClient ?? void 0);
12125
13197
  if (usage.subscription === null) {
12126
13198
  logActivity(state, {
12127
13199
  type: "info",