@evident-ai/cli 3.6.1-dev.10d71b3 → 3.6.1-dev.2106dfe

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
@@ -1179,9 +1179,9 @@ async function claudeUsage() {
1179
1179
  }
1180
1180
 
1181
1181
  // src/commands/run.ts
1182
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
1182
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
1183
1183
  import { homedir as homedir8 } from "node:os";
1184
- import { isAbsolute as isAbsolute4, join as join14, parse, resolve as resolvePath2 } from "node:path";
1184
+ import { isAbsolute as isAbsolute4, join as join16, parse, resolve as resolvePath2 } from "node:path";
1185
1185
  import chalk7 from "chalk";
1186
1186
 
1187
1187
  // ../../packages/types/src/agents/index.ts
@@ -1445,7 +1445,7 @@ import ora4 from "ora";
1445
1445
  import { select as select4 } from "@inquirer/prompts";
1446
1446
 
1447
1447
  // src/lib/telemetry.ts
1448
- var CLI_VERSION = (true ? "3.6.1-dev.10d71b3" : void 0) ?? process.env.npm_package_version ?? "unknown";
1448
+ var CLI_VERSION = (true ? "3.6.1-dev.2106dfe" : void 0) ?? process.env.npm_package_version ?? "unknown";
1449
1449
  function getCliVersion() {
1450
1450
  return CLI_VERSION;
1451
1451
  }
@@ -2195,6 +2195,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
2195
2195
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
2196
2196
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
2197
2197
  "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2198
+ "cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2198
2199
  "cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
2199
2200
  };
2200
2201
  var SEVERITY_BY_LEVEL = {
@@ -2757,6 +2758,943 @@ function boundedError(error2) {
2757
2758
  return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
2758
2759
  }
2759
2760
 
2761
+ // src/lib/opencode/session-binding-ledger.ts
2762
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2763
+ import { dirname as dirname5, join as join5 } from "node:path";
2764
+ var SESSION_BINDING_LEDGER_MAX_RECORDS = 1e3;
2765
+ var MAX_DIAGNOSTIC_LENGTH2 = 200;
2766
+ function sessionBindingLedgerPath(homeDir, env) {
2767
+ const override = env.EVIDENT_SESSION_BINDING_LEDGER?.trim();
2768
+ return override || join5(homeDir, ".local", "state", "evident", "session-bindings.jsonl");
2769
+ }
2770
+ function appendSessionBinding(path, record, log3) {
2771
+ mkdirSync2(dirname5(path), { recursive: true });
2772
+ const current = readSessionBindingRecords(path, log3);
2773
+ if (current.readError !== null) {
2774
+ throw new Error(`cannot enforce the ledger limit after reading ${path}: ${current.readError}`);
2775
+ }
2776
+ if (current.records.some(
2777
+ (existing) => existing.conversation_id === record.conversation_id && existing.opencode_session_id === record.opencode_session_id
2778
+ )) {
2779
+ return;
2780
+ }
2781
+ const kept = [...current.records, record].slice(-SESSION_BINDING_LEDGER_MAX_RECORDS);
2782
+ if (kept.length > current.records.length) {
2783
+ appendFileSync2(path, `${JSON.stringify(record)}
2784
+ `, "utf8");
2785
+ } else {
2786
+ writeFileSync2(path, `${kept.map((entry) => JSON.stringify(entry)).join("\n")}
2787
+ `, "utf8");
2788
+ }
2789
+ }
2790
+ function readSessionBindings(path, log3) {
2791
+ const result = readSessionBindingRecords(path, log3);
2792
+ if (result.readError !== null) {
2793
+ log3(`[session-binding-ledger] could not read ${path}: ${result.readError}`);
2794
+ }
2795
+ return new Map(
2796
+ result.records.map((record) => [record.opencode_session_id, record.conversation_id])
2797
+ );
2798
+ }
2799
+ function readSessionBindingRecords(path, log3) {
2800
+ let content;
2801
+ try {
2802
+ content = readFileSync4(path, "utf8");
2803
+ } catch (error2) {
2804
+ if (isMissingFile2(error2)) return { records: [], readError: null };
2805
+ const readError = boundedError2(error2);
2806
+ return { records: [], readError };
2807
+ }
2808
+ const records = content.split("\n").flatMap((line) => {
2809
+ if (!line.trim()) return [];
2810
+ const record = parseRecord2(line, path, log3);
2811
+ return record ? [record] : [];
2812
+ });
2813
+ return { records, readError: null };
2814
+ }
2815
+ function parseRecord2(line, path, log3) {
2816
+ try {
2817
+ const value = JSON.parse(line);
2818
+ if (isSessionBindingRecord(value)) return value;
2819
+ log3(`[session-binding-ledger] skipped malformed record in ${path}: invalid shape`);
2820
+ } catch (error2) {
2821
+ log3(`[session-binding-ledger] skipped malformed record in ${path}: ${boundedError2(error2)}`);
2822
+ }
2823
+ return null;
2824
+ }
2825
+ function isSessionBindingRecord(value) {
2826
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2827
+ const record = value;
2828
+ return typeof record.conversation_id === "string" && record.conversation_id.length > 0 && typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.observed_at === "string";
2829
+ }
2830
+ function isMissingFile2(error2) {
2831
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
2832
+ }
2833
+ function boundedError2(error2) {
2834
+ const message = error2 instanceof Error ? error2.message : String(error2);
2835
+ return message.slice(0, MAX_DIAGNOSTIC_LENGTH2);
2836
+ }
2837
+
2838
+ // src/lib/opencode/session-archive-config.ts
2839
+ import { readFileSync as readFileSync5 } from "node:fs";
2840
+
2841
+ // src/lib/opencode/session-cleanup.ts
2842
+ var DURATION_UNIT_MS = {
2843
+ s: 1e3,
2844
+ m: 60 * 1e3,
2845
+ h: 60 * 60 * 1e3,
2846
+ d: 24 * 60 * 60 * 1e3
2847
+ };
2848
+ function parseDurationMs(input) {
2849
+ const trimmed = input.trim();
2850
+ const match = /^(\d+)([smhd])$/.exec(trimmed);
2851
+ if (!match) {
2852
+ throw new Error(
2853
+ `Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
2854
+ );
2855
+ }
2856
+ const value = Number(match[1]);
2857
+ if (value <= 0) {
2858
+ throw new Error(`Invalid duration "${input}": must be a positive value.`);
2859
+ }
2860
+ return value * DURATION_UNIT_MS[match[2]];
2861
+ }
2862
+ function selectSessionsToDelete(sessions, opts) {
2863
+ const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
2864
+ if (maxAgeMs === void 0 && maxCount === void 0) return [];
2865
+ const ageEligible = (s) => {
2866
+ if (maxAgeMs === void 0) return false;
2867
+ if (s.lastActivityMs === null) return true;
2868
+ return nowMs - s.lastActivityMs > maxAgeMs;
2869
+ };
2870
+ const countEligibleIds = /* @__PURE__ */ new Set();
2871
+ if (maxCount !== void 0) {
2872
+ const byActivityDesc = [...sessions].sort(
2873
+ (a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
2874
+ );
2875
+ for (const s of byActivityDesc.slice(maxCount)) {
2876
+ countEligibleIds.add(s.id);
2877
+ }
2878
+ }
2879
+ const toDelete = [];
2880
+ for (const s of sessions) {
2881
+ if (protectedIds.has(s.id)) continue;
2882
+ if (ageEligible(s) || countEligibleIds.has(s.id)) {
2883
+ toDelete.push(s.id);
2884
+ }
2885
+ }
2886
+ const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
2887
+ return toDelete.sort(
2888
+ (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
2889
+ );
2890
+ }
2891
+ var DEFAULT_INTERVAL = "1h";
2892
+ function resolve2(flag, envValue, fallback) {
2893
+ return flag ?? envValue ?? fallback;
2894
+ }
2895
+ function parseMaxCount(input) {
2896
+ const trimmed = input.trim();
2897
+ if (!/^\d+$/.test(trimmed)) {
2898
+ throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
2899
+ }
2900
+ const value = Number(trimmed);
2901
+ if (value <= 0) {
2902
+ throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
2903
+ }
2904
+ return value;
2905
+ }
2906
+ function resolveSessionCleanupConfig(flags, env = process.env) {
2907
+ const warnings = [];
2908
+ const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
2909
+ const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
2910
+ const intervalRaw = resolve2(
2911
+ flags.interval,
2912
+ env.EVIDENT_SESSION_CLEANUP_INTERVAL,
2913
+ DEFAULT_INTERVAL
2914
+ );
2915
+ let maxAgeMs;
2916
+ if (maxAgeRaw !== void 0) {
2917
+ try {
2918
+ maxAgeMs = parseDurationMs(maxAgeRaw);
2919
+ } catch (err) {
2920
+ warnings.push(
2921
+ `Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
2922
+ );
2923
+ }
2924
+ }
2925
+ let maxCount;
2926
+ if (maxCountRaw !== void 0) {
2927
+ try {
2928
+ maxCount = parseMaxCount(maxCountRaw);
2929
+ } catch (err) {
2930
+ warnings.push(
2931
+ `Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
2932
+ );
2933
+ }
2934
+ }
2935
+ let intervalMs;
2936
+ try {
2937
+ intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
2938
+ } catch (err) {
2939
+ warnings.push(
2940
+ `Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
2941
+ );
2942
+ intervalMs = parseDurationMs(DEFAULT_INTERVAL);
2943
+ }
2944
+ const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
2945
+ return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
2946
+ }
2947
+
2948
+ // src/lib/opencode/session-archive-config.ts
2949
+ var DEFAULT_INTERVAL2 = "1h";
2950
+ var DEFAULT_FLUSH_BUDGET_MS = 2e3;
2951
+ var MAX_SESSION_ARCHIVE_TIMER_DELAY_MS = 2147483647;
2952
+ var STATE_PREFIX_FILE = "/dev/shm/evident-state-prefix";
2953
+ var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
2954
+ var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
2955
+ function nonEmpty(value) {
2956
+ const trimmed = value?.trim();
2957
+ return trimmed ? trimmed : null;
2958
+ }
2959
+ function parseFlushBudgetMs(value) {
2960
+ const trimmed = value.trim();
2961
+ const budgetMs = Number(trimmed);
2962
+ if (!/^\d+$/.test(trimmed) || !Number.isSafeInteger(budgetMs) || budgetMs <= 0 || budgetMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
2963
+ throw new Error(
2964
+ `Invalid flush budget "${value}": expected a positive integer number of milliseconds no greater than ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}.`
2965
+ );
2966
+ }
2967
+ return budgetMs;
2968
+ }
2969
+ function resolveSessionArchiveConfig(flags, env = process.env) {
2970
+ const warnings = [];
2971
+ const archiveRaw = env.EVIDENT_SESSION_ARCHIVE;
2972
+ let enabled = flags.enabled ?? false;
2973
+ if (flags.enabled === void 0 && archiveRaw !== void 0 && archiveRaw.trim() !== "") {
2974
+ const normalized = archiveRaw.trim().toLowerCase();
2975
+ if (ENABLED_VALUES.has(normalized)) {
2976
+ enabled = true;
2977
+ } else if (DISABLED_VALUES.has(normalized)) {
2978
+ enabled = false;
2979
+ } else {
2980
+ warnings.push(
2981
+ `Ignoring invalid EVIDENT_SESSION_ARCHIVE "${archiveRaw}": expected on or off; session archiving remains off.`
2982
+ );
2983
+ }
2984
+ }
2985
+ let intervalMs;
2986
+ const intervalRaw = flags.interval ?? env.EVIDENT_SESSION_ARCHIVE_INTERVAL ?? DEFAULT_INTERVAL2;
2987
+ try {
2988
+ intervalMs = parseDurationMs(intervalRaw);
2989
+ if (intervalMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
2990
+ throw new Error(
2991
+ `Duration "${intervalRaw}" exceeds Node's maximum timer delay of ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}ms.`
2992
+ );
2993
+ }
2994
+ } catch (err) {
2995
+ warnings.push(
2996
+ `Ignoring invalid session archive interval, using default ${DEFAULT_INTERVAL2}: ${err instanceof Error ? err.message : String(err)}`
2997
+ );
2998
+ intervalMs = parseDurationMs(DEFAULT_INTERVAL2);
2999
+ }
3000
+ let flushBudgetMs;
3001
+ const flushBudgetRaw = env.EVIDENT_SESSION_ARCHIVE_FLUSH_MS ?? String(DEFAULT_FLUSH_BUDGET_MS);
3002
+ try {
3003
+ flushBudgetMs = parseFlushBudgetMs(flushBudgetRaw);
3004
+ } catch (err) {
3005
+ warnings.push(
3006
+ `Ignoring invalid EVIDENT_SESSION_ARCHIVE_FLUSH_MS, using default ${DEFAULT_FLUSH_BUDGET_MS}: ${err instanceof Error ? err.message : String(err)}`
3007
+ );
3008
+ flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
3009
+ }
3010
+ const bucket = nonEmpty(env.PERSISTENCE_BUCKET);
3011
+ let keyPrefix = nonEmpty(env.LITESTREAM_PREFIX);
3012
+ if (enabled && bucket === null) {
3013
+ warnings.push("Session archiving is off because PERSISTENCE_BUCKET is unset or empty.");
3014
+ enabled = false;
3015
+ }
3016
+ if (enabled && keyPrefix === null) {
3017
+ let fallbackReadError = null;
3018
+ try {
3019
+ keyPrefix = nonEmpty(readFileSync5(STATE_PREFIX_FILE, "utf8"));
3020
+ } catch (err) {
3021
+ fallbackReadError = err instanceof Error ? err.message : String(err);
3022
+ }
3023
+ if (keyPrefix === null) {
3024
+ const readFailure = fallbackReadError ? ` Could not read ${STATE_PREFIX_FILE}: ${fallbackReadError}.` : "";
3025
+ warnings.push(
3026
+ `Session archiving is off because neither LITESTREAM_PREFIX nor ${STATE_PREFIX_FILE} contains a prefix.${readFailure}`
3027
+ );
3028
+ enabled = false;
3029
+ }
3030
+ }
3031
+ return { enabled, bucket, keyPrefix, intervalMs, flushBudgetMs, warnings };
3032
+ }
3033
+
3034
+ // src/lib/opencode/session-archive-snapshot.ts
3035
+ import { createHash } from "node:crypto";
3036
+ var SessionArchiveExportError = class extends Error {
3037
+ constructor(code, message) {
3038
+ super(message);
3039
+ this.code = code;
3040
+ this.name = "SessionArchiveExportError";
3041
+ }
3042
+ };
3043
+ function canonicalize(value) {
3044
+ if (Array.isArray(value)) return value.map(canonicalize);
3045
+ if (typeof value !== "object" || value === null) return value;
3046
+ return Object.fromEntries(
3047
+ Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, canonicalize(nested)])
3048
+ );
3049
+ }
3050
+ function contentForFingerprint(archive) {
3051
+ return JSON.stringify(
3052
+ canonicalize({
3053
+ opencode_api_version: archive.opencode_api_version,
3054
+ opencode_session_id: archive.opencode_session_id,
3055
+ evident_conversation_id: archive.evident_conversation_id,
3056
+ messages: archive.messages
3057
+ })
3058
+ );
3059
+ }
3060
+ function fingerprintArchive(archive) {
3061
+ return createHash("sha256").update(contentForFingerprint(archive)).digest("hex");
3062
+ }
3063
+ function buildSessionArchive(input) {
3064
+ const archive = {
3065
+ schema_version: 1,
3066
+ opencode_api_version: input.apiVersion,
3067
+ opencode_session_id: input.session.id,
3068
+ evident_conversation_id: input.conversationId,
3069
+ evident_runner_id: input.runnerId,
3070
+ binding_source: input.bindingSource,
3071
+ session: input.session,
3072
+ messages: input.messages,
3073
+ message_count: input.messages.length,
3074
+ archived_at: input.archivedAt
3075
+ };
3076
+ return { ...archive, content_fingerprint: fingerprintArchive(archive) };
3077
+ }
3078
+ function isRecord(value) {
3079
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3080
+ }
3081
+ function errorMessage2(error2) {
3082
+ return error2 instanceof Error ? error2.message : String(error2);
3083
+ }
3084
+ function invalidResponse(version2, sessionId, path, reason) {
3085
+ return new SessionArchiveExportError(
3086
+ "invalid-response",
3087
+ `OpenCode ${version2} session ${sessionId} returned an invalid response from GET ${path}: ${reason}`
3088
+ );
3089
+ }
3090
+ async function requestPage(client, sessionId, path, signal) {
3091
+ let response;
3092
+ try {
3093
+ response = await client.request(path, signal ? { signal } : void 0, {
3094
+ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100)
3095
+ });
3096
+ } catch (error2) {
3097
+ throw new SessionArchiveExportError(
3098
+ "request-failed",
3099
+ `OpenCode ${client.version} session ${sessionId} request GET ${path} failed: ${errorMessage2(error2)}`
3100
+ );
3101
+ }
3102
+ if (response.status === 404) {
3103
+ throw new SessionArchiveExportError(
3104
+ "not-found",
3105
+ `OpenCode ${client.version} session ${sessionId} was not found at GET ${path} (HTTP 404)`
3106
+ );
3107
+ }
3108
+ if (!response.ok) {
3109
+ throw new SessionArchiveExportError(
3110
+ "request-failed",
3111
+ `OpenCode ${client.version} session ${sessionId} request GET ${path} returned HTTP ${response.status}`
3112
+ );
3113
+ }
3114
+ let bodyText;
3115
+ try {
3116
+ bodyText = await response.text();
3117
+ } catch (error2) {
3118
+ throw new SessionArchiveExportError(
3119
+ "request-failed",
3120
+ `OpenCode ${client.version} session ${sessionId} response body from GET ${path} could not be read: ${errorMessage2(error2)}`
3121
+ );
3122
+ }
3123
+ try {
3124
+ return { response, body: JSON.parse(bodyText) };
3125
+ } catch (error2) {
3126
+ if (!(error2 instanceof SyntaxError)) throw error2;
3127
+ const contentType = response.headers.get("content-type") ?? "";
3128
+ const looksLikeHtml = contentType.toLowerCase().includes("text/html") || /^\s*<!doctype\s+html|^\s*<html/i.test(bodyText);
3129
+ throw invalidResponse(
3130
+ client.version,
3131
+ sessionId,
3132
+ path,
3133
+ looksLikeHtml ? "received text/html, expected JSON" : "received non-JSON content, expected JSON"
3134
+ );
3135
+ }
3136
+ }
3137
+ function v1NextCursor(response, sessionId, path) {
3138
+ const headerCursor = response.headers.get("x-next-cursor")?.trim() || null;
3139
+ const linkHeader = response.headers.get("link");
3140
+ const nextLink = linkHeader?.split(",").map((link) => link.trim()).find((link) => /;\s*rel\s*=\s*"?next"?/i.test(link));
3141
+ let linkCursor = null;
3142
+ if (nextLink) {
3143
+ const target = nextLink.match(/<([^>]+)>/)?.[1];
3144
+ if (!target) {
3145
+ throw new SessionArchiveExportError(
3146
+ "pagination-failed",
3147
+ `OpenCode v1 session ${sessionId} returned a next Link without a URL for GET ${path}`
3148
+ );
3149
+ }
3150
+ try {
3151
+ linkCursor = new URL(target, "http://127.0.0.1").searchParams.get("before");
3152
+ } catch (error2) {
3153
+ throw new SessionArchiveExportError(
3154
+ "pagination-failed",
3155
+ `OpenCode v1 session ${sessionId} returned an invalid next Link for GET ${path}: ${errorMessage2(error2)}`
3156
+ );
3157
+ }
3158
+ }
3159
+ if (headerCursor && linkCursor && headerCursor !== linkCursor) {
3160
+ throw new SessionArchiveExportError(
3161
+ "pagination-failed",
3162
+ `OpenCode v1 session ${sessionId} returned conflicting next cursors for GET ${path}`
3163
+ );
3164
+ }
3165
+ const cursor = headerCursor ?? linkCursor;
3166
+ if ((headerCursor || nextLink) && !cursor) {
3167
+ throw new SessionArchiveExportError(
3168
+ "pagination-failed",
3169
+ `OpenCode v1 session ${sessionId} returned a next page without a usable cursor for GET ${path}`
3170
+ );
3171
+ }
3172
+ return cursor;
3173
+ }
3174
+ function v1Messages(body, sessionId, path) {
3175
+ if (!Array.isArray(body)) {
3176
+ throw invalidResponse("v1", sessionId, path, "expected an array of messages");
3177
+ }
3178
+ return body;
3179
+ }
3180
+ async function exportV1SessionMessages(client, sessionId, signal) {
3181
+ const basePath = `/session/${encodeURIComponent(sessionId)}/message`;
3182
+ const firstPage = await requestPage(client, sessionId, basePath, signal);
3183
+ let messages = v1Messages(firstPage.body, sessionId, basePath);
3184
+ let cursor = v1NextCursor(firstPage.response, sessionId, basePath);
3185
+ const seenCursors = /* @__PURE__ */ new Set();
3186
+ while (cursor !== null) {
3187
+ if (seenCursors.has(cursor)) {
3188
+ throw new SessionArchiveExportError(
3189
+ "pagination-failed",
3190
+ `OpenCode v1 session ${sessionId} repeated cursor while reading GET ${basePath}`
3191
+ );
3192
+ }
3193
+ seenCursors.add(cursor);
3194
+ const path = `${basePath}?${new URLSearchParams({ before: cursor }).toString()}`;
3195
+ const page = await requestPage(client, sessionId, path, signal);
3196
+ messages = [...v1Messages(page.body, sessionId, path), ...messages];
3197
+ cursor = v1NextCursor(page.response, sessionId, path);
3198
+ }
3199
+ return messages;
3200
+ }
3201
+ async function exportV2SessionMessages(client, sessionId, signal) {
3202
+ const basePath = `/api/session/${encodeURIComponent(sessionId)}/message`;
3203
+ const messages = [];
3204
+ const seenCursors = /* @__PURE__ */ new Set();
3205
+ let cursor;
3206
+ let hasNextPage = true;
3207
+ while (hasNextPage) {
3208
+ const params = new URLSearchParams({ limit: "100" });
3209
+ if (cursor === void 0) {
3210
+ params.set("order", "asc");
3211
+ } else {
3212
+ params.set("cursor", cursor);
3213
+ }
3214
+ const path = `${basePath}?${params.toString()}`;
3215
+ const { body } = await requestPage(client, sessionId, path, signal);
3216
+ if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) {
3217
+ throw invalidResponse("v2", sessionId, path, "expected a {data: [], cursor: {\u2026}} envelope");
3218
+ }
3219
+ messages.push(...body.data);
3220
+ const next = body.cursor.next;
3221
+ if (next === void 0 || next === null) {
3222
+ hasNextPage = false;
3223
+ continue;
3224
+ }
3225
+ if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
3226
+ throw new SessionArchiveExportError(
3227
+ "pagination-failed",
3228
+ `OpenCode v2 session ${sessionId} returned an invalid or repeated cursor from GET ${path}`
3229
+ );
3230
+ }
3231
+ seenCursors.add(next);
3232
+ cursor = next;
3233
+ }
3234
+ return messages;
3235
+ }
3236
+ async function exportSessionMessages(client, sessionId, signal) {
3237
+ if (client.version === "v1") return exportV1SessionMessages(client, sessionId, signal);
3238
+ if (client.version === "v2") return exportV2SessionMessages(client, sessionId, signal);
3239
+ throw new SessionArchiveExportError(
3240
+ "unsupported-dialect",
3241
+ `OpenCode session ${sessionId} cannot be archived: unsupported API dialect ${String(client.version)}`
3242
+ );
3243
+ }
3244
+
3245
+ // src/lib/opencode/session-archive-watermarks.ts
3246
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
3247
+ import { dirname as dirname6, join as join6 } from "node:path";
3248
+ function sessionArchiveWatermarkPath(homeDir, env) {
3249
+ const override = env.EVIDENT_SESSION_ARCHIVE_WATERMARK?.trim();
3250
+ return override || join6(homeDir, ".local", "state", "evident", "session-archive-watermarks.json");
3251
+ }
3252
+ function readWatermarks(path, log3) {
3253
+ let content;
3254
+ try {
3255
+ content = readFileSync6(path, "utf8");
3256
+ } catch (error2) {
3257
+ if (isMissingFile3(error2)) return /* @__PURE__ */ new Map();
3258
+ log3(`[session-archive-watermarks] could not read ${path}; treating every session as changed`);
3259
+ return /* @__PURE__ */ new Map();
3260
+ }
3261
+ try {
3262
+ const parsed = JSON.parse(content);
3263
+ if (!isRecord2(parsed)) throw new Error("invalid watermark object");
3264
+ const entries = Object.entries(parsed);
3265
+ if (!entries.every(([sessionId, value]) => sessionId.length > 0 && isWatermark(value))) {
3266
+ throw new Error("invalid watermark entry");
3267
+ }
3268
+ return new Map(entries);
3269
+ } catch (error2) {
3270
+ log3(`[session-archive-watermarks] could not parse ${path}; treating every session as changed`);
3271
+ return /* @__PURE__ */ new Map();
3272
+ }
3273
+ }
3274
+ function recordUpload(path, sessionId, entry, watermarks) {
3275
+ const updated = new Map(watermarks);
3276
+ updated.set(sessionId, entry);
3277
+ mkdirSync3(dirname6(path), { recursive: true });
3278
+ writeFileSync3(path, `${JSON.stringify(Object.fromEntries(updated))}
3279
+ `, {
3280
+ encoding: "utf8",
3281
+ mode: 384
3282
+ });
3283
+ watermarks.clear();
3284
+ for (const [id, watermark] of updated) watermarks.set(id, watermark);
3285
+ }
3286
+ function isRecord2(value) {
3287
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3288
+ }
3289
+ function isWatermark(value) {
3290
+ if (!isRecord2(value)) return false;
3291
+ return typeof value.fingerprint === "string" && typeof value.uploaded_at === "string" && typeof value.key === "string";
3292
+ }
3293
+ function isMissingFile3(error2) {
3294
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
3295
+ }
3296
+
3297
+ // src/lib/runner-synchroniser.ts
3298
+ import { spawn } from "node:child_process";
3299
+
3300
+ // ../../runner/synchroniser/src/shell-contract.json
3301
+ var shell_contract_default = {
3302
+ commands: [
3303
+ "env",
3304
+ "litestream-config",
3305
+ "restore",
3306
+ "sync-once",
3307
+ "model-auth-ready",
3308
+ "self-stop",
3309
+ "session-db-classify",
3310
+ "session-db-verify",
3311
+ "archive-put"
3312
+ ],
3313
+ archiveStdinMaxBytes: 16777216
3314
+ };
3315
+
3316
+ // src/lib/runner-synchroniser.ts
3317
+ var MAX_ARCHIVE_STDIN_BYTES = shell_contract_default.archiveStdinMaxBytes;
3318
+ var ERROR_CLOSE_FALLBACK_MS = 1e3;
3319
+ function isPipeClosed(error2) {
3320
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPIPE";
3321
+ }
3322
+ function appendError(stderr, error2) {
3323
+ const message = error2 instanceof Error ? error2.message : String(error2);
3324
+ return stderr === "" ? message : `${stderr}
3325
+ ${message}`;
3326
+ }
3327
+ function runSynchroniser(args, opts) {
3328
+ return new Promise((resolve5) => {
3329
+ let child;
3330
+ let stdout = "";
3331
+ let stderr = "";
3332
+ let settled = false;
3333
+ let shuttingDown = false;
3334
+ let timedOut = false;
3335
+ let stdinFailure;
3336
+ let childFailure;
3337
+ const timer = {};
3338
+ const closeFallbackTimer = {};
3339
+ let abortListener;
3340
+ let spawnListener;
3341
+ const finish = (result) => {
3342
+ if (settled) return;
3343
+ settled = true;
3344
+ if (timer.handle) clearTimeout(timer.handle);
3345
+ if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
3346
+ if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
3347
+ if (spawnListener) child.removeListener("spawn", spawnListener);
3348
+ resolve5(result);
3349
+ };
3350
+ const armErrorCloseFallback = () => {
3351
+ if (closeFallbackTimer.handle) return;
3352
+ closeFallbackTimer.handle = setTimeout(() => {
3353
+ const failure = stdinFailure ?? childFailure;
3354
+ finish({
3355
+ code: null,
3356
+ stdout,
3357
+ stderr: failure === void 0 ? stderr : appendError(stderr, failure),
3358
+ timedOut
3359
+ });
3360
+ }, ERROR_CLOSE_FALLBACK_MS);
3361
+ };
3362
+ const terminateChild = () => {
3363
+ if (!shuttingDown) {
3364
+ shuttingDown = true;
3365
+ child.stdin?.destroy();
3366
+ }
3367
+ if (child.pid !== void 0) {
3368
+ child.kill("SIGKILL");
3369
+ } else if (!spawnListener) {
3370
+ spawnListener = () => {
3371
+ if (child.pid !== void 0) child.kill("SIGKILL");
3372
+ };
3373
+ child.once("spawn", spawnListener);
3374
+ }
3375
+ };
3376
+ try {
3377
+ child = spawn("runner-synchroniser", args, {
3378
+ env: opts.env ?? process.env,
3379
+ stdio: [opts.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
3380
+ });
3381
+ } catch (error2) {
3382
+ finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
3383
+ return;
3384
+ }
3385
+ child.stdout?.setEncoding("utf8");
3386
+ child.stdout?.on("data", (chunk) => {
3387
+ stdout += chunk;
3388
+ });
3389
+ child.stderr?.setEncoding("utf8");
3390
+ child.stderr?.on("data", (chunk) => {
3391
+ stderr += chunk;
3392
+ });
3393
+ child.once("error", (error2) => {
3394
+ childFailure = error2;
3395
+ if (child.pid !== void 0) terminateChild();
3396
+ armErrorCloseFallback();
3397
+ });
3398
+ child.once("close", (code) => {
3399
+ const failure = stdinFailure ?? childFailure;
3400
+ finish({
3401
+ code: failure === void 0 ? code : null,
3402
+ stdout,
3403
+ stderr: failure === void 0 ? stderr : appendError(stderr, failure),
3404
+ timedOut
3405
+ });
3406
+ });
3407
+ const stdin = child.stdin;
3408
+ const failStdin = (error2) => {
3409
+ if (settled || shuttingDown || isPipeClosed(error2)) return;
3410
+ stdinFailure = error2;
3411
+ terminateChild();
3412
+ armErrorCloseFallback();
3413
+ };
3414
+ stdin?.once("error", failStdin);
3415
+ if (opts.stdin !== void 0 && stdin === null) {
3416
+ failStdin(new Error("synchroniser stdin is unavailable"));
3417
+ } else if (opts.stdin !== void 0 && stdin !== null) {
3418
+ const drained = stdin.write(opts.stdin, (error2) => {
3419
+ if (error2) failStdin(error2);
3420
+ });
3421
+ if (!drained) {
3422
+ stdin.once("drain", () => stdin.end());
3423
+ } else {
3424
+ stdin.end();
3425
+ }
3426
+ }
3427
+ if (opts.signal) {
3428
+ const killChild = () => {
3429
+ terminateChild();
3430
+ };
3431
+ abortListener = killChild;
3432
+ if (opts.signal.aborted) {
3433
+ abortListener();
3434
+ } else {
3435
+ opts.signal.addEventListener("abort", abortListener, { once: true });
3436
+ if (opts.signal.aborted) abortListener();
3437
+ }
3438
+ }
3439
+ timer.handle = setTimeout(
3440
+ () => {
3441
+ timedOut = true;
3442
+ terminateChild();
3443
+ },
3444
+ Math.max(0, opts.timeoutMs)
3445
+ );
3446
+ });
3447
+ }
3448
+
3449
+ // src/lib/opencode/session-archive-upload.ts
3450
+ var MAX_SESSION_ARCHIVE_BYTES = MAX_ARCHIVE_STDIN_BYTES;
3451
+ var EXIT_ARCHIVE_CONFIG_MISSING = 50;
3452
+ var EXIT_ARCHIVE_ACCESS_DENIED = 51;
3453
+ function transportFailure(bytes, reason) {
3454
+ const message = reason === "missing-executable" ? "runner-synchroniser is unavailable; session archival requires a hosted runner." : reason === "timeout" ? "runner-synchroniser timed out before the archive upload completed." : "session archive upload did not complete.";
3455
+ return { kind: "transport-failed", bytes, reason, message };
3456
+ }
3457
+ function isJson(body) {
3458
+ try {
3459
+ JSON.parse(body.toString("utf8"));
3460
+ return true;
3461
+ } catch (error2) {
3462
+ void error2;
3463
+ return false;
3464
+ }
3465
+ }
3466
+ async function uploadSessionArchive(key, body, options) {
3467
+ const bytes = Buffer.from(body);
3468
+ if (bytes.byteLength > MAX_SESSION_ARCHIVE_BYTES) {
3469
+ return transportFailure(bytes.byteLength, "too-large");
3470
+ }
3471
+ if (!isJson(bytes)) return transportFailure(bytes.byteLength, "invalid-json");
3472
+ const run2 = options.runSynchroniser ?? runSynchroniser;
3473
+ let result;
3474
+ try {
3475
+ result = await run2(["archive-put", key], {
3476
+ timeoutMs: options.timeoutMs,
3477
+ env: options.env,
3478
+ signal: options.signal,
3479
+ stdin: bytes
3480
+ });
3481
+ } catch (error2) {
3482
+ return transportFailure(
3483
+ bytes.byteLength,
3484
+ error2 instanceof Error && /ENOENT|runner-synchroniser/i.test(error2.message) ? "missing-executable" : "spawn-failed"
3485
+ );
3486
+ }
3487
+ if (result.timedOut) return transportFailure(bytes.byteLength, "timeout");
3488
+ if (result.code === 0) return { kind: "ok", bytes: bytes.byteLength };
3489
+ if (result.code === EXIT_ARCHIVE_CONFIG_MISSING) {
3490
+ return { kind: "config-missing", bytes: bytes.byteLength };
3491
+ }
3492
+ if (result.code === EXIT_ARCHIVE_ACCESS_DENIED) {
3493
+ return { kind: "access-denied", bytes: bytes.byteLength };
3494
+ }
3495
+ if (result.code === null && /ENOENT|runner-synchroniser/i.test(result.stderr)) {
3496
+ return transportFailure(bytes.byteLength, "missing-executable");
3497
+ }
3498
+ return transportFailure(
3499
+ bytes.byteLength,
3500
+ result.code === null ? "spawn-failed" : "process-failed"
3501
+ );
3502
+ }
3503
+
3504
+ // src/lib/opencode/session-archive-run.ts
3505
+ var SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS = 6e4;
3506
+ function updatedAt(session) {
3507
+ const updated = session.time?.updated;
3508
+ return typeof updated === "number" && Number.isFinite(updated) ? updated : -Infinity;
3509
+ }
3510
+ function sessionArchiveKey(prefix, sessionId) {
3511
+ return `${prefix.replace(/\/+$/, "")}/conversation-archives/${encodeURIComponent(sessionId)}.json`;
3512
+ }
3513
+ function failureMessage(error2) {
3514
+ return error2 instanceof Error ? error2.message : String(error2);
3515
+ }
3516
+ function isBudgetExpired(deadline, now, signal) {
3517
+ return signal?.aborted === true || now() >= deadline;
3518
+ }
3519
+ function createBudgetSignal(parent, timeoutMs) {
3520
+ const controller = new AbortController();
3521
+ const forwardAbort = () => controller.abort(parent?.reason);
3522
+ if (parent?.aborted) forwardAbort();
3523
+ else parent?.addEventListener("abort", forwardAbort, { once: true });
3524
+ const timer = setTimeout(
3525
+ () => controller.abort(new Error("session archive budget exceeded")),
3526
+ timeoutMs
3527
+ );
3528
+ return {
3529
+ signal: controller.signal,
3530
+ dispose: () => {
3531
+ clearTimeout(timer);
3532
+ parent?.removeEventListener("abort", forwardAbort);
3533
+ }
3534
+ };
3535
+ }
3536
+ async function runSessionArchive({
3537
+ sessions,
3538
+ deps,
3539
+ budgetMs,
3540
+ now = Date.now
3541
+ }) {
3542
+ const startedAt = now();
3543
+ const deadline = startedAt + Math.max(0, budgetMs);
3544
+ const summary = {
3545
+ inspected: sessions.length,
3546
+ uploaded: 0,
3547
+ unchanged: 0,
3548
+ failed: 0,
3549
+ uploadAttempts: 0,
3550
+ uploadFailures: 0,
3551
+ skippedBudget: 0,
3552
+ skippedBudgetIds: [],
3553
+ neverWatermarkedFailures: 0,
3554
+ unmappedIds: [],
3555
+ archivedSessionIds: [],
3556
+ failures: [],
3557
+ staleSince: null
3558
+ };
3559
+ const ordered = [...sessions].sort((left, right) => updatedAt(right) - updatedAt(left));
3560
+ const upload = deps.upload ?? uploadSessionArchive;
3561
+ const exportMessages = deps.exportMessages ?? exportSessionMessages;
3562
+ const writeWatermark = deps.recordWatermark ?? recordUpload;
3563
+ const failedWatermarks = [];
3564
+ const recordFailedWatermark = (watermark) => {
3565
+ if (watermark) failedWatermarks.push(watermark.uploaded_at);
3566
+ else summary.neverWatermarkedFailures++;
3567
+ };
3568
+ for (let index = 0; index < ordered.length; index++) {
3569
+ const session = ordered[index];
3570
+ const remainingMs = Math.floor(deadline - now());
3571
+ if (remainingMs <= 0 || deps.signal?.aborted) {
3572
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3573
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3574
+ break;
3575
+ }
3576
+ const conversationId = deps.bindings.get(session.id) ?? null;
3577
+ if (conversationId === null) summary.unmappedIds.push(session.id);
3578
+ const exportBudget = createBudgetSignal(deps.signal, remainingMs);
3579
+ let messages;
3580
+ try {
3581
+ messages = await exportMessages(deps.client, session.id, exportBudget.signal);
3582
+ } catch (error2) {
3583
+ if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
3584
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3585
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3586
+ break;
3587
+ }
3588
+ summary.failed++;
3589
+ const reason = error2 instanceof SessionArchiveExportError ? error2.code : "export-failed";
3590
+ summary.failures.push({
3591
+ sessionId: session.id,
3592
+ outcome: "export-failed",
3593
+ reason,
3594
+ message: failureMessage(error2),
3595
+ archived: false
3596
+ });
3597
+ recordFailedWatermark(deps.watermarks.get(session.id));
3598
+ continue;
3599
+ } finally {
3600
+ exportBudget.dispose();
3601
+ }
3602
+ if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
3603
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3604
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3605
+ break;
3606
+ }
3607
+ try {
3608
+ const archivedAt = new Date(now()).toISOString();
3609
+ const snapshot = buildSessionArchive({
3610
+ session,
3611
+ messages,
3612
+ apiVersion: deps.client.version,
3613
+ conversationId,
3614
+ bindingSource: conversationId === null ? "unmapped" : "ledger",
3615
+ runnerId: deps.runnerId,
3616
+ archivedAt
3617
+ });
3618
+ const key = sessionArchiveKey(deps.keyPrefix, session.id);
3619
+ const existing = deps.watermarks.get(session.id);
3620
+ if (existing?.fingerprint === snapshot.content_fingerprint) {
3621
+ summary.unchanged++;
3622
+ summary.archivedSessionIds.push(session.id);
3623
+ continue;
3624
+ }
3625
+ const uploadRemainingMs = Math.floor(deadline - now());
3626
+ if (uploadRemainingMs <= 0 || deps.signal?.aborted) {
3627
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3628
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3629
+ break;
3630
+ }
3631
+ summary.uploadAttempts++;
3632
+ const uploadBudget = createBudgetSignal(deps.signal, uploadRemainingMs);
3633
+ let outcome;
3634
+ try {
3635
+ outcome = await upload(key, JSON.stringify(snapshot), {
3636
+ timeoutMs: Math.min(uploadRemainingMs, SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS),
3637
+ signal: uploadBudget.signal
3638
+ });
3639
+ } finally {
3640
+ uploadBudget.dispose();
3641
+ }
3642
+ if (outcome.kind !== "ok") {
3643
+ summary.failed++;
3644
+ summary.uploadFailures++;
3645
+ const reason = "reason" in outcome ? outcome.reason : void 0;
3646
+ summary.failures.push({
3647
+ sessionId: session.id,
3648
+ outcome: outcome.kind,
3649
+ ...reason !== void 0 ? { reason } : {},
3650
+ ..."message" in outcome ? { message: outcome.message } : {},
3651
+ archived: false
3652
+ });
3653
+ recordFailedWatermark(existing);
3654
+ if (uploadBudget.signal.aborted) {
3655
+ summary.skippedBudgetIds.push(...ordered.slice(index + 1).map(({ id }) => id));
3656
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3657
+ break;
3658
+ }
3659
+ continue;
3660
+ }
3661
+ summary.uploaded++;
3662
+ summary.archivedSessionIds.push(session.id);
3663
+ const watermark = {
3664
+ fingerprint: snapshot.content_fingerprint,
3665
+ uploaded_at: new Date(now()).toISOString(),
3666
+ key
3667
+ };
3668
+ try {
3669
+ writeWatermark(deps.watermarkPath, session.id, watermark, deps.watermarks);
3670
+ } catch (error2) {
3671
+ summary.failures.push({
3672
+ sessionId: session.id,
3673
+ outcome: "watermark-write-failed",
3674
+ message: failureMessage(error2),
3675
+ archived: true
3676
+ });
3677
+ }
3678
+ } catch (error2) {
3679
+ if (isBudgetExpired(deadline, now, deps.signal)) {
3680
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3681
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3682
+ break;
3683
+ }
3684
+ summary.failed++;
3685
+ summary.failures.push({
3686
+ sessionId: session.id,
3687
+ outcome: "archive-failed",
3688
+ message: failureMessage(error2),
3689
+ archived: false
3690
+ });
3691
+ recordFailedWatermark(deps.watermarks.get(session.id));
3692
+ }
3693
+ }
3694
+ summary.staleSince = failedWatermarks.sort()[0] ?? null;
3695
+ return summary;
3696
+ }
3697
+
2760
3698
  // src/lib/opencode/auth.ts
2761
3699
  function buildOpenCodeBasicAuthHeader(password) {
2762
3700
  return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
@@ -2775,8 +3713,26 @@ async function checkOpenCodeHealth(port) {
2775
3713
  if (!response.ok) {
2776
3714
  return { healthy: false, error: `HTTP ${response.status}` };
2777
3715
  }
2778
- const data = await response.json().catch(() => ({}));
2779
- return { healthy: true, version: data.version };
3716
+ const body = await response.text();
3717
+ let data;
3718
+ try {
3719
+ data = JSON.parse(body);
3720
+ } catch (error2) {
3721
+ return {
3722
+ healthy: false,
3723
+ okWithNonJsonBody: true,
3724
+ error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
3725
+ };
3726
+ }
3727
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
3728
+ return {
3729
+ healthy: false,
3730
+ okWithNonJsonBody: true,
3731
+ error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
3732
+ };
3733
+ }
3734
+ const version2 = data.version;
3735
+ return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
2780
3736
  } catch (error2) {
2781
3737
  const message = error2 instanceof Error ? error2.message : "Unknown error";
2782
3738
  return { healthy: false, error: message };
@@ -2792,10 +3748,17 @@ async function checkOpenCode2Health(port, password) {
2792
3748
  if (password !== void 0) {
2793
3749
  headers.Authorization = buildOpenCodeBasicAuthHeader(password);
2794
3750
  }
2795
- const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
3751
+ let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
2796
3752
  headers,
2797
3753
  signal: AbortSignal.timeout(2e3)
2798
3754
  });
3755
+ const usesHealthFallback = response.status === 404;
3756
+ if (usesHealthFallback) {
3757
+ response = await fetch(`http://127.0.0.1:${port}/api/health`, {
3758
+ headers,
3759
+ signal: AbortSignal.timeout(2e3)
3760
+ });
3761
+ }
2799
3762
  if (response.status === 401) {
2800
3763
  return { healthy: false, authFailed: true, error: "HTTP 401" };
2801
3764
  }
@@ -2810,23 +3773,29 @@ async function checkOpenCode2Health(port, password) {
2810
3773
  } catch (error2) {
2811
3774
  return {
2812
3775
  healthy: false,
2813
- error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
3776
+ error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
2814
3777
  };
2815
3778
  }
2816
3779
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
2817
3780
  return {
2818
3781
  healthy: false,
2819
- error: `Invalid health response${excerpt}: expected healthy=true`
3782
+ error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
2820
3783
  };
2821
3784
  }
2822
- const health = data;
2823
- if (health.healthy !== true) {
3785
+ const bodyObject = data;
3786
+ if (usesHealthFallback && bodyObject.healthy !== true) {
2824
3787
  return {
2825
3788
  healthy: false,
2826
3789
  error: `Invalid health response${excerpt}: expected healthy=true`
2827
3790
  };
2828
3791
  }
2829
- return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
3792
+ if (!usesHealthFallback) {
3793
+ return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
3794
+ healthy: false,
3795
+ error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
3796
+ };
3797
+ }
3798
+ return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
2830
3799
  } catch (error2) {
2831
3800
  const message = error2 instanceof Error ? error2.message : "Unknown error";
2832
3801
  return { healthy: false, error: message };
@@ -2884,10 +3853,18 @@ function describeRequestOperation(input, init) {
2884
3853
  function withRequestTimeout(fetchImpl, timeoutMs) {
2885
3854
  return (async (input, init) => {
2886
3855
  const startedAt = Date.now();
3856
+ const controller = new AbortController();
3857
+ const abortFromCaller = () => controller.abort(init?.signal?.reason);
3858
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
3859
+ const abortFromTimeout = () => controller.abort(timeoutSignal.reason);
3860
+ if (init?.signal?.aborted) abortFromCaller();
3861
+ else init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
3862
+ if (timeoutSignal.aborted) abortFromTimeout();
3863
+ else timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
2887
3864
  try {
2888
- return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
3865
+ return await fetchImpl(input, { ...init, signal: controller.signal });
2889
3866
  } catch (error2) {
2890
- if (error2?.name === "TimeoutError") {
3867
+ if (timeoutSignal.aborted) {
2891
3868
  throw new RequestTimeoutError(
2892
3869
  describeRequestOperation(input, init),
2893
3870
  timeoutMs,
@@ -2895,11 +3872,21 @@ function withRequestTimeout(fetchImpl, timeoutMs) {
2895
3872
  );
2896
3873
  }
2897
3874
  throw error2;
3875
+ } finally {
3876
+ init?.signal?.removeEventListener("abort", abortFromCaller);
3877
+ timeoutSignal.removeEventListener("abort", abortFromTimeout);
2898
3878
  }
2899
3879
  });
2900
3880
  }
2901
3881
 
2902
3882
  // src/lib/opencode/client.ts
3883
+ var OpenCodeRequestError = class extends Error {
3884
+ constructor(statusCode, message) {
3885
+ super(message);
3886
+ this.statusCode = statusCode;
3887
+ this.name = "OpenCodeRequestError";
3888
+ }
3889
+ };
2903
3890
  function redactPassword(message, password) {
2904
3891
  return message.replaceAll(password, "[redacted]");
2905
3892
  }
@@ -2921,7 +3908,8 @@ function createOpenCodeClient(options) {
2921
3908
  const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
2922
3909
  if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
2923
3910
  const body = await response.text();
2924
- throw new Error(
3911
+ throw new OpenCodeRequestError(
3912
+ response.status,
2925
3913
  `OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
2926
3914
  );
2927
3915
  }
@@ -2929,7 +3917,11 @@ function createOpenCodeClient(options) {
2929
3917
  } catch (error2) {
2930
3918
  if (options.version === "v2" && password !== null) {
2931
3919
  const message = error2 instanceof Error ? error2.message : String(error2);
2932
- throw new Error(redactPassword(message, password));
3920
+ const redactedMessage = redactPassword(message, password);
3921
+ if (error2 instanceof OpenCodeRequestError) {
3922
+ throw new OpenCodeRequestError(error2.statusCode, redactedMessage);
3923
+ }
3924
+ throw new Error(redactedMessage);
2933
3925
  }
2934
3926
  throw error2;
2935
3927
  }
@@ -3078,28 +4070,38 @@ function sessionListPath(options) {
3078
4070
  async function listSessionPage(port, client, options) {
3079
4071
  const path = sessionListPath(options);
3080
4072
  try {
3081
- const res = await requestWithClient(port, client, path);
3082
- if (!res.ok) return null;
4073
+ const res = await requestWithClient(
4074
+ port,
4075
+ client,
4076
+ path,
4077
+ options?.signal ? { signal: options.signal } : void 0,
4078
+ { allowStatuses: ALL_HTTP_STATUSES }
4079
+ );
4080
+ if (!res.ok) {
4081
+ console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
4082
+ return null;
4083
+ }
3083
4084
  const body = await res.json();
3084
4085
  return Array.isArray(body) ? body : null;
3085
4086
  } catch (error2) {
3086
- console.error(
3087
- `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3088
- );
4087
+ if (options?.signal?.aborted) return null;
4088
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4089
+ console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
3089
4090
  return null;
3090
4091
  }
3091
4092
  }
3092
4093
  async function listSessions(port, client, options) {
3093
- if (client?.version === "v2") return listV2Sessions(client);
4094
+ if (client?.version === "v2") return listV2Sessions(client, options?.signal);
3094
4095
  return listSessionPage(port, client, options);
3095
4096
  }
3096
- async function listAllSessions(port, client) {
4097
+ async function listAllSessions(port, client, signal) {
3097
4098
  if (client?.version === "v2") {
3098
- const sessions2 = await listV2Sessions(client);
4099
+ const sessions2 = await listV2Sessions(client, signal);
3099
4100
  return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
3100
4101
  }
3101
4102
  const sessions = await listSessionPage(port, client, {
3102
- limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
4103
+ limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
4104
+ signal
3103
4105
  });
3104
4106
  if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
3105
4107
  if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
@@ -3595,21 +4597,24 @@ function findLastAssistantReplyFor(messages, userMessageId) {
3595
4597
  }
3596
4598
  return last;
3597
4599
  }
4600
+ function assistantMessagesInTurnBlock(messages, userMessageId) {
4601
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
4602
+ if (userIndex === -1) return [];
4603
+ const assistants = [];
4604
+ for (let i = userIndex + 1; i < messages.length; i++) {
4605
+ const message = messages[i];
4606
+ const role = roleOf2(message);
4607
+ if (role === "user") break;
4608
+ if (role === "assistant") assistants.push(message);
4609
+ }
4610
+ return assistants;
4611
+ }
3598
4612
  function collectSubagentSessions(messages, userMessageId) {
3599
4613
  if (!messages || messages.length === 0) return [];
3600
4614
  const byParent = messages.filter(
3601
4615
  (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
3602
4616
  );
3603
- const assistants = byParent.length > 0 ? byParent : [];
3604
- if (assistants.length === 0) {
3605
- const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
3606
- if (userIndex === -1) return [];
3607
- for (let i = userIndex + 1; i < messages.length; i++) {
3608
- const message = messages[i];
3609
- if (roleOf2(message) === "user") break;
3610
- if (roleOf2(message) === "assistant") assistants.push(message);
3611
- }
3612
- }
4617
+ const assistants = byParent.length > 0 ? byParent : assistantMessagesInTurnBlock(messages, userMessageId);
3613
4618
  const refs = [];
3614
4619
  const seen = /* @__PURE__ */ new Set();
3615
4620
  for (const message of assistants) {
@@ -3764,8 +4769,7 @@ function messageUsage(messages, userMessageId) {
3764
4769
  if (byParentAll.length > 0) {
3765
4770
  correlated = byParentAll;
3766
4771
  } else {
3767
- const reply = findAssistantReplyAfter(messages, userMessageId);
3768
- correlated = reply ? [reply] : [];
4772
+ correlated = assistantMessagesInTurnBlock(messages, userMessageId);
3769
4773
  }
3770
4774
  if (correlated.length === 0) return null;
3771
4775
  return sumAssistantUsage(correlated);
@@ -3900,8 +4904,8 @@ function classifyReplyAuthError(reply) {
3900
4904
  };
3901
4905
  }
3902
4906
  }
3903
- const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3904
- if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
4907
+ const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
4908
+ if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
3905
4909
  return {
3906
4910
  kind: "model_auth",
3907
4911
  providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
@@ -3987,39 +4991,85 @@ async function hasAnyConfiguredProvider(port, client) {
3987
4991
  );
3988
4992
  return null;
3989
4993
  }
3990
- const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4994
+ const query = `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
4995
+ const integrationPath = `/api/integration${query}`;
4996
+ let integrationReady = null;
3991
4997
  try {
3992
- const res = await client.request(path);
4998
+ const res = await client.request(integrationPath);
3993
4999
  if (!res.ok) {
3994
5000
  console.error(
3995
- `[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
5001
+ `[hasAnyConfiguredProvider] GET ${integrationPath} returned HTTP ${res.status} (port ${port})`
3996
5002
  );
3997
- return null;
5003
+ } else {
5004
+ const body = await res.json();
5005
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
5006
+ console.error(
5007
+ `[hasAnyConfiguredProvider] GET ${integrationPath} body had no integration data array (port ${port})`
5008
+ );
5009
+ } else {
5010
+ let valid = true;
5011
+ for (const integration of body.data) {
5012
+ if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
5013
+ console.error(
5014
+ `[hasAnyConfiguredProvider] GET ${integrationPath} body contained an invalid integration (port ${port})`
5015
+ );
5016
+ valid = false;
5017
+ break;
5018
+ }
5019
+ }
5020
+ if (valid) {
5021
+ integrationReady = body.data.some(
5022
+ (integration) => integration.connections.length > 0
5023
+ );
5024
+ }
5025
+ }
3998
5026
  }
3999
- const body = await res.json();
4000
- if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
5027
+ } catch (error2) {
5028
+ console.error(
5029
+ `[hasAnyConfiguredProvider] GET ${integrationPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
5030
+ );
5031
+ }
5032
+ if (integrationReady === true) return true;
5033
+ const providerPath = `/api/provider${query}`;
5034
+ let providerReady = null;
5035
+ try {
5036
+ const res = await client.request(providerPath);
5037
+ if (!res.ok) {
4001
5038
  console.error(
4002
- `[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
5039
+ `[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
4003
5040
  );
4004
- return null;
4005
- }
4006
- for (const integration of body.data) {
4007
- if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
5041
+ } else {
5042
+ const body = await res.json();
5043
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
4008
5044
  console.error(
4009
- `[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
5045
+ `[hasAnyConfiguredProvider] GET ${providerPath} body had no provider data array (port ${port})`
4010
5046
  );
4011
- return null;
5047
+ } else {
5048
+ let valid = true;
5049
+ for (const provider of body.data) {
5050
+ if (!provider || typeof provider !== "object" || Array.isArray(provider) || typeof provider.id !== "string") {
5051
+ console.error(
5052
+ `[hasAnyConfiguredProvider] GET ${providerPath} body contained an invalid provider (port ${port})`
5053
+ );
5054
+ valid = false;
5055
+ break;
5056
+ }
5057
+ }
5058
+ if (valid) {
5059
+ providerReady = body.data.some(
5060
+ (provider) => provider.activation === "enabled"
5061
+ );
5062
+ }
4012
5063
  }
4013
5064
  }
4014
- return body.data.some(
4015
- (integration) => Array.isArray(integration.connections) && integration.connections.length > 0
4016
- );
4017
5065
  } catch (error2) {
4018
5066
  console.error(
4019
- `[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
5067
+ `[hasAnyConfiguredProvider] GET ${providerPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
4020
5068
  );
4021
- return null;
4022
5069
  }
5070
+ if (providerReady === true) return true;
5071
+ if (integrationReady === null || providerReady === null) return null;
5072
+ return false;
4023
5073
  }
4024
5074
  try {
4025
5075
  const res = await requestWithClient(port, client, "/config/providers");
@@ -4172,14 +5222,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
4172
5222
  }
4173
5223
 
4174
5224
  // src/lib/opencode/session-v2.ts
4175
- function isRecord(value) {
5225
+ var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
5226
+ function isRecord3(value) {
4176
5227
  return typeof value === "object" && value !== null && !Array.isArray(value);
4177
5228
  }
4178
5229
  function finiteNumber2(value) {
4179
5230
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4180
5231
  }
4181
5232
  function adaptTime(value) {
4182
- if (!isRecord(value)) return void 0;
5233
+ if (!isRecord3(value)) return void 0;
4183
5234
  const created = finiteNumber2(value.created);
4184
5235
  const completed = finiteNumber2(value.completed);
4185
5236
  if (created === void 0 && completed === void 0) return void 0;
@@ -4189,11 +5240,11 @@ function adaptTime(value) {
4189
5240
  };
4190
5241
  }
4191
5242
  function adaptTokens(value) {
4192
- if (!isRecord(value)) return void 0;
5243
+ if (!isRecord3(value)) return void 0;
4193
5244
  const input = finiteNumber2(value.input);
4194
5245
  const output = finiteNumber2(value.output);
4195
5246
  const reasoning = finiteNumber2(value.reasoning);
4196
- const cache = isRecord(value.cache) ? {
5247
+ const cache = isRecord3(value.cache) ? {
4197
5248
  ...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
4198
5249
  ...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
4199
5250
  } : void 0;
@@ -4217,7 +5268,7 @@ function adaptMessageInfo(value, role) {
4217
5268
  if (typeof value.finish === "string") info.finish = value.finish;
4218
5269
  if ("error" in value) info.error = value.error;
4219
5270
  if (typeof value.agent === "string") info.agent = value.agent;
4220
- if (isRecord(value.model)) {
5271
+ if (isRecord3(value.model)) {
4221
5272
  if (typeof value.model.id === "string") info.modelID = value.model.id;
4222
5273
  if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
4223
5274
  }
@@ -4227,7 +5278,7 @@ function adaptMessageInfo(value, role) {
4227
5278
  return info;
4228
5279
  }
4229
5280
  function adaptV2Message(value) {
4230
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
5281
+ if (!isRecord3(value) || typeof value.id !== "string" || typeof value.type !== "string") {
4231
5282
  return null;
4232
5283
  }
4233
5284
  if (value.type === "user") {
@@ -4240,7 +5291,7 @@ function adaptV2Message(value) {
4240
5291
  if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
4241
5292
  const parts = [];
4242
5293
  for (const content of value.content) {
4243
- if (!isRecord(content) || typeof content.type !== "string") return null;
5294
+ if (!isRecord3(content) || typeof content.type !== "string") return null;
4244
5295
  if (content.type === "text") {
4245
5296
  if (typeof content.text !== "string") return null;
4246
5297
  parts.push({ type: "text", text: content.text });
@@ -4254,23 +5305,23 @@ function adaptV2Message(value) {
4254
5305
  };
4255
5306
  }
4256
5307
  function adaptFormTool(value) {
4257
- if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
5308
+ if (!isRecord3(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
4258
5309
  return void 0;
4259
5310
  }
4260
5311
  return { messageID: value.messageID, callID: value.id };
4261
5312
  }
4262
5313
  function adaptV2FormWire(value) {
4263
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5314
+ if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
4264
5315
  return null;
4265
5316
  }
4266
5317
  return value;
4267
5318
  }
4268
5319
  function adaptV2FormField(value, header) {
4269
- if (!isRecord(value)) return null;
5320
+ if (!isRecord3(value)) return null;
4270
5321
  const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
4271
5322
  if (!question) return null;
4272
5323
  const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
4273
- if (!isRecord(option)) return [];
5324
+ if (!isRecord3(option)) return [];
4274
5325
  const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
4275
5326
  if (!label) return [];
4276
5327
  return [
@@ -4288,7 +5339,7 @@ function adaptV2Form(value) {
4288
5339
  const header = typeof form.title === "string" ? form.title : "";
4289
5340
  const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
4290
5341
  if (questions.length === 0) return null;
4291
- const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
5342
+ const tool = isRecord3(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
4292
5343
  return {
4293
5344
  id: form.id,
4294
5345
  sessionID: form.sessionID,
@@ -4298,7 +5349,7 @@ function adaptV2Form(value) {
4298
5349
  };
4299
5350
  }
4300
5351
  function adaptV2FormList(value) {
4301
- if (!isRecord(value) || !Array.isArray(value.data)) return null;
5352
+ if (!isRecord3(value) || !Array.isArray(value.data)) return null;
4302
5353
  return value.data.map(adaptV2Form).filter((form) => form !== null);
4303
5354
  }
4304
5355
  function adaptPattern(value) {
@@ -4309,7 +5360,7 @@ function adaptPattern(value) {
4309
5360
  return void 0;
4310
5361
  }
4311
5362
  function adaptV2PermissionWire(value) {
4312
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5363
+ if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
4313
5364
  return null;
4314
5365
  }
4315
5366
  if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
@@ -4321,12 +5372,12 @@ function adaptV2Permission(value) {
4321
5372
  const type = permission.permission ?? permission.action;
4322
5373
  if (!type) return null;
4323
5374
  const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
4324
- const time = isRecord(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
5375
+ const time = isRecord3(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
4325
5376
  return {
4326
5377
  id: permission.id,
4327
5378
  type,
4328
5379
  sessionID: permission.sessionID,
4329
- metadata: isRecord(permission.metadata) ? permission.metadata : {},
5380
+ metadata: isRecord3(permission.metadata) ? permission.metadata : {},
4330
5381
  raw: permission,
4331
5382
  ...pattern !== void 0 ? { pattern } : {},
4332
5383
  ...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
@@ -4336,12 +5387,12 @@ function adaptV2Permission(value) {
4336
5387
  };
4337
5388
  }
4338
5389
  function adaptV2PermissionList(value) {
4339
- if (!isRecord(value) || !Array.isArray(value.data)) return null;
5390
+ if (!isRecord3(value) || !Array.isArray(value.data)) return null;
4340
5391
  return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
4341
5392
  }
4342
5393
  function adaptV2Session(value) {
4343
- if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
4344
- const time = isRecord(value.time) ? {
5394
+ if (!isRecord3(value) || typeof value.id !== "string" || value.id.length === 0) return null;
5395
+ const time = isRecord3(value.time) ? {
4345
5396
  ...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
4346
5397
  ...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
4347
5398
  } : void 0;
@@ -4353,7 +5404,7 @@ function adaptV2Session(value) {
4353
5404
  };
4354
5405
  }
4355
5406
  function adaptV2SessionList(value) {
4356
- if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
5407
+ if (!isRecord3(value) || !Array.isArray(value.data) || !isRecord3(value.cursor)) return null;
4357
5408
  return {
4358
5409
  data: value.data.map(adaptV2Session).filter((session) => session !== null),
4359
5410
  cursor: value.cursor
@@ -4362,18 +5413,18 @@ function adaptV2SessionList(value) {
4362
5413
  function adaptV2Location(value) {
4363
5414
  const candidates = [
4364
5415
  value,
4365
- isRecord(value) ? value.data : void 0,
4366
- isRecord(value) ? value.location : void 0
5416
+ isRecord3(value) ? value.data : void 0,
5417
+ isRecord3(value) ? value.location : void 0
4367
5418
  ];
4368
5419
  for (const candidate of candidates) {
4369
- if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
5420
+ if (!isRecord3(candidate) || typeof candidate.directory !== "string") continue;
4370
5421
  const directory = candidate.directory.trim();
4371
5422
  if (directory) return directory;
4372
5423
  }
4373
5424
  return null;
4374
5425
  }
4375
5426
  function adaptV2Messages(value) {
4376
- if (!isRecord(value) || !Array.isArray(value.data)) return [];
5427
+ if (!isRecord3(value) || !Array.isArray(value.data)) return [];
4377
5428
  return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
4378
5429
  }
4379
5430
  async function readJson(response) {
@@ -4388,7 +5439,7 @@ async function readJson(response) {
4388
5439
  async function readData(client, path, init) {
4389
5440
  const response = await client.request(path, init);
4390
5441
  const body = await readJson(response);
4391
- if (!isRecord(body) || !("data" in body)) {
5442
+ if (!isRecord3(body) || !("data" in body)) {
4392
5443
  throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
4393
5444
  }
4394
5445
  return body.data;
@@ -4399,6 +5450,14 @@ var OpenCodeV2PromptAckError = class extends Error {
4399
5450
  this.name = "OpenCodeV2PromptAckError";
4400
5451
  }
4401
5452
  };
5453
+ var OpenCodeV2ModelSwitchError = class extends Error {
5454
+ statusCode;
5455
+ constructor(error2) {
5456
+ super(error2 instanceof Error ? error2.message : String(error2));
5457
+ this.name = "OpenCodeV2ModelSwitchError";
5458
+ this.statusCode = error2 instanceof OpenCodeRequestError ? error2.statusCode : null;
5459
+ }
5460
+ };
4402
5461
  async function getOpenCodeDirectoryV2(client) {
4403
5462
  try {
4404
5463
  return adaptV2Location(await readJson(await client.request("/api/location")));
@@ -4415,7 +5474,7 @@ async function createV2Session(client, directory) {
4415
5474
  headers: { "Content-Type": "application/json" },
4416
5475
  body: JSON.stringify({ location: { directory } })
4417
5476
  });
4418
- if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
5477
+ if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
4419
5478
  throw new Error("OpenCode V2 create session response was missing data.id");
4420
5479
  }
4421
5480
  return data.id;
@@ -4426,25 +5485,35 @@ async function getV2Session(client, sessionId) {
4426
5485
  if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
4427
5486
  return session;
4428
5487
  }
4429
- async function listV2SessionPage(client, cursor) {
5488
+ async function listV2SessionPage(client, cursor, signal) {
5489
+ const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
4430
5490
  const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
4431
5491
  try {
4432
- return adaptV2SessionList(await readJson(await client.request(path)));
5492
+ const response = await client.request(path, signal ? { signal } : void 0, {
5493
+ allowStatuses: ALL_HTTP_STATUSES2
5494
+ });
5495
+ if (!response.ok) {
5496
+ console.error(
5497
+ `[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
5498
+ );
5499
+ return null;
5500
+ }
5501
+ return adaptV2SessionList(await readJson(response));
4433
5502
  } catch (error2) {
4434
- console.error(
4435
- `[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
4436
- );
5503
+ if (signal?.aborted) return null;
5504
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
5505
+ console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
4437
5506
  return null;
4438
5507
  }
4439
5508
  }
4440
- async function listV2Sessions(client) {
5509
+ async function listV2Sessions(client, signal) {
4441
5510
  const sessions = [];
4442
5511
  const seenCursors = /* @__PURE__ */ new Set();
4443
5512
  let cursor;
4444
5513
  let hasNextPage = true;
4445
5514
  try {
4446
5515
  while (hasNextPage) {
4447
- const page = await listV2SessionPage(client, cursor);
5516
+ const page = await listV2SessionPage(client, cursor, signal);
4448
5517
  if (!page) return null;
4449
5518
  sessions.push(...page.data);
4450
5519
  const next = page.cursor.next;
@@ -4460,8 +5529,9 @@ async function listV2Sessions(client) {
4460
5529
  }
4461
5530
  return sessions;
4462
5531
  } catch (error2) {
5532
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4463
5533
  console.error(
4464
- `[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
5534
+ `[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
4465
5535
  );
4466
5536
  return null;
4467
5537
  }
@@ -4471,7 +5541,7 @@ async function deleteV2Session(client, sessionId) {
4471
5541
  const response = await client.request(
4472
5542
  `/api/session/${encodeURIComponent(sessionId)}`,
4473
5543
  { method: "DELETE" },
4474
- { allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
5544
+ { allowStatuses: ALL_HTTP_STATUSES2 }
4475
5545
  );
4476
5546
  if (response.status >= 200 && response.status < 300) {
4477
5547
  return { ok: true, status: response.status };
@@ -4507,7 +5577,34 @@ async function v2SessionExists(client, sessionId) {
4507
5577
  return null;
4508
5578
  }
4509
5579
  }
4510
- async function sendV2Prompt(client, sessionId, text) {
5580
+ async function sendV2Prompt(client, sessionId, text, options, onInvalidModelSelection) {
5581
+ const modelSelection = options?.model?.trim();
5582
+ const { model, variant } = splitModelVariant(modelSelection);
5583
+ const separator = model?.indexOf("/") ?? -1;
5584
+ const modelReference = model && separator > 0 && separator < model.length - 1 ? {
5585
+ id: model.slice(separator + 1),
5586
+ providerID: model.slice(0, separator),
5587
+ ...variant ? { variant } : {}
5588
+ } : void 0;
5589
+ if (modelSelection && !modelReference) onInvalidModelSelection?.();
5590
+ if (options?.agent) {
5591
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/agent`, {
5592
+ method: "POST",
5593
+ headers: { "Content-Type": "application/json" },
5594
+ body: JSON.stringify({ agent: options.agent })
5595
+ });
5596
+ }
5597
+ if (modelReference) {
5598
+ try {
5599
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/model`, {
5600
+ method: "POST",
5601
+ headers: { "Content-Type": "application/json" },
5602
+ body: JSON.stringify({ model: modelReference })
5603
+ });
5604
+ } catch (error2) {
5605
+ throw new OpenCodeV2ModelSwitchError(error2);
5606
+ }
5607
+ }
4511
5608
  const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
4512
5609
  const response = await client.request(path, {
4513
5610
  method: "POST",
@@ -4522,8 +5619,8 @@ async function sendV2Prompt(client, sessionId, text) {
4522
5619
  `OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
4523
5620
  );
4524
5621
  }
4525
- const data = isRecord(body) && "data" in body ? body.data : void 0;
4526
- if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
5622
+ const data = isRecord3(body) && "data" in body ? body.data : void 0;
5623
+ if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
4527
5624
  throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
4528
5625
  }
4529
5626
  return data.id;
@@ -4532,7 +5629,7 @@ async function getV2SessionMessages(client, sessionId) {
4532
5629
  const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
4533
5630
  try {
4534
5631
  const body = await readJson(await client.request(path));
4535
- if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
5632
+ if (!isRecord3(body) || !Array.isArray(body.data) || !isRecord3(body.cursor)) return null;
4536
5633
  return adaptV2Messages(body);
4537
5634
  } catch (error2) {
4538
5635
  console.error(
@@ -4571,7 +5668,7 @@ async function interruptV2Session(client, sessionId) {
4571
5668
  async function getV2ActiveSessions(client) {
4572
5669
  try {
4573
5670
  const body = await readJson(await client.request("/api/session/active"));
4574
- if (!isRecord(body) || !isRecord(body.data)) return null;
5671
+ if (!isRecord3(body) || !isRecord3(body.data)) return null;
4575
5672
  return body.data;
4576
5673
  } catch (error2) {
4577
5674
  console.error(
@@ -4587,8 +5684,8 @@ async function isV2SessionOngoing(client, sessionId) {
4587
5684
  }
4588
5685
  function sessionErrorReason2(value) {
4589
5686
  if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
4590
- if (isRecord(value)) {
4591
- const data = isRecord(value.data) ? value.data : void 0;
5687
+ if (isRecord3(value)) {
5688
+ const data = isRecord3(value.data) ? value.data : void 0;
4592
5689
  const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
4593
5690
  if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
4594
5691
  }
@@ -4604,17 +5701,17 @@ function adaptV2SessionErrorEvent(value) {
4604
5701
  return null;
4605
5702
  }
4606
5703
  }
4607
- if (!isRecord(parsed)) return null;
5704
+ if (!isRecord3(parsed)) return null;
4608
5705
  try {
4609
5706
  const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
4610
5707
  if (establishedShape) return establishedShape;
4611
5708
  } catch (error2) {
4612
5709
  void error2;
4613
5710
  }
4614
- const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
5711
+ const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord3);
4615
5712
  for (const event of candidates) {
4616
5713
  if (event.type !== "session.error") continue;
4617
- const properties = [event.properties, event.data, event].find(isRecord);
5714
+ const properties = [event.properties, event.data, event].find(isRecord3);
4618
5715
  if (!properties) continue;
4619
5716
  const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
4620
5717
  if (!sessionId) continue;
@@ -4664,13 +5761,13 @@ async function readV2SessionErrorStream(client, options) {
4664
5761
  }
4665
5762
 
4666
5763
  // src/lib/opencode/credential-import-v2.ts
4667
- import { readFileSync as readFileSync5 } from "node:fs";
4668
- import { join as join6 } from "node:path";
5764
+ import { readFileSync as readFileSync8 } from "node:fs";
5765
+ import { join as join8 } from "node:path";
4669
5766
 
4670
5767
  // src/lib/openai-usage.ts
4671
- import { readFileSync as readFileSync4 } from "node:fs";
5768
+ import { readFileSync as readFileSync7 } from "node:fs";
4672
5769
  import { homedir as homedir3 } from "node:os";
4673
- import { join as join5 } from "node:path";
5770
+ import { join as join7 } from "node:path";
4674
5771
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4675
5772
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4676
5773
  var OpenAiUsageError = class extends Error {
@@ -4684,7 +5781,7 @@ function isLocalCredentialProblem2(err) {
4684
5781
  }
4685
5782
  function readOpenCodeChatGptCredentials() {
4686
5783
  try {
4687
- const raw = readFileSync4(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
5784
+ const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4688
5785
  let parsed;
4689
5786
  try {
4690
5787
  parsed = JSON.parse(raw);
@@ -4750,7 +5847,7 @@ function parseCodexUsageHeaders(headers) {
4750
5847
  function normalizeProbeModel(model) {
4751
5848
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4752
5849
  }
4753
- function isRecord2(value) {
5850
+ function isRecord4(value) {
4754
5851
  return typeof value === "object" && value !== null && !Array.isArray(value);
4755
5852
  }
4756
5853
  function unsupportedProbeModels(reason, port) {
@@ -4761,14 +5858,14 @@ async function resolveV1ProbeModels(client, port) {
4761
5858
  try {
4762
5859
  const response = await client.request("/config/providers");
4763
5860
  const body = await response.json();
4764
- if (!isRecord2(body) || !Array.isArray(body.providers)) {
5861
+ if (!isRecord4(body) || !Array.isArray(body.providers)) {
4765
5862
  return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
4766
5863
  }
4767
5864
  const provider = body.providers.find(
4768
- (candidate) => isRecord2(candidate) && candidate.id === "openai"
5865
+ (candidate) => isRecord4(candidate) && candidate.id === "openai"
4769
5866
  );
4770
- if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
4771
- const defaults2 = isRecord2(body.default) ? body.default : void 0;
5867
+ if (!provider || !isRecord4(provider.models)) return { status: "supported", models: [] };
5868
+ const defaults2 = isRecord4(body.default) ? body.default : void 0;
4772
5869
  const candidates = [
4773
5870
  ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4774
5871
  ...Object.keys(provider.models)
@@ -4790,14 +5887,14 @@ async function resolveV2ProbeModels(client, port) {
4790
5887
  try {
4791
5888
  const response = await client.request(path);
4792
5889
  const body = await response.json();
4793
- if (!isRecord2(body) || !Array.isArray(body.data)) {
5890
+ if (!isRecord4(body) || !Array.isArray(body.data)) {
4794
5891
  return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
4795
5892
  }
4796
5893
  const provider = body.data.find(
4797
- (candidate) => isRecord2(candidate) && candidate.id === "openai"
5894
+ (candidate) => isRecord4(candidate) && candidate.id === "openai"
4798
5895
  );
4799
5896
  if (!provider) return { status: "supported", models: [] };
4800
- if (!isRecord2(provider.models)) {
5897
+ if (!isRecord4(provider.models)) {
4801
5898
  return unsupportedProbeModels(
4802
5899
  "V2 provider response has no safe OpenAI model catalogue",
4803
5900
  port
@@ -4907,7 +6004,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
4907
6004
  var CONNECT_KEY_RETRY_DELAY_MS = 250;
4908
6005
  var INTEGRATION_LIST_MAX_READS = 10;
4909
6006
  var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
4910
- function isRecord3(value) {
6007
+ function isRecord5(value) {
4911
6008
  return typeof value === "object" && value !== null && !Array.isArray(value);
4912
6009
  }
4913
6010
  var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
@@ -4916,14 +6013,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
4916
6013
  }
4917
6014
  function readOpenCodeCredentials(homeDir, log3) {
4918
6015
  try {
4919
- const raw = readFileSync5(join6(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
6016
+ const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4920
6017
  let parsed;
4921
6018
  try {
4922
6019
  parsed = JSON.parse(raw);
4923
6020
  } catch (error2) {
4924
6021
  return null;
4925
6022
  }
4926
- return isRecord3(parsed) ? parsed : null;
6023
+ return isRecord5(parsed) ? parsed : null;
4927
6024
  } catch (error2) {
4928
6025
  const code = error2.code;
4929
6026
  if (code !== "ENOENT" && code !== "ENOTDIR") {
@@ -4936,7 +6033,7 @@ function classifyOpenCodeCredentials(credentials2) {
4936
6033
  const importable = [];
4937
6034
  const unsupported = [];
4938
6035
  for (const [integrationId, value] of Object.entries(credentials2)) {
4939
- if (!isRecord3(value) || typeof value.type !== "string") {
6036
+ if (!isRecord5(value) || typeof value.type !== "string") {
4940
6037
  unsupported.push({ integrationId, reason: "unrecognised credential entry" });
4941
6038
  continue;
4942
6039
  }
@@ -4951,7 +6048,7 @@ function classifyOpenCodeCredentials(credentials2) {
4951
6048
  unsupported.push({ integrationId, reason: "unrecognised credential entry" });
4952
6049
  continue;
4953
6050
  }
4954
- if (value.metadata === void 0 || isRecord3(value.metadata) && Object.keys(value.metadata).length === 0) {
6051
+ if (value.metadata === void 0 || isRecord5(value.metadata) && Object.keys(value.metadata).length === 0) {
4955
6052
  importable.push({ integrationId, key: value.key });
4956
6053
  } else {
4957
6054
  unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
@@ -4964,7 +6061,7 @@ function statusFromRequestError(error2) {
4964
6061
  const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
4965
6062
  return match ? Number(match[1]) : void 0;
4966
6063
  }
4967
- function failureMessage(status2) {
6064
+ function failureMessage2(status2) {
4968
6065
  return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
4969
6066
  }
4970
6067
  function isRetryableStatus(status2) {
@@ -4991,18 +6088,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
4991
6088
  const failure = {
4992
6089
  integrationId: credential.integrationId,
4993
6090
  status: response.status,
4994
- message: failureMessage(response.status)
6091
+ message: failureMessage2(response.status)
4995
6092
  };
4996
- log3(`${failureMessage(failure.status)} for ${failure.integrationId}`, "warn");
6093
+ log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
4997
6094
  return { ok: false, failure };
4998
6095
  } catch (error2) {
4999
6096
  const status2 = statusFromRequestError(error2);
5000
6097
  const failure = {
5001
6098
  integrationId: credential.integrationId,
5002
6099
  ...status2 === void 0 ? {} : { status: status2 },
5003
- message: failureMessage(status2)
6100
+ message: failureMessage2(status2)
5004
6101
  };
5005
- log3(`${failureMessage(status2)} for ${failure.integrationId}`, "warn");
6102
+ log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
5006
6103
  return { ok: false, failure };
5007
6104
  }
5008
6105
  }
@@ -5028,10 +6125,10 @@ async function importOpenCodeCredentials({
5028
6125
  return result;
5029
6126
  }
5030
6127
  function parseIntegrationList(value) {
5031
- if (!isRecord3(value) || !Array.isArray(value.data)) return null;
6128
+ if (!isRecord5(value) || !Array.isArray(value.data)) return null;
5032
6129
  const integrationIds = /* @__PURE__ */ new Set();
5033
6130
  for (const integration of value.data) {
5034
- if (!isRecord3(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
6131
+ if (!isRecord5(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
5035
6132
  return null;
5036
6133
  }
5037
6134
  if (integration.connections.length > 0) integrationIds.add(integration.id);
@@ -5116,87 +6213,9 @@ async function importOpenCodeV2Credentials({
5116
6213
 
5117
6214
  // src/lib/opencode/session-db-boot.ts
5118
6215
  import { spawn as spawn2 } from "node:child_process";
5119
- import { mkdirSync as mkdirSync2, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
6216
+ import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
5120
6217
  import { homedir as homedir4 } from "node:os";
5121
- import { dirname as dirname5, resolve as resolvePath } from "node:path";
5122
-
5123
- // src/lib/runner-synchroniser.ts
5124
- import { spawn } from "node:child_process";
5125
- function appendError(stderr, error2) {
5126
- const message = error2 instanceof Error ? error2.message : String(error2);
5127
- return stderr === "" ? message : `${stderr}
5128
- ${message}`;
5129
- }
5130
- function runSynchroniser(args, opts) {
5131
- return new Promise((resolve5) => {
5132
- let child;
5133
- let stdout = "";
5134
- let stderr = "";
5135
- let settled = false;
5136
- const timer = {};
5137
- let abortListener;
5138
- let spawnListener;
5139
- const finish = (result) => {
5140
- if (settled) return;
5141
- settled = true;
5142
- if (timer.handle) clearTimeout(timer.handle);
5143
- if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
5144
- if (spawnListener) child.removeListener("spawn", spawnListener);
5145
- resolve5(result);
5146
- };
5147
- try {
5148
- child = spawn("runner-synchroniser", args, {
5149
- env: opts.env ?? process.env,
5150
- stdio: ["ignore", "pipe", "pipe"]
5151
- });
5152
- } catch (error2) {
5153
- finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
5154
- return;
5155
- }
5156
- child.stdout?.setEncoding("utf8");
5157
- child.stdout?.on("data", (chunk) => {
5158
- stdout += chunk;
5159
- });
5160
- child.stderr?.setEncoding("utf8");
5161
- child.stderr?.on("data", (chunk) => {
5162
- stderr += chunk;
5163
- });
5164
- child.once("error", (error2) => {
5165
- finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
5166
- });
5167
- child.once("close", (code) => {
5168
- finish({ code, stdout, stderr, timedOut: false });
5169
- });
5170
- if (opts.signal) {
5171
- const killChild = () => {
5172
- if (child.pid === void 0) {
5173
- if (!spawnListener) {
5174
- spawnListener = killChild;
5175
- child.once("spawn", spawnListener);
5176
- }
5177
- return;
5178
- }
5179
- child.kill("SIGKILL");
5180
- };
5181
- abortListener = killChild;
5182
- if (opts.signal.aborted) {
5183
- abortListener();
5184
- } else {
5185
- opts.signal.addEventListener("abort", abortListener, { once: true });
5186
- if (opts.signal.aborted) abortListener();
5187
- }
5188
- }
5189
- timer.handle = setTimeout(
5190
- () => {
5191
- child.kill("SIGKILL");
5192
- finish({ code: null, stdout, stderr, timedOut: true });
5193
- },
5194
- Math.max(0, opts.timeoutMs)
5195
- );
5196
- });
5197
- }
5198
-
5199
- // src/lib/opencode/session-db-boot.ts
6218
+ import { dirname as dirname7, resolve as resolvePath } from "node:path";
5200
6219
  var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
5201
6220
  var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
5202
6221
  var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
@@ -5246,8 +6265,8 @@ function clearMarker(options) {
5246
6265
  function markNoReplicate(options, message) {
5247
6266
  if (options.noReplicateMarker) {
5248
6267
  try {
5249
- mkdirSync2(dirname5(options.noReplicateMarker), { recursive: true });
5250
- writeFileSync2(options.noReplicateMarker, "");
6268
+ mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
6269
+ writeFileSync4(options.noReplicateMarker, "");
5251
6270
  } catch (error2) {
5252
6271
  options.log(
5253
6272
  `Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5401,8 +6420,8 @@ async function ensureLitestreamConfig(options, env) {
5401
6420
  return null;
5402
6421
  }
5403
6422
  try {
5404
- mkdirSync2(dirname5(configPath), { recursive: true });
5405
- writeFileSync2(configPath, rendered.stdout);
6423
+ mkdirSync4(dirname7(configPath), { recursive: true });
6424
+ writeFileSync4(configPath, rendered.stdout);
5406
6425
  } catch (error2) {
5407
6426
  options.log(
5408
6427
  `Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5667,8 +6686,8 @@ async function restoreAndVerifySessionDb(options) {
5667
6686
 
5668
6687
  // src/lib/opencode/session-db-provenance.ts
5669
6688
  import { createRequire } from "node:module";
5670
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5671
- import { dirname as dirname6, join as join7 } from "node:path";
6689
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
6690
+ import { dirname as dirname8, join as join9 } from "node:path";
5672
6691
  var require2 = createRequire(import.meta.url);
5673
6692
  function readSessionDbMigrationIds(dbPath) {
5674
6693
  let db;
@@ -5705,12 +6724,12 @@ function readSessionDbMigrationIds(dbPath) {
5705
6724
  }
5706
6725
  function sessionDbProvenanceStatePath(homeDir, env) {
5707
6726
  const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
5708
- return override || join7(homeDir, ".local", "state", "evident", "session-db-provenance.json");
6727
+ return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5709
6728
  }
5710
6729
  function loadSessionDbProvenanceState(path) {
5711
6730
  let value;
5712
6731
  try {
5713
- value = JSON.parse(readFileSync6(path, "utf8"));
6732
+ value = JSON.parse(readFileSync9(path, "utf8"));
5714
6733
  } catch (error2) {
5715
6734
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
5716
6735
  console.error(
@@ -5734,8 +6753,8 @@ function loadSessionDbProvenanceState(path) {
5734
6753
  }
5735
6754
  function saveSessionDbProvenanceState(path, state) {
5736
6755
  try {
5737
- mkdirSync3(dirname6(path), { recursive: true });
5738
- writeFileSync3(path, `${JSON.stringify(state, null, 2)}
6756
+ mkdirSync5(dirname8(path), { recursive: true });
6757
+ writeFileSync5(path, `${JSON.stringify(state, null, 2)}
5739
6758
  `, "utf8");
5740
6759
  } catch (error2) {
5741
6760
  console.error(
@@ -5790,6 +6809,26 @@ function isSessionDbProvenanceRecord(value) {
5790
6809
  return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
5791
6810
  }
5792
6811
 
6812
+ // src/lib/opencode/detect.ts
6813
+ function describeProbe(route, result) {
6814
+ if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
6815
+ if (result.error) return `${route}: no HTTP response (${result.error})`;
6816
+ return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
6817
+ }
6818
+ async function classifyRunningOpenCode(port) {
6819
+ const v1 = await checkOpenCodeHealth(port);
6820
+ if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
6821
+ const v2 = await checkOpenCode2Health(port);
6822
+ if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
6823
+ const detail = [
6824
+ describeProbe("V1 /global/health", v1),
6825
+ describeProbe("V2 /api/info (or /api/health fallback)", v2)
6826
+ ].join("; ");
6827
+ if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
6828
+ if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
6829
+ return { kind: "no-server-identified", detail };
6830
+ }
6831
+
5793
6832
  // src/lib/opencode/opencode2-package.ts
5794
6833
  var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
5795
6834
 
@@ -6098,26 +7137,28 @@ async function startOpenCode(port, options = {}) {
6098
7137
  }
6099
7138
  async function startOpenCode2(port, options = {}) {
6100
7139
  const password = randomBytes(24).toString("hex");
6101
- let command = "opencode2";
7140
+ let command = options.command ?? "opencode2";
6102
7141
  const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
6103
7142
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
6104
- try {
6105
- execSync("which opencode2", { stdio: "ignore" });
6106
- } catch {
6107
- command = "npx";
6108
- args = [
6109
- "-y",
6110
- "-p",
6111
- OPENCODE2_PACKAGE_SPEC,
6112
- "--",
6113
- "opencode2",
6114
- "serve",
6115
- "--port",
6116
- port.toString(),
6117
- "--hostname",
6118
- "127.0.0.1",
6119
- ...logLevel
6120
- ];
7143
+ if (options.command === void 0) {
7144
+ try {
7145
+ execSync("which opencode2", { stdio: "ignore" });
7146
+ } catch {
7147
+ command = "npx";
7148
+ args = [
7149
+ "-y",
7150
+ "-p",
7151
+ OPENCODE2_PACKAGE_SPEC,
7152
+ "--",
7153
+ "opencode2",
7154
+ "serve",
7155
+ "--port",
7156
+ port.toString(),
7157
+ "--hostname",
7158
+ "127.0.0.1",
7159
+ ...logLevel
7160
+ ];
7161
+ }
6121
7162
  }
6122
7163
  const child = spawn3(command, args, {
6123
7164
  env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
@@ -6174,6 +7215,56 @@ function isOpenCode2Installed() {
6174
7215
  return false;
6175
7216
  }
6176
7217
  }
7218
+ function readVersionOutput(command) {
7219
+ try {
7220
+ return execSync2(`${command} --version`, {
7221
+ encoding: "utf-8",
7222
+ stdio: ["ignore", "pipe", "pipe"]
7223
+ }).trim();
7224
+ } catch (error2) {
7225
+ const output = typeof error2 === "object" && error2 !== null ? [
7226
+ "stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
7227
+ "stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
7228
+ ].filter(Boolean).join("\n") : "";
7229
+ return output || (error2 instanceof Error ? error2.message : String(error2));
7230
+ }
7231
+ }
7232
+ function parseOpenCodeVersion(rawVersion) {
7233
+ const output = rawVersion.trim();
7234
+ const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
7235
+ const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
7236
+ if (prefix === "opencode2") return { major: "v2", version: version2 };
7237
+ const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
7238
+ if (!major) return null;
7239
+ if (Number(major) === 1) return { major: "v1", version: version2 };
7240
+ if (Number(major) >= 2) return { major: "v2", version: version2 };
7241
+ return null;
7242
+ }
7243
+ function resolveInstalledOpenCode() {
7244
+ let opencode2Available = false;
7245
+ if (isOpenCodeInstalled()) {
7246
+ const rawVersion = readVersionOutput("opencode");
7247
+ const parsed = parseOpenCodeVersion(rawVersion);
7248
+ if (parsed?.major === "v1") {
7249
+ return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
7250
+ }
7251
+ if (parsed?.major === "v2") {
7252
+ return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
7253
+ }
7254
+ opencode2Available = isOpenCode2Installed();
7255
+ if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
7256
+ }
7257
+ if (opencode2Available || isOpenCode2Installed()) {
7258
+ const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
7259
+ return {
7260
+ kind: "installed",
7261
+ major: "v2",
7262
+ command: "opencode2",
7263
+ version: parsed?.version
7264
+ };
7265
+ }
7266
+ return { kind: "none" };
7267
+ }
6177
7268
  async function promptOpenCodeInstall(interactive) {
6178
7269
  if (!interactive) {
6179
7270
  console.log(
@@ -6244,154 +7335,47 @@ async function promptOpenCodeInstall(interactive) {
6244
7335
  value: "exit",
6245
7336
  description: "Exit now and run the command again later"
6246
7337
  }
6247
- ]
6248
- });
6249
- if (afterInstall === "continue") {
6250
- if (isOpenCodeInstalled()) {
6251
- console.log(chalk4.green("\n\u2713 OpenCode detected!"));
6252
- return "installed";
6253
- } else {
6254
- console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
6255
- console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
6256
- const proceed = await select({
6257
- message: "Continue anyway?",
6258
- choices: [
6259
- { name: "Yes, continue", value: "continue" },
6260
- { name: "No, exit", value: "exit" }
6261
- ]
6262
- });
6263
- return proceed === "continue" ? "continue" : "exit";
6264
- }
6265
- }
6266
- return "exit";
6267
- }
6268
- return action;
6269
- }
6270
-
6271
- // src/lib/opencode/provider-check.ts
6272
- function buildNoProviderWarning(hasProvider) {
6273
- if (hasProvider !== false) return null;
6274
- 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).";
6275
- }
6276
-
6277
- // src/lib/opencode/session-cleanup.ts
6278
- var DURATION_UNIT_MS = {
6279
- s: 1e3,
6280
- m: 60 * 1e3,
6281
- h: 60 * 60 * 1e3,
6282
- d: 24 * 60 * 60 * 1e3
6283
- };
6284
- function parseDurationMs(input) {
6285
- const trimmed = input.trim();
6286
- const match = /^(\d+)([smhd])$/.exec(trimmed);
6287
- if (!match) {
6288
- throw new Error(
6289
- `Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
6290
- );
6291
- }
6292
- const value = Number(match[1]);
6293
- if (value <= 0) {
6294
- throw new Error(`Invalid duration "${input}": must be a positive value.`);
6295
- }
6296
- return value * DURATION_UNIT_MS[match[2]];
6297
- }
6298
- function selectSessionsToDelete(sessions, opts) {
6299
- const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
6300
- if (maxAgeMs === void 0 && maxCount === void 0) return [];
6301
- const ageEligible = (s) => {
6302
- if (maxAgeMs === void 0) return false;
6303
- if (s.lastActivityMs === null) return true;
6304
- return nowMs - s.lastActivityMs > maxAgeMs;
6305
- };
6306
- const countEligibleIds = /* @__PURE__ */ new Set();
6307
- if (maxCount !== void 0) {
6308
- const byActivityDesc = [...sessions].sort(
6309
- (a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
6310
- );
6311
- for (const s of byActivityDesc.slice(maxCount)) {
6312
- countEligibleIds.add(s.id);
6313
- }
6314
- }
6315
- const toDelete = [];
6316
- for (const s of sessions) {
6317
- if (protectedIds.has(s.id)) continue;
6318
- if (ageEligible(s) || countEligibleIds.has(s.id)) {
6319
- toDelete.push(s.id);
6320
- }
6321
- }
6322
- const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
6323
- return toDelete.sort(
6324
- (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
6325
- );
6326
- }
6327
- var DEFAULT_INTERVAL = "1h";
6328
- function resolve2(flag, envValue, fallback) {
6329
- return flag ?? envValue ?? fallback;
6330
- }
6331
- function parseMaxCount(input) {
6332
- const trimmed = input.trim();
6333
- if (!/^\d+$/.test(trimmed)) {
6334
- throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
6335
- }
6336
- const value = Number(trimmed);
6337
- if (value <= 0) {
6338
- throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
6339
- }
6340
- return value;
6341
- }
6342
- function resolveSessionCleanupConfig(flags, env = process.env) {
6343
- const warnings = [];
6344
- const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
6345
- const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
6346
- const intervalRaw = resolve2(
6347
- flags.interval,
6348
- env.EVIDENT_SESSION_CLEANUP_INTERVAL,
6349
- DEFAULT_INTERVAL
6350
- );
6351
- let maxAgeMs;
6352
- if (maxAgeRaw !== void 0) {
6353
- try {
6354
- maxAgeMs = parseDurationMs(maxAgeRaw);
6355
- } catch (err) {
6356
- warnings.push(
6357
- `Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
6358
- );
6359
- }
6360
- }
6361
- let maxCount;
6362
- if (maxCountRaw !== void 0) {
6363
- try {
6364
- maxCount = parseMaxCount(maxCountRaw);
6365
- } catch (err) {
6366
- warnings.push(
6367
- `Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
6368
- );
7338
+ ]
7339
+ });
7340
+ if (afterInstall === "continue") {
7341
+ if (isOpenCodeInstalled()) {
7342
+ console.log(chalk4.green("\n\u2713 OpenCode detected!"));
7343
+ return "installed";
7344
+ } else {
7345
+ console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
7346
+ console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
7347
+ const proceed = await select({
7348
+ message: "Continue anyway?",
7349
+ choices: [
7350
+ { name: "Yes, continue", value: "continue" },
7351
+ { name: "No, exit", value: "exit" }
7352
+ ]
7353
+ });
7354
+ return proceed === "continue" ? "continue" : "exit";
7355
+ }
6369
7356
  }
7357
+ return "exit";
6370
7358
  }
6371
- let intervalMs;
6372
- try {
6373
- intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
6374
- } catch (err) {
6375
- warnings.push(
6376
- `Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
6377
- );
6378
- intervalMs = parseDurationMs(DEFAULT_INTERVAL);
6379
- }
6380
- const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
6381
- return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
7359
+ return action;
7360
+ }
7361
+
7362
+ // src/lib/opencode/provider-check.ts
7363
+ function buildNoProviderWarning(hasProvider) {
7364
+ if (hasProvider !== false) return null;
7365
+ 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).";
6382
7366
  }
6383
7367
 
6384
7368
  // src/lib/opencode/session-db-size.ts
6385
7369
  import { statSync as statSync4 } from "node:fs";
6386
- import { join as join8 } from "node:path";
7370
+ import { join as join10 } from "node:path";
6387
7371
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
6388
7372
  function statSessionDbBytes(homeDir) {
6389
- const dbPath = join8(homeDir, ".local", "share", "opencode", "opencode.db");
7373
+ const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
6390
7374
  try {
6391
7375
  return statSync4(dbPath).size;
6392
7376
  } catch (err) {
6393
- const isMissingFile2 = err instanceof Error && "code" in err && err.code === "ENOENT";
6394
- if (!isMissingFile2) {
7377
+ const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
7378
+ if (!isMissingFile4) {
6395
7379
  console.error(
6396
7380
  `[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
6397
7381
  );
@@ -6416,12 +7400,12 @@ function buildSessionStoreSizeWarning(input) {
6416
7400
  // src/lib/opencode/log-tail.ts
6417
7401
  import { statSync as statSync5 } from "node:fs";
6418
7402
  import { homedir as homedir5 } from "node:os";
6419
- import { join as join9 } from "node:path";
7403
+ import { join as join11 } from "node:path";
6420
7404
  import { open as open2, stat } from "node:fs/promises";
6421
7405
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
6422
7406
  function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
6423
- const dataDir = env.XDG_DATA_HOME || join9(homeDir, ".local", "share");
6424
- return join9(dataDir, "opencode", "log", "opencode.log");
7407
+ const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
7408
+ return join11(dataDir, "opencode", "log", "opencode.log");
6425
7409
  }
6426
7410
  function isEnoent(error2) {
6427
7411
  return error2?.code === "ENOENT";
@@ -6906,8 +7890,8 @@ function connectTunnel(options) {
6906
7890
  }
6907
7891
  forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
6908
7892
  } catch (error2) {
6909
- const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
6910
- onError?.(`Failed to handle binary message: ${errorMessage3}`);
7893
+ const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
7894
+ onError?.(`Failed to handle binary message: ${errorMessage4}`);
6911
7895
  }
6912
7896
  return;
6913
7897
  }
@@ -6915,8 +7899,8 @@ function connectTunnel(options) {
6915
7899
  try {
6916
7900
  message = JSON.parse(data.toString());
6917
7901
  } catch (error2) {
6918
- const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
6919
- onError?.(`Failed to handle message: ${errorMessage3}`);
7902
+ const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
7903
+ onError?.(`Failed to handle message: ${errorMessage4}`);
6920
7904
  return;
6921
7905
  }
6922
7906
  if (isStreamFrame(message)) {
@@ -7088,10 +8072,10 @@ var RunnerConnection = class _RunnerConnection {
7088
8072
  };
7089
8073
 
7090
8074
  // src/lib/tunnel/ready-marker.ts
7091
- import { writeFileSync as writeFileSync4 } from "node:fs";
8075
+ import { writeFileSync as writeFileSync6 } from "node:fs";
7092
8076
  function writeTunnelReadyMarker(path, agentId) {
7093
8077
  try {
7094
- writeFileSync4(path, `${agentId}
8078
+ writeFileSync6(path, `${agentId}
7095
8079
  `);
7096
8080
  return { ok: true };
7097
8081
  } catch (error2) {
@@ -7116,7 +8100,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
7116
8100
  }
7117
8101
 
7118
8102
  // src/lib/process-liveness.ts
7119
- import { readFileSync as readFileSync7 } from "node:fs";
8103
+ import { readFileSync as readFileSync10 } from "node:fs";
7120
8104
  function isProcessAlive(pid) {
7121
8105
  try {
7122
8106
  process.kill(pid, 0);
@@ -7131,7 +8115,7 @@ function isProcessAlive(pid) {
7131
8115
  }
7132
8116
  if (process.platform !== "linux") return true;
7133
8117
  try {
7134
- const status2 = readFileSync7(`/proc/${pid}/status`, "utf8");
8118
+ const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
7135
8119
  return !/^State:\s+Z(?:\s|$)/m.test(status2);
7136
8120
  } catch (error2) {
7137
8121
  console.error(
@@ -7198,8 +8182,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
7198
8182
  }
7199
8183
 
7200
8184
  // src/lib/resource-usage-reporting.ts
7201
- var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
7202
- var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
8185
+ var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
8186
+ var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
7203
8187
  function resolveResourceUsageReportingEnabled(flagValue, env) {
7204
8188
  if (flagValue === false) {
7205
8189
  return { enabled: false, warnings: [] };
@@ -7209,10 +8193,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
7209
8193
  return { enabled: true, warnings: [] };
7210
8194
  }
7211
8195
  const normalized = raw.trim().toLowerCase();
7212
- if (DISABLED_VALUES.has(normalized)) {
8196
+ if (DISABLED_VALUES2.has(normalized)) {
7213
8197
  return { enabled: false, warnings: [] };
7214
8198
  }
7215
- if (ENABLED_VALUES.has(normalized)) {
8199
+ if (ENABLED_VALUES2.has(normalized)) {
7216
8200
  return { enabled: true, warnings: [] };
7217
8201
  }
7218
8202
  return {
@@ -7490,12 +8474,12 @@ function createSessionStoreHealth(options = {}) {
7490
8474
  import { homedir as homedir6 } from "node:os";
7491
8475
 
7492
8476
  // src/lib/runner-file-sync.ts
7493
- import { join as join11 } from "node:path";
8477
+ import { join as join13 } from "node:path";
7494
8478
 
7495
8479
  // src/lib/file-push.ts
7496
8480
  import { randomUUID } from "node:crypto";
7497
8481
  import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
7498
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
8482
+ import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
7499
8483
  var FILE_MODE = 384;
7500
8484
  var DIRECTORY_MODE = 448;
7501
8485
  async function writePushedFile(request3) {
@@ -7526,9 +8510,9 @@ async function writePushedFile(request3) {
7526
8510
  }
7527
8511
  try {
7528
8512
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
7529
- dirname7(candidate)
8513
+ dirname9(candidate)
7530
8514
  );
7531
- const realTarget = join10(existingAncestor, ...missingSegments, basename2(candidate));
8515
+ const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
7532
8516
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
7533
8517
  if (allowedDirectory === null) {
7534
8518
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -7538,8 +8522,8 @@ async function writePushedFile(request3) {
7538
8522
  }
7539
8523
  if (missingSegments.length > 0) {
7540
8524
  await createMissingDirectories(existingAncestor, missingSegments);
7541
- const realParent = await realpath(dirname7(realTarget));
7542
- if (realParent !== dirname7(realTarget) || !contains(allowedDirectory, realTarget)) {
8525
+ const realParent = await realpath(dirname9(realTarget));
8526
+ if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
7543
8527
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7544
8528
  path: realTarget,
7545
8529
  bytes,
@@ -7564,7 +8548,7 @@ function expandAndValidate(requestedPath, homeDir) {
7564
8548
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
7565
8549
  return null;
7566
8550
  }
7567
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
8551
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
7568
8552
  if (expanded.split(/[/\\]/).includes("..")) {
7569
8553
  return null;
7570
8554
  }
@@ -7582,7 +8566,7 @@ async function resolveNearestExistingAncestor(directory) {
7582
8566
  try {
7583
8567
  return { existingAncestor: await realpath(current), missingSegments };
7584
8568
  } catch (err) {
7585
- const parent = dirname7(current);
8569
+ const parent = dirname9(current);
7586
8570
  if (err.code !== "ENOENT" || parent === current) {
7587
8571
  throw err;
7588
8572
  }
@@ -7613,8 +8597,8 @@ async function findMatchingAllowedFile(allowedFiles, realTarget) {
7613
8597
  });
7614
8598
  continue;
7615
8599
  }
7616
- const realParent = await realpathCreatingIfMissing(dirname7(entry));
7617
- if (realParent !== null && join10(realParent, basename2(entry)) === realTarget) {
8600
+ const realParent = await realpathCreatingIfMissing(dirname9(entry));
8601
+ if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
7618
8602
  return realParent;
7619
8603
  }
7620
8604
  }
@@ -7653,13 +8637,13 @@ function contains(realDirectory, realTarget) {
7653
8637
  async function createMissingDirectories(existingAncestor, missingSegments) {
7654
8638
  let current = existingAncestor;
7655
8639
  for (const segment of missingSegments) {
7656
- current = join10(current, segment);
8640
+ current = join12(current, segment);
7657
8641
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
7658
8642
  await chmod(current, DIRECTORY_MODE);
7659
8643
  }
7660
8644
  }
7661
8645
  async function writeAtomically(realTarget, content) {
7662
- const temporaryPath = join10(dirname7(realTarget), `.evident-push-${randomUUID()}.tmp`);
8646
+ const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
7663
8647
  let handle;
7664
8648
  try {
7665
8649
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -7789,12 +8773,12 @@ var NOT_APPLIED = {
7789
8773
  opencodeAuthApplied: false
7790
8774
  };
7791
8775
  function isClaudeCredentialPath(requestedPath, homeDir) {
7792
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7793
- return expanded === join11(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
8776
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
8777
+ return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7794
8778
  }
7795
8779
  function isOpenCodeAuthPath(requestedPath, homeDir) {
7796
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7797
- return expanded === join11(homeDir, ...OPENCODE_AUTH_SEGMENTS);
8780
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
8781
+ return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7798
8782
  }
7799
8783
  async function applyOne(options, file) {
7800
8784
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -7933,7 +8917,7 @@ function describe(err) {
7933
8917
  }
7934
8918
 
7935
8919
  // src/lib/vault-file-sync.ts
7936
- import { createHash } from "node:crypto";
8920
+ import { createHash as createHash2 } from "node:crypto";
7937
8921
  async function syncVaultFiles(options) {
7938
8922
  const files = await listVaultFiles(options);
7939
8923
  if (files === null) return { applied: 0 };
@@ -8029,7 +9013,7 @@ async function reconcileVaultFile(options, file) {
8029
9013
  }
8030
9014
  const content = await downloadVaultFile(options, file);
8031
9015
  if (content === null) return false;
8032
- const actualSha256 = createHash("sha256").update(content).digest("hex");
9016
+ const actualSha256 = createHash2("sha256").update(content).digest("hex");
8033
9017
  if (actualSha256 !== file.content_sha256) {
8034
9018
  options.log({
8035
9019
  level: "warn",
@@ -8230,6 +9214,7 @@ var ChannelDriver = class _ChannelDriver {
8230
9214
  conversationFilter;
8231
9215
  retry;
8232
9216
  log;
9217
+ recordBinding;
8233
9218
  fetchImpl;
8234
9219
  sleep;
8235
9220
  pausedPollIntervalMs;
@@ -8592,6 +9577,7 @@ var ChannelDriver = class _ChannelDriver {
8592
9577
  this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
8593
9578
  this.log = config.log ?? (() => {
8594
9579
  });
9580
+ this.recordBinding = config.recordBinding;
8595
9581
  this.fetchImpl = withRequestTimeout(
8596
9582
  config.fetchImpl ?? fetch,
8597
9583
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
@@ -8779,6 +9765,9 @@ var ChannelDriver = class _ChannelDriver {
8779
9765
  let cappedSkips = 0;
8780
9766
  for (const conv of conversations) {
8781
9767
  if (this.stopped) break;
9768
+ if (conv.opencode_session_id) {
9769
+ this.recordSessionBinding(conv.id, conv.opencode_session_id);
9770
+ }
8782
9771
  if (this.maxActiveSessions !== void 0) {
8783
9772
  const activeSessionIds = this.activeSessionIdsForCap();
8784
9773
  const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
@@ -8861,12 +9850,12 @@ var ChannelDriver = class _ChannelDriver {
8861
9850
  this.sessions.delete(conversationId);
8862
9851
  this.supersede(conversationId, sessionId);
8863
9852
  const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
8864
- const errorMessage3 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
8865
- this.untrackableAck.set(messageId, errorMessage3);
9853
+ const errorMessage4 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
9854
+ this.untrackableAck.set(messageId, errorMessage4);
8866
9855
  this.untrackableAckSessionIds.set(messageId, null);
8867
9856
  this.log({
8868
9857
  level: "error",
8869
- message: errorMessage3,
9858
+ message: errorMessage4,
8870
9859
  conversation_id: conversationId,
8871
9860
  message_id: messageId
8872
9861
  });
@@ -8874,7 +9863,7 @@ var ChannelDriver = class _ChannelDriver {
8874
9863
  conversationId,
8875
9864
  messageId,
8876
9865
  null,
8877
- errorMessage3
9866
+ errorMessage4
8878
9867
  );
8879
9868
  if (reportError !== null) {
8880
9869
  this.log({
@@ -9055,12 +10044,12 @@ var ChannelDriver = class _ChannelDriver {
9055
10044
  conversation_id: conv.id,
9056
10045
  message_id: message.id
9057
10046
  });
9058
- const errorMessage3 = this.untrackableAck.get(message.id);
10047
+ const errorMessage4 = this.untrackableAck.get(message.id);
9059
10048
  const reportError = await this.reportUntrackableAck(
9060
10049
  conv.id,
9061
10050
  message.id,
9062
10051
  null,
9063
- errorMessage3
10052
+ errorMessage4
9064
10053
  );
9065
10054
  if (reportError === null) {
9066
10055
  continue;
@@ -9148,7 +10137,7 @@ var ChannelDriver = class _ChannelDriver {
9148
10137
  const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
9149
10138
  const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
9150
10139
  const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
9151
- const errorMessage3 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
10140
+ const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
9152
10141
  this.log({
9153
10142
  level: "error",
9154
10143
  message: `Refusing message ${message.id.slice(0, 8)} in conversation ${conv.id.slice(0, 8)}: provider namespace ${providerId ?? "(unknown)"} is not loaded; no turn was started`,
@@ -9156,7 +10145,7 @@ var ChannelDriver = class _ChannelDriver {
9156
10145
  message_id: message.id
9157
10146
  });
9158
10147
  try {
9159
- await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
10148
+ await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
9160
10149
  classification: {
9161
10150
  kind: "model_auth",
9162
10151
  providerId,
@@ -9201,7 +10190,13 @@ var ChannelDriver = class _ChannelDriver {
9201
10190
  false
9202
10191
  );
9203
10192
  }
9204
- opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
10193
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(
10194
+ this.openCodeClient,
10195
+ sessionId,
10196
+ message.content,
10197
+ options,
10198
+ () => this.logInvalidV2ModelSelection(conv.id, message.id)
10199
+ ) : await this.dispatchLocked(
9205
10200
  sessionId,
9206
10201
  () => sendPromptAsync(
9207
10202
  this.port,
@@ -9228,12 +10223,12 @@ var ChannelDriver = class _ChannelDriver {
9228
10223
  } catch (err) {
9229
10224
  if (err instanceof ChannelAuthError) throw err;
9230
10225
  if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
9231
- const errorMessage4 = err instanceof Error ? err.message : String(err);
9232
- this.untrackableAck.set(message.id, errorMessage4);
10226
+ const errorMessage5 = err instanceof Error ? err.message : String(err);
10227
+ this.untrackableAck.set(message.id, errorMessage5);
9233
10228
  this.untrackableAckSessionIds.set(message.id, null);
9234
10229
  this.log({
9235
10230
  level: "error",
9236
- message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
10231
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
9237
10232
  conversation_id: conv.id,
9238
10233
  message_id: message.id
9239
10234
  });
@@ -9241,7 +10236,7 @@ var ChannelDriver = class _ChannelDriver {
9241
10236
  conv.id,
9242
10237
  message.id,
9243
10238
  null,
9244
- errorMessage4
10239
+ errorMessage5
9245
10240
  );
9246
10241
  if (markError !== null) {
9247
10242
  this.log({
@@ -9276,7 +10271,11 @@ var ChannelDriver = class _ChannelDriver {
9276
10271
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
9277
10272
  break;
9278
10273
  }
9279
- const errorMessage3 = err instanceof Error ? err.message : String(err);
10274
+ if (this.isV2 && err instanceof OpenCodeV2ModelSwitchError) {
10275
+ await this.failV2ModelSwitch(conv, message, sessionId, err);
10276
+ break;
10277
+ }
10278
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
9280
10279
  this.sessions.delete(conv.id);
9281
10280
  this.supersede(conv.id, sessionId);
9282
10281
  this.log({
@@ -9285,7 +10284,7 @@ var ChannelDriver = class _ChannelDriver {
9285
10284
  conversation_id: conv.id,
9286
10285
  message_id: message.id
9287
10286
  });
9288
- await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
10287
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
9289
10288
  this.log({
9290
10289
  level: "warn",
9291
10290
  message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -9296,7 +10295,7 @@ var ChannelDriver = class _ChannelDriver {
9296
10295
  });
9297
10296
  this.log({
9298
10297
  level: "error",
9299
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
10298
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
9300
10299
  conversation_id: conv.id,
9301
10300
  message_id: message.id
9302
10301
  });
@@ -9869,6 +10868,14 @@ var ChannelDriver = class _ChannelDriver {
9869
10868
  this.dispatchNotStartedSignalled.set(message.id, branch);
9870
10869
  void this.postSignal(conv.id, message.id, "dispatch_not_started", { branch });
9871
10870
  }
10871
+ logInvalidV2ModelSelection(conversationId, messageId) {
10872
+ this.log({
10873
+ level: "warn",
10874
+ message: `Skipping an invalid OpenCode V2 model selection for message ${messageId.slice(0, 8)}; expected provider/model format`,
10875
+ conversation_id: conversationId,
10876
+ message_id: messageId
10877
+ });
10878
+ }
9872
10879
  /**
9873
10880
  * Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
9874
10881
  * but its own PATCH to record it failed. Fires at most once per (message,
@@ -10083,6 +11090,7 @@ var ChannelDriver = class _ChannelDriver {
10083
11090
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
10084
11091
  }
10085
11092
  this.sessions.set(conv.id, bound);
11093
+ this.recordSessionBinding(conv.id, bound);
10086
11094
  return { sessionId: bound, created: false };
10087
11095
  }
10088
11096
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
@@ -10096,6 +11104,7 @@ var ChannelDriver = class _ChannelDriver {
10096
11104
  const directory = await this.resolveOpenCodeDirectory();
10097
11105
  const sessionId = await this.createOpenCodeSession(directory);
10098
11106
  this.sessions.set(conversationId, sessionId);
11107
+ this.recordSessionBinding(conversationId, sessionId);
10099
11108
  await this.persistSession(conversationId, sessionId).catch((err) => {
10100
11109
  this.log({
10101
11110
  level: "warn",
@@ -10105,6 +11114,18 @@ var ChannelDriver = class _ChannelDriver {
10105
11114
  });
10106
11115
  return sessionId;
10107
11116
  }
11117
+ recordSessionBinding(conversationId, sessionId) {
11118
+ if (!this.recordBinding) return;
11119
+ try {
11120
+ this.recordBinding(conversationId, sessionId);
11121
+ } catch (error2) {
11122
+ this.log({
11123
+ level: "warn",
11124
+ message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
11125
+ conversation_id: conversationId
11126
+ });
11127
+ }
11128
+ }
10108
11129
  /**
10109
11130
  * Lazily resolve (and cache) opencode's root directory via the selected client's
10110
11131
  * location lookup.
@@ -12205,12 +13226,12 @@ var ChannelDriver = class _ChannelDriver {
12205
13226
  conversation_id: row.conversation_id,
12206
13227
  message_id: row.id
12207
13228
  });
12208
- const errorMessage3 = this.untrackableAck.get(row.id);
13229
+ const errorMessage4 = this.untrackableAck.get(row.id);
12209
13230
  const reportError = await this.reportUntrackableAck(
12210
13231
  row.conversation_id,
12211
13232
  row.id,
12212
13233
  this.untrackableAckSessionIds.get(row.id) ?? null,
12213
- errorMessage3
13234
+ errorMessage4
12214
13235
  );
12215
13236
  if (reportError !== null) {
12216
13237
  this.log({
@@ -12495,7 +13516,13 @@ var ChannelDriver = class _ChannelDriver {
12495
13516
  let ocId;
12496
13517
  let preDispatchUserIds = null;
12497
13518
  try {
12498
- ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
13519
+ ocId = this.isV2 ? await sendV2Prompt(
13520
+ this.openCodeClient,
13521
+ sessionId,
13522
+ row.content,
13523
+ options,
13524
+ () => this.logInvalidV2ModelSelection(row.conversation_id, row.id)
13525
+ ) : await this.dispatchLocked(
12499
13526
  sessionId,
12500
13527
  () => sendPromptAsync(
12501
13528
  this.port,
@@ -12523,14 +13550,14 @@ var ChannelDriver = class _ChannelDriver {
12523
13550
  this.awaitingReadopt.delete(row.id);
12524
13551
  if (err instanceof ChannelAuthError) throw err;
12525
13552
  if (this.isV2) {
12526
- const errorMessage3 = err instanceof Error ? err.message : String(err);
13553
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
12527
13554
  const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
12528
13555
  if (!invalidPromptAck) {
12529
13556
  const exists = await this.sessionExists(sessionId);
12530
13557
  if (exists === false) {
12531
13558
  this.log({
12532
13559
  level: "warn",
12533
- 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}`,
13560
+ 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: ${errorMessage4}`,
12534
13561
  conversation_id: row.conversation_id,
12535
13562
  message_id: row.id
12536
13563
  });
@@ -12540,7 +13567,7 @@ var ChannelDriver = class _ChannelDriver {
12540
13567
  if (exists === null) {
12541
13568
  this.log({
12542
13569
  level: "warn",
12543
- 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}`,
13570
+ 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: ${errorMessage4}`,
12544
13571
  conversation_id: row.conversation_id,
12545
13572
  message_id: row.id
12546
13573
  });
@@ -12548,12 +13575,16 @@ var ChannelDriver = class _ChannelDriver {
12548
13575
  return;
12549
13576
  }
12550
13577
  } else {
12551
- this.untrackableAck.set(row.id, errorMessage3);
13578
+ this.untrackableAck.set(row.id, errorMessage4);
12552
13579
  this.untrackableAckSessionIds.set(row.id, sessionId);
12553
13580
  }
13581
+ if (err instanceof OpenCodeV2ModelSwitchError) {
13582
+ await this.failV2ModelSwitch(readoptConv, readoptMessage, sessionId, err);
13583
+ return;
13584
+ }
12554
13585
  this.log({
12555
13586
  level: "error",
12556
- message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
13587
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
12557
13588
  conversation_id: row.conversation_id,
12558
13589
  message_id: row.id
12559
13590
  });
@@ -12562,7 +13593,7 @@ var ChannelDriver = class _ChannelDriver {
12562
13593
  row.conversation_id,
12563
13594
  row.id,
12564
13595
  sessionId,
12565
- errorMessage3
13596
+ errorMessage4
12566
13597
  );
12567
13598
  if (markError !== null) {
12568
13599
  this.log({
@@ -12573,7 +13604,7 @@ var ChannelDriver = class _ChannelDriver {
12573
13604
  });
12574
13605
  }
12575
13606
  } else {
12576
- await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
13607
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
12577
13608
  (markErr) => {
12578
13609
  this.log({
12579
13610
  level: "warn",
@@ -13557,9 +14588,9 @@ var ChannelDriver = class _ChannelDriver {
13557
14588
  * trackable. Callers use the returned error to keep their path-specific logging
13558
14589
  * while the same report payload can be retried on a later drain.
13559
14590
  */
13560
- async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage3) {
14591
+ async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
13561
14592
  try {
13562
- await this.markFailed(conversationId, messageId, sessionId, errorMessage3);
14593
+ await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
13563
14594
  return null;
13564
14595
  } catch (err) {
13565
14596
  this.signalUntrackableAck(conversationId, messageId);
@@ -13628,6 +14659,38 @@ var ChannelDriver = class _ChannelDriver {
13628
14659
  }
13629
14660
  this.clearSubagentInvocationCaches(messageId);
13630
14661
  }
14662
+ async failV2ModelSwitch(conv, message, sessionId, error2) {
14663
+ const { model } = splitModelVariant(message.opencode_model ?? void 0);
14664
+ const separator = model?.indexOf("/") ?? -1;
14665
+ const classification = error2.statusCode === 404 && model && separator > 0 && separator < model.length - 1 ? {
14666
+ kind: "model_auth",
14667
+ providerId: model.slice(0, separator),
14668
+ modelId: model.slice(separator + 1),
14669
+ reason: "missing"
14670
+ } : null;
14671
+ this.log({
14672
+ level: "error",
14673
+ message: `OpenCode could not apply the selected model for message ${message.id.slice(0, 8)}: ${error2.message}`,
14674
+ conversation_id: conv.id,
14675
+ message_id: message.id
14676
+ });
14677
+ try {
14678
+ await this.markFailed(conv.id, message.id, sessionId, error2.message, void 0, {
14679
+ classification,
14680
+ errorShape: { name: null, statusCode: error2.statusCode }
14681
+ });
14682
+ } catch (markErr) {
14683
+ this.log({
14684
+ level: "warn",
14685
+ message: `markFailed PATCH for V2 model switch failure on message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
14686
+ conversation_id: conv.id,
14687
+ message_id: message.id
14688
+ });
14689
+ this.signalDispatchNotStarted(conv, message, "failure_unreported");
14690
+ return;
14691
+ }
14692
+ if (classification) await this.repairProviderRegistry(conv.id, message.id);
14693
+ }
13631
14694
  /**
13632
14695
  * Classify an errored turn and extract its bounded error shape for `markFailed`.
13633
14696
  *
@@ -14071,7 +15134,7 @@ import ora3 from "ora";
14071
15134
  import { select as select3 } from "@inquirer/prompts";
14072
15135
  function unknownPasswordError2(port) {
14073
15136
  return new Error(
14074
- `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
15137
+ `An assistant on port ${port} requires credentials this runner does not have. Released OpenCode V2 generates a server password even when none is configured. This CLI has no option to supply the password for an already-running server, so it will not join one you started. Free port ${port} so Evident can start its own, or pass --port to choose another.`
14075
15138
  );
14076
15139
  }
14077
15140
  var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
@@ -14089,9 +15152,9 @@ async function ensureOpenCode2Running(ctx) {
14089
15152
  password: null
14090
15153
  };
14091
15154
  }
14092
- if (!isOpenCode2Installed()) {
15155
+ if (!ctx.command && !isOpenCode2Installed()) {
14093
15156
  throw new Error(
14094
- `OpenCode V2 (opencode2) is not installed. Install it with: npm install -g ${OPENCODE2_PACKAGE_SPEC}`
15157
+ "OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
14095
15158
  );
14096
15159
  }
14097
15160
  let port = ctx.port;
@@ -14119,7 +15182,8 @@ Port ${port} is already in use.`));
14119
15182
  if (!ctx.interactive) {
14120
15183
  ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
14121
15184
  const { child: proc, password } = await startOpenCode2(port, {
14122
- inheritStdio: ctx.inheritStdio
15185
+ inheritStdio: ctx.inheritStdio,
15186
+ command: ctx.command
14123
15187
  });
14124
15188
  const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
14125
15189
  if (!health.healthy) {
@@ -14140,13 +15204,14 @@ Port ${port} is already in use.`));
14140
15204
  password
14141
15205
  };
14142
15206
  }
15207
+ const command = ctx.command ?? "opencode2";
14143
15208
  const action = await select3({
14144
15209
  message: "OpenCode V2 is not running. What would you like to do?",
14145
15210
  choices: [
14146
15211
  {
14147
15212
  name: "Start OpenCode V2 for me",
14148
15213
  value: "start",
14149
- description: `Run 'opencode2 serve --port ${port}'`
15214
+ description: `Run '${command} serve --port ${port}'`
14150
15215
  },
14151
15216
  {
14152
15217
  name: "Show me the command",
@@ -14164,14 +15229,15 @@ Port ${port} is already in use.`));
14164
15229
  blank();
14165
15230
  console.log(chalk6.bold("Run this command in another terminal:"));
14166
15231
  blank();
14167
- console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
15232
+ console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
14168
15233
  blank();
14169
15234
  throw new Error("Please start OpenCode V2 manually");
14170
15235
  }
14171
15236
  if (action === "start") {
14172
15237
  const spinner = ora3("Starting OpenCode V2...").start();
14173
15238
  const { child: proc, password } = await startOpenCode2(port, {
14174
- inheritStdio: ctx.inheritStdio
15239
+ inheritStdio: ctx.inheritStdio,
15240
+ command: ctx.command
14175
15241
  });
14176
15242
  const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
14177
15243
  if (!health.healthy) {
@@ -14197,7 +15263,7 @@ Port ${port} is already in use.`));
14197
15263
  }
14198
15264
 
14199
15265
  // src/lib/runner-credentials.ts
14200
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync5 } from "node:fs";
15266
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
14201
15267
  import { spawn as spawn5 } from "node:child_process";
14202
15268
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
14203
15269
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
@@ -14440,8 +15506,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
14440
15506
  }
14441
15507
  try {
14442
15508
  env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
14443
- writeFileSync5(GIT_CONFIG_GLOBAL, "");
14444
- writeFileSync5(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
15509
+ writeFileSync7(GIT_CONFIG_GLOBAL, "");
15510
+ writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
14445
15511
  chmodSync2(GIT_CREDENTIAL_HELPER, 448);
14446
15512
  const config = [
14447
15513
  ["user.name", env.GIT_USER_NAME ?? "evident-bot"],
@@ -14474,7 +15540,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
14474
15540
  // src/lib/opencode/config-overlay.ts
14475
15541
  import { execFileSync as execFileSync2 } from "node:child_process";
14476
15542
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
14477
- import { isAbsolute as isAbsolute3, join as join12, resolve as resolve4 } from "node:path";
15543
+ import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
14478
15544
  function parseConfigContent(content) {
14479
15545
  if (!content?.trim()) return {};
14480
15546
  const parsed = JSON.parse(content);
@@ -14504,7 +15570,7 @@ function applyRunnerOpenCodeConfig({
14504
15570
  return;
14505
15571
  }
14506
15572
  const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
14507
- const target = isFile(join12(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
15573
+ const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
14508
15574
  if (!isFile(source)) {
14509
15575
  log3(
14510
15576
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -14512,7 +15578,7 @@ function applyRunnerOpenCodeConfig({
14512
15578
  );
14513
15579
  return;
14514
15580
  }
14515
- copyFileSync(source, join12(cwd, target));
15581
+ copyFileSync(source, join14(cwd, target));
14516
15582
  try {
14517
15583
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
14518
15584
  stdio: "ignore"
@@ -14521,13 +15587,13 @@ function applyRunnerOpenCodeConfig({
14521
15587
  const detail = error2 instanceof Error ? error2.message : String(error2);
14522
15588
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
14523
15589
  }
14524
- log3(`Applied runner OpenCode config ${source} to ${join12(cwd, target)}`);
15590
+ log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
14525
15591
  }
14526
15592
 
14527
15593
  // src/lib/opencode/runtime-provenance.ts
14528
- import { readFileSync as readFileSync8, readdirSync } from "node:fs";
15594
+ import { readFileSync as readFileSync11, readdirSync } from "node:fs";
14529
15595
  import { homedir as homedir7 } from "node:os";
14530
- import { join as join13 } from "node:path";
15596
+ import { join as join15 } from "node:path";
14531
15597
  var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
14532
15598
  var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
14533
15599
  function errorKind(error2) {
@@ -14595,7 +15661,7 @@ function parseJsonc(value) {
14595
15661
  return JSON.parse(output);
14596
15662
  }
14597
15663
  function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14598
- return join13(env.XDG_CACHE_HOME ?? join13(homeDir, ".cache"), "opencode");
15664
+ return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
14599
15665
  }
14600
15666
  function readConfiguredPluginSpecs({
14601
15667
  cwd = process.cwd(),
@@ -14604,10 +15670,10 @@ function readConfiguredPluginSpecs({
14604
15670
  let config;
14605
15671
  try {
14606
15672
  try {
14607
- config = parseJsonc(readFileSync8(join13(cwd, "opencode.jsonc"), "utf8"));
15673
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
14608
15674
  } catch (error2) {
14609
15675
  if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14610
- config = parseJsonc(readFileSync8(join13(cwd, "opencode.json"), "utf8"));
15676
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
14611
15677
  }
14612
15678
  } catch (error2) {
14613
15679
  log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
@@ -14635,7 +15701,7 @@ function resolveInstalledPluginVersions({
14635
15701
  homeDir = homedir7(),
14636
15702
  log: log3
14637
15703
  }) {
14638
- const packagesRoot = join13(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
15704
+ const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14639
15705
  let directories;
14640
15706
  try {
14641
15707
  directories = readdirSync(packagesRoot, { withFileTypes: true });
@@ -14656,8 +15722,8 @@ function resolveInstalledPluginVersions({
14656
15722
  let resolvedVersion = null;
14657
15723
  try {
14658
15724
  const packageJson = JSON.parse(
14659
- readFileSync8(
14660
- join13(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
15725
+ readFileSync11(
15726
+ join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14661
15727
  "utf8"
14662
15728
  )
14663
15729
  );
@@ -14678,7 +15744,7 @@ function resolveInstalledPluginVersions({
14678
15744
  }
14679
15745
 
14680
15746
  // src/lib/credential-sync.ts
14681
- import { renameSync, writeFileSync as writeFileSync6 } from "node:fs";
15747
+ import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
14682
15748
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
14683
15749
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
14684
15750
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -14688,7 +15754,7 @@ var MAX_FLUSH_PASSES = 2;
14688
15754
  function outcomesWith(outcome) {
14689
15755
  return { claude: outcome, opencode: outcome };
14690
15756
  }
14691
- function errorMessage2(error2) {
15757
+ function errorMessage3(error2) {
14692
15758
  return error2 instanceof Error ? error2.message : String(error2);
14693
15759
  }
14694
15760
  function waitForSettlement(promise, timeoutMs) {
@@ -14712,10 +15778,10 @@ function writeMarker(markerPath, outcomes, log3) {
14712
15778
  `;
14713
15779
  const temporaryPath = `${markerPath}.tmp`;
14714
15780
  try {
14715
- writeFileSync6(temporaryPath, body, { mode: 384 });
15781
+ writeFileSync8(temporaryPath, body, { mode: 384 });
14716
15782
  renameSync(temporaryPath, markerPath);
14717
15783
  } catch (error2) {
14718
- log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
15784
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
14719
15785
  }
14720
15786
  }
14721
15787
  function intervalSeconds(env, log3) {
@@ -14747,7 +15813,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
14747
15813
  },
14748
15814
  (error2) => {
14749
15815
  failed = true;
14750
- log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
15816
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
14751
15817
  }
14752
15818
  );
14753
15819
  const abortTimer = setTimeout(() => controller.abort(), remainingMs);
@@ -14807,7 +15873,7 @@ function createCredentialSync({
14807
15873
  outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
14808
15874
  } catch (error2) {
14809
15875
  outcomes[store] = "failed";
14810
- log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
15876
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
14811
15877
  }
14812
15878
  }
14813
15879
  const failed = STORES.some((store) => outcomes[store] === "failed");
@@ -14930,14 +15996,21 @@ function request2(options) {
14930
15996
  function safeFailure(operation, status2) {
14931
15997
  return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
14932
15998
  }
14933
- function logTransportFailure(options, operation, error2, authorization) {
14934
- let detail = error2 instanceof Error ? error2.message : String(error2);
15999
+ function redactAuthorizedText(message, authorization) {
16000
+ let redacted = message;
14935
16001
  const credential = authorization.split(/\s+/, 2)[1];
14936
16002
  for (const secret of [authorization, credential]) {
14937
- if (secret) detail = detail.replaceAll(secret, "[redacted]");
16003
+ if (secret) redacted = redacted.replaceAll(secret, "[redacted]");
14938
16004
  }
16005
+ return redactDiagnosticText(redacted);
16006
+ }
16007
+ function logTransportFailure(options, operation, error2, authorization) {
16008
+ const detail = redactAuthorizedText(
16009
+ error2 instanceof Error ? error2.message : String(error2),
16010
+ authorization
16011
+ );
14939
16012
  options.log(
14940
- `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${redactDiagnosticText(detail)}`,
16013
+ `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${detail}`,
14941
16014
  "debug"
14942
16015
  );
14943
16016
  }
@@ -14957,6 +16030,18 @@ function readSetup(value) {
14957
16030
  }
14958
16031
  return setup;
14959
16032
  }
16033
+ function readModelDescriptors(value) {
16034
+ if (!Array.isArray(value)) return null;
16035
+ return value.every(
16036
+ (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
16037
+ ) ? value : null;
16038
+ }
16039
+ function logDescriptorRefreshFallback(options, branch, generation) {
16040
+ options.log(
16041
+ `MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
16042
+ "warn"
16043
+ );
16044
+ }
14960
16045
  function clearLocalSetup(options) {
14961
16046
  const env = options.env ?? process.env;
14962
16047
  const store = options.store ?? defaultStore;
@@ -14988,7 +16073,14 @@ async function discoverManagedInferenceSetup(options) {
14988
16073
  return { kind: "none" };
14989
16074
  }
14990
16075
  if (!response.ok) {
14991
- return { kind: "failed", reason: safeFailure("Managed setup discovery", response.status) };
16076
+ const body2 = await responseJson(response);
16077
+ const serverMessage = typeof body2?.error === "string" && body2.error.trim() ? redactAuthorizedText(body2.error.trim(), headers.Authorization).slice(0, 500) : void 0;
16078
+ const failure = serverMessage ? `Managed setup discovery returned HTTP ${response.status}: ${serverMessage}` : safeFailure("Managed setup discovery", response.status);
16079
+ const runnerKeyGuidance = response.status === 401 || response.status === 403 ? "Authenticate as this runner with its runner key (EVIDENT_RUNNER_KEY)." : void 0;
16080
+ return {
16081
+ kind: "failed",
16082
+ reason: runnerKeyGuidance ? `${failure}. ${runnerKeyGuidance}` : failure
16083
+ };
14992
16084
  }
14993
16085
  const body = await responseJson(response);
14994
16086
  const setup = readSetup(body?.setup);
@@ -15088,7 +16180,27 @@ async function reaffirmStored(options, credential) {
15088
16180
  if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
15089
16181
  return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
15090
16182
  }
15091
- const failure = applyCredential(options, credential);
16183
+ const descriptorsValue = body?.model_descriptors;
16184
+ const refreshedDescriptors = readModelDescriptors(descriptorsValue);
16185
+ let credentialToApply = credential;
16186
+ if (descriptorsValue === void 0 || descriptorsValue === null) {
16187
+ logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
16188
+ } else if (refreshedDescriptors === null) {
16189
+ logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
16190
+ } else if (refreshedDescriptors.length === 0) {
16191
+ logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
16192
+ } else {
16193
+ const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
16194
+ inferenceBaseUrl: credential.inferenceBaseUrl,
16195
+ modelDescriptors: refreshedDescriptors
16196
+ });
16197
+ if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
16198
+ logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
16199
+ } else {
16200
+ credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
16201
+ }
16202
+ }
16203
+ const failure = applyCredential(options, credentialToApply);
15092
16204
  return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
15093
16205
  }
15094
16206
  async function prepareManagedInferenceSetup(options, discovery, health) {
@@ -15122,10 +16234,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
15122
16234
  const setup = readSetup(body?.setup);
15123
16235
  const token = body?.inference_token;
15124
16236
  const inferenceBaseUrl = body?.inference_base_url;
15125
- const modelDescriptors = body?.model_descriptors;
15126
- if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || !Array.isArray(modelDescriptors) || !modelDescriptors.every(
15127
- (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
15128
- )) {
16237
+ const modelDescriptors = readModelDescriptors(body?.model_descriptors);
16238
+ if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
15129
16239
  return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
15130
16240
  }
15131
16241
  const credential = {
@@ -15199,7 +16309,7 @@ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
15199
16309
  if (trimmed === "") {
15200
16310
  throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
15201
16311
  }
15202
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join14(homeDir, trimmed.slice(2)) : trimmed;
16312
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
15203
16313
  if (!isAbsolute4(expanded)) {
15204
16314
  throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
15205
16315
  }
@@ -15252,13 +16362,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
15252
16362
  raw = env[OPENCODE_VERSION_ENV];
15253
16363
  source = OPENCODE_VERSION_ENV;
15254
16364
  } else {
15255
- return { version: "v1", warnings: [] };
16365
+ return { version: "auto", warnings: [] };
15256
16366
  }
15257
16367
  const normalized = raw.trim().toLowerCase();
15258
- if (normalized !== "v1" && normalized !== "v2") {
16368
+ if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
15259
16369
  return {
15260
- version: "v1",
15261
- warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
16370
+ version: "auto",
16371
+ warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
15262
16372
  };
15263
16373
  }
15264
16374
  return { version: normalized, warnings: [] };
@@ -15768,8 +16878,8 @@ async function driveChannels(state, driver) {
15768
16878
  state.running = false;
15769
16879
  break;
15770
16880
  }
15771
- const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
15772
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
16881
+ const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
16882
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
15773
16883
  if (state.interactive) displayStatus(state);
15774
16884
  if (driver.hasInFlightWatchers()) {
15775
16885
  consecutiveDrainFailures = 0;
@@ -15859,6 +16969,7 @@ async function driveChannels(state, driver) {
15859
16969
  }
15860
16970
  }
15861
16971
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
16972
+ var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
15862
16973
  var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
15863
16974
  var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
15864
16975
  var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
@@ -15873,7 +16984,7 @@ function shouldWarnForReclaimSkip(reason) {
15873
16984
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
15874
16985
  }
15875
16986
  function sessionDbPath() {
15876
- return join14(homedir8(), ".local", "share", "opencode", "opencode.db");
16987
+ return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
15877
16988
  }
15878
16989
  function logSessionCleanupActivity(state, phase, entry) {
15879
16990
  logActivity(state, {
@@ -15882,6 +16993,63 @@ function logSessionCleanupActivity(state, phase, entry) {
15882
16993
  metadata: { ...entry.metadata, phase }
15883
16994
  });
15884
16995
  }
16996
+ function logSessionArchiveActivity(state, phase, entry) {
16997
+ logActivity(state, {
16998
+ ...entry,
16999
+ source: "cli.session-archive",
17000
+ metadata: { ...entry.metadata, phase }
17001
+ });
17002
+ }
17003
+ function logSessionArchiveSummary(state, phase, summary) {
17004
+ const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
17005
+ const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
17006
+ const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
17007
+ const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed;
17008
+ logSessionArchiveActivity(state, phase, {
17009
+ type: "info",
17010
+ level: warning2 ? "warn" : "info",
17011
+ message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}` + staleSince + neverWatermarked,
17012
+ metadata: {
17013
+ outcome: "archive-run-complete",
17014
+ sessions_inspected: summary.inspected,
17015
+ sessions_uploaded: summary.uploaded,
17016
+ sessions_unchanged: summary.unchanged,
17017
+ sessions_failed: summary.failed,
17018
+ uploads_attempted: summary.uploadAttempts,
17019
+ uploads_failed: summary.uploadFailures,
17020
+ sessions_skipped_budget: summary.skippedBudget,
17021
+ sessions_unmapped: summary.unmappedIds.length,
17022
+ sessions_without_prior_watermark: summary.neverWatermarkedFailures,
17023
+ ...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
17024
+ }
17025
+ });
17026
+ for (const failure of summary.failures) {
17027
+ const permanentSizeFailure = failure.reason === "too-large";
17028
+ const safeMessage = failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
17029
+ logSessionArchiveActivity(state, phase, {
17030
+ type: "info",
17031
+ level: "warn",
17032
+ message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + safeMessage,
17033
+ metadata: {
17034
+ outcome: failure.outcome,
17035
+ session_id: failure.sessionId,
17036
+ archived: failure.archived,
17037
+ ...failure.reason !== void 0 ? { reason: failure.reason } : {}
17038
+ }
17039
+ });
17040
+ }
17041
+ if (summary.unmappedIds.length > 0) {
17042
+ logSessionArchiveActivity(state, phase, {
17043
+ type: "info",
17044
+ level: "warn",
17045
+ message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
17046
+ metadata: {
17047
+ outcome: "unmapped-sessions",
17048
+ sessions_unmapped: summary.unmappedIds.length
17049
+ }
17050
+ });
17051
+ }
17052
+ }
15885
17053
  async function drainSessionDeletionReports(state) {
15886
17054
  const path = sessionDeletionReportPath(homedir8(), process.env);
15887
17055
  let delivered = 0;
@@ -15979,6 +17147,7 @@ async function runSweep(state, driver, config) {
15979
17147
  let failedOtherStatus = 0;
15980
17148
  let failedThrew = 0;
15981
17149
  let skippedNewlyActive = 0;
17150
+ let skippedArchive = 0;
15982
17151
  let remaining = 0;
15983
17152
  const deletionRecords = [];
15984
17153
  let protectedNow = driver.protectedSessionIds();
@@ -15990,6 +17159,7 @@ async function runSweep(state, driver, config) {
15990
17159
  const lastActivityById = new Map(
15991
17160
  snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
15992
17161
  );
17162
+ const sessionById = new Map(sessions.map((session) => [session.id, session]));
15993
17163
  const toDelete = selectSessionsToDelete(snapshots, {
15994
17164
  maxAgeMs: config.maxAgeMs,
15995
17165
  maxCount: config.maxCount,
@@ -16012,6 +17182,42 @@ async function runSweep(state, driver, config) {
16012
17182
  });
16013
17183
  continue;
16014
17184
  }
17185
+ if (state.sessionArchiveConfig.enabled) {
17186
+ const candidate = sessionById.get(id);
17187
+ const archiveConfig = state.sessionArchiveConfig;
17188
+ const archiveBudgetMs = Math.min(
17189
+ archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
17190
+ MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
17191
+ );
17192
+ const archived = candidate ? await runSessionArchiveTask(
17193
+ state,
17194
+ "sweep",
17195
+ archiveBudgetMs,
17196
+ (signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
17197
+ ) : null;
17198
+ if (!archived?.archivedSessionIds.includes(id)) {
17199
+ skippedArchive++;
17200
+ logSessionArchiveActivity(state, "sweep", {
17201
+ type: "info",
17202
+ level: "warn",
17203
+ message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
17204
+ metadata: {
17205
+ outcome: "deletion-held-for-archive",
17206
+ session_id: id
17207
+ }
17208
+ });
17209
+ continue;
17210
+ }
17211
+ protectedNow = driver.protectedSessionIds();
17212
+ if (protectedNow.has(id)) {
17213
+ skippedNewlyActive++;
17214
+ logActivity(state, {
17215
+ type: "info",
17216
+ message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
17217
+ });
17218
+ continue;
17219
+ }
17220
+ }
16015
17221
  const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
16016
17222
  if (outcome.ok) {
16017
17223
  deleted++;
@@ -16075,10 +17281,11 @@ async function runSweep(state, driver, config) {
16075
17281
  }
16076
17282
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
16077
17283
  const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
17284
+ const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
16078
17285
  const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
16079
17286
  logActivity(state, {
16080
17287
  type: "info",
16081
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
17288
+ message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
16082
17289
  });
16083
17290
  }
16084
17291
  if (deletionRecords.length > 0) {
@@ -16116,6 +17323,7 @@ async function runSweep(state, driver, config) {
16116
17323
  sessions_failed_other: failedOtherStatus,
16117
17324
  sessions_failed_threw: failedThrew,
16118
17325
  sessions_skipped_active: skippedNewlyActive,
17326
+ sessions_archive_held: skippedArchive,
16119
17327
  sessions_remaining: remaining,
16120
17328
  ...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
16121
17329
  };
@@ -16247,6 +17455,186 @@ function scheduleSessionCleanup(state, driver, options) {
16247
17455
  );
16248
17456
  state.sessionCleanupTimers.push(interval, firstSweep);
16249
17457
  }
17458
+ function runSessionArchiveTask(state, phase, budgetMs, work) {
17459
+ if (state.sessionArchiveInFlight) {
17460
+ logSessionArchiveActivity(state, phase, {
17461
+ type: "info",
17462
+ level: "debug",
17463
+ message: "Session archive skipped \u2014 previous archive run still in progress",
17464
+ metadata: { outcome: "skipped-in-flight" }
17465
+ });
17466
+ return Promise.resolve(null);
17467
+ }
17468
+ const controller = new AbortController();
17469
+ state.sessionArchiveAbortController = controller;
17470
+ const timeout = setTimeout(() => {
17471
+ controller.abort();
17472
+ logSessionArchiveActivity(state, phase, {
17473
+ type: "info",
17474
+ level: "warn",
17475
+ message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
17476
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17477
+ });
17478
+ }, budgetMs);
17479
+ const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
17480
+ logSessionArchiveActivity(state, phase, {
17481
+ type: "info",
17482
+ level: "warn",
17483
+ message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
17484
+ metadata: { outcome: "archive-run-failed" }
17485
+ });
17486
+ return null;
17487
+ }).finally(() => {
17488
+ clearTimeout(timeout);
17489
+ if (state.sessionArchiveInFlight === task) {
17490
+ state.sessionArchiveInFlight = null;
17491
+ state.sessionArchiveAbortController = null;
17492
+ }
17493
+ });
17494
+ state.sessionArchiveInFlight = task;
17495
+ return task;
17496
+ }
17497
+ async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
17498
+ const config = state.sessionArchiveConfig;
17499
+ const client = state.opencodeClient;
17500
+ const watermarkPath = state.sessionArchiveWatermarkPath;
17501
+ if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
17502
+ logSessionArchiveActivity(state, phase, {
17503
+ type: "info",
17504
+ level: "warn",
17505
+ message: "Session archive skipped because runner identity or archive configuration is unavailable",
17506
+ metadata: { outcome: "archive-configuration-unavailable" }
17507
+ });
17508
+ return null;
17509
+ }
17510
+ const summary = await runSessionArchive({
17511
+ sessions,
17512
+ deps: {
17513
+ client,
17514
+ runnerId: state.agentId,
17515
+ keyPrefix: config.keyPrefix,
17516
+ bindings: readSessionBindings(
17517
+ sessionBindingLedgerPath(homedir8(), process.env),
17518
+ (message) => logSessionArchiveActivity(state, phase, {
17519
+ type: "info",
17520
+ level: "warn",
17521
+ message: redactDiagnosticText(message),
17522
+ metadata: { outcome: "binding-ledger-read-failed" }
17523
+ })
17524
+ ),
17525
+ watermarks: state.sessionArchiveWatermarks,
17526
+ watermarkPath,
17527
+ signal,
17528
+ upload: uploadSessionArchive,
17529
+ exportMessages: exportSessionMessages,
17530
+ recordWatermark: recordUpload
17531
+ },
17532
+ budgetMs
17533
+ });
17534
+ logSessionArchiveSummary(state, phase, summary);
17535
+ return summary;
17536
+ }
17537
+ async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
17538
+ const startedAt = Date.now();
17539
+ const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
17540
+ if (enumeration.sessions === null) {
17541
+ const interrupted = signal.aborted;
17542
+ logSessionArchiveActivity(state, phase, {
17543
+ type: "info",
17544
+ level: "warn",
17545
+ message: interrupted ? "Session archive: enumeration was interrupted before the complete store was known; no session was marked archived" : `Session archive: could not enumerate the complete session store (${enumeration.reason}); no session was marked archived`,
17546
+ metadata: {
17547
+ outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
17548
+ ...!interrupted ? { reason: enumeration.reason } : {}
17549
+ }
17550
+ });
17551
+ return null;
17552
+ }
17553
+ return runSessionArchiveBatch(
17554
+ state,
17555
+ phase,
17556
+ enumeration.sessions,
17557
+ Math.max(0, budgetMs - (Date.now() - startedAt)),
17558
+ signal
17559
+ );
17560
+ }
17561
+ async function runSessionArchiveShutdownFlush(state, budgetMs) {
17562
+ const deadline = Date.now() + budgetMs;
17563
+ const previousRun = state.sessionArchiveInFlight;
17564
+ if (previousRun) {
17565
+ state.sessionArchiveAbortController?.abort(
17566
+ new Error("graceful shutdown archive flush requested")
17567
+ );
17568
+ let timeout;
17569
+ const settled = await Promise.race([
17570
+ previousRun.then(() => true),
17571
+ new Promise((resolve5) => {
17572
+ timeout = setTimeout(() => resolve5(false), budgetMs);
17573
+ })
17574
+ ]);
17575
+ if (timeout) clearTimeout(timeout);
17576
+ if (!settled) {
17577
+ state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
17578
+ logSessionArchiveActivity(state, "shutdown", {
17579
+ type: "info",
17580
+ level: "warn",
17581
+ message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
17582
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17583
+ });
17584
+ return null;
17585
+ }
17586
+ }
17587
+ const remainingMs = Math.floor(deadline - Date.now());
17588
+ if (remainingMs <= 0) {
17589
+ logSessionArchiveActivity(state, "shutdown", {
17590
+ type: "info",
17591
+ level: "warn",
17592
+ message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
17593
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17594
+ });
17595
+ return null;
17596
+ }
17597
+ return runSessionArchiveTask(
17598
+ state,
17599
+ "shutdown",
17600
+ remainingMs,
17601
+ (signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
17602
+ );
17603
+ }
17604
+ function scheduleSessionArchive(state) {
17605
+ const config = state.sessionArchiveConfig;
17606
+ if (!config.enabled) return;
17607
+ state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
17608
+ state.sessionArchiveWatermarks = readWatermarks(
17609
+ state.sessionArchiveWatermarkPath,
17610
+ (message) => logSessionArchiveActivity(state, "startup", {
17611
+ type: "info",
17612
+ level: "warn",
17613
+ message: redactDiagnosticText(message),
17614
+ metadata: { outcome: "watermark-read-failed" }
17615
+ })
17616
+ );
17617
+ logSessionArchiveActivity(state, "startup", {
17618
+ type: "info",
17619
+ message: `Session archive enabled (interval=${config.intervalMs}ms)`,
17620
+ metadata: { outcome: "armed", interval_ms: config.intervalMs }
17621
+ });
17622
+ const cycleBudgetMs = Math.min(
17623
+ config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
17624
+ MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
17625
+ );
17626
+ const run2 = (phase = "scheduled") => {
17627
+ void runSessionArchiveTask(
17628
+ state,
17629
+ phase,
17630
+ cycleBudgetMs,
17631
+ (signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
17632
+ );
17633
+ };
17634
+ const interval = setInterval(run2, config.intervalMs);
17635
+ const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
17636
+ state.sessionArchiveTimers.push(interval, firstRun);
17637
+ }
16250
17638
  function scheduleUsageReporting(state, params) {
16251
17639
  const { mode, warnings } = params.resolved;
16252
17640
  for (const warning2 of warnings) {
@@ -16521,6 +17909,12 @@ async function cleanup(state, opts = {}) {
16521
17909
  clearTimeout(timer);
16522
17910
  }
16523
17911
  state.sessionCleanupTimers = [];
17912
+ for (const timer of state.sessionArchiveTimers) {
17913
+ clearInterval(timer);
17914
+ clearTimeout(timer);
17915
+ }
17916
+ state.sessionArchiveTimers = [];
17917
+ state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
16524
17918
  state.stopOpenCodeLogTail?.();
16525
17919
  state.stopOpenCodeLogTail = null;
16526
17920
  if (state.claudeUsageTimer) {
@@ -16588,6 +17982,15 @@ async function cleanup(state, opts = {}) {
16588
17982
  await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
16589
17983
  state.connection = null;
16590
17984
  }
17985
+ if (opts.graceful && state.sessionArchiveConfig.enabled) {
17986
+ const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
17987
+ await timeShutdownPhase(
17988
+ state,
17989
+ durations,
17990
+ "session_archive_flush",
17991
+ () => runSessionArchiveShutdownFlush(state, budgetMs)
17992
+ );
17993
+ }
16591
17994
  if (state.opencodeProcess) {
16592
17995
  const opencodeProcess = state.opencodeProcess;
16593
17996
  const result = await timeShutdownPhase(
@@ -16711,6 +18114,18 @@ async function run(options) {
16711
18114
  messageCount: 0,
16712
18115
  lastProxiedActivityAt: null,
16713
18116
  sessionCleanupTimers: [],
18117
+ sessionArchiveConfig: resolveSessionArchiveConfig(
18118
+ {
18119
+ enabled: options.sessionArchive,
18120
+ interval: options.sessionArchiveInterval
18121
+ },
18122
+ process.env
18123
+ ),
18124
+ sessionArchiveTimers: [],
18125
+ sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
18126
+ sessionArchiveWatermarkPath: null,
18127
+ sessionArchiveInFlight: null,
18128
+ sessionArchiveAbortController: null,
16714
18129
  sessionDeleteFailures: /* @__PURE__ */ new Map(),
16715
18130
  claudeUsageTimer: null,
16716
18131
  claudeUsageRearm: null,
@@ -16724,6 +18139,14 @@ async function run(options) {
16724
18139
  };
16725
18140
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
16726
18141
  setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
18142
+ for (const warning2 of state.sessionArchiveConfig.warnings) {
18143
+ logSessionArchiveActivity(state, "startup", {
18144
+ type: "info",
18145
+ level: "warn",
18146
+ message: `Session archive: ${warning2}`,
18147
+ metadata: { outcome: "configuration-warning" }
18148
+ });
18149
+ }
16727
18150
  if (options.credentialSyncMarker) {
16728
18151
  state.credentialSync = createCredentialSync({
16729
18152
  markerPath: options.credentialSyncMarker,
@@ -17045,13 +18468,75 @@ async function run(options) {
17045
18468
  for (const warning2 of opencodeStartTimeoutWarnings) {
17046
18469
  logActivity(state, { type: "info", level: "warn", message: warning2 });
17047
18470
  }
17048
- const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
17049
- options,
17050
- process.env
17051
- );
18471
+ const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
17052
18472
  for (const warning2 of opencodeVersionWarnings) {
17053
18473
  logActivity(state, { type: "info", level: "warn", message: warning2 });
17054
18474
  }
18475
+ let opencodeMajor;
18476
+ let opencodeV2Command;
18477
+ let opencodeDetectionSource;
18478
+ let opencodeDetectionLevel = "info";
18479
+ let opencodeDetectionMessage;
18480
+ if (opencodeVersionSelection === "v1") {
18481
+ opencodeMajor = "v1";
18482
+ opencodeDetectionSource = "explicit";
18483
+ opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
18484
+ } else if (opencodeVersionSelection === "v2") {
18485
+ const installed = resolveInstalledOpenCode();
18486
+ opencodeMajor = "v2";
18487
+ opencodeDetectionSource = "explicit";
18488
+ if (installed.kind === "installed" && installed.major === "v2") {
18489
+ opencodeV2Command = installed.command;
18490
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
18491
+ } else if (installed.kind === "unknown") {
18492
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
18493
+ } else if (installed.kind === "installed") {
18494
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
18495
+ } else {
18496
+ opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
18497
+ }
18498
+ } else {
18499
+ const running = await classifyRunningOpenCode(state.port);
18500
+ if (running.kind === "v1") {
18501
+ opencodeMajor = "v1";
18502
+ opencodeDetectionSource = "running-server";
18503
+ opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
18504
+ } else if (running.kind === "v2") {
18505
+ opencodeMajor = "v2";
18506
+ opencodeDetectionSource = "running-server";
18507
+ opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
18508
+ } else if (running.kind === "authenticated-unknown") {
18509
+ const message = `A server on port ${state.port} requires credentials this runner does not have. This can be a password-protected V1 or a V2 server, so the protocol cannot be established. Select it explicitly with --opencode-version v1 or --opencode-version v2.`;
18510
+ logActivity(state, {
18511
+ type: "info",
18512
+ level: "warn",
18513
+ message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
18514
+ });
18515
+ throw new Error(message);
18516
+ } else {
18517
+ const installed = resolveInstalledOpenCode();
18518
+ opencodeDetectionLevel = "warn";
18519
+ if (installed.kind === "installed") {
18520
+ opencodeMajor = installed.major;
18521
+ opencodeDetectionSource = "installed-binary";
18522
+ if (installed.major === "v2") opencodeV2Command = installed.command;
18523
+ opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); using installed ${installed.major} command ${installed.command}${installed.version ? ` (${installed.version})` : " with an unknown version"}.`;
18524
+ } else if (installed.kind === "unknown") {
18525
+ opencodeMajor = "v1";
18526
+ opencodeDetectionSource = "fallback-v1";
18527
+ opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, so assuming V1. Use --opencode-version v2 to select V2 explicitly.`;
18528
+ } else {
18529
+ opencodeMajor = "v1";
18530
+ opencodeDetectionSource = "fallback-v1";
18531
+ opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); no recognizable OpenCode binary is installed, so continuing into the V1 install flow.`;
18532
+ }
18533
+ }
18534
+ }
18535
+ logActivity(state, {
18536
+ type: "info",
18537
+ level: opencodeDetectionLevel,
18538
+ message: opencodeDetectionMessage
18539
+ });
17055
18540
  const managedSetupOptions = {
17056
18541
  agentId: state.agentId,
17057
18542
  apiUrl: getApiUrlConfig(),
@@ -17072,7 +18557,7 @@ async function run(options) {
17072
18557
  managedPreparation = managedDiscovery;
17073
18558
  managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
17074
18559
  } else if (managedDiscovery.kind === "setup") {
17075
- const managedHealth = opencodeVersion === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
18560
+ const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
17076
18561
  managedPreparation = await prepareManagedInferenceSetup(
17077
18562
  managedSetupOptions,
17078
18563
  managedDiscovery,
@@ -17097,13 +18582,14 @@ async function run(options) {
17097
18582
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
17098
18583
  const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
17099
18584
  try {
17100
- const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
18585
+ const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
17101
18586
  port: state.port,
17102
18587
  interactive: state.interactive,
17103
18588
  agentId: state.agentId,
17104
18589
  log: (message) => log2(state, message),
17105
18590
  startTimeoutMs: opencodeStartTimeoutMs,
17106
- inheritStdio: Boolean(options.opencodePidFile)
18591
+ inheritStdio: Boolean(options.opencodePidFile),
18592
+ command: opencodeV2Command
17107
18593
  }) : await ensureOpenCodeRunning({
17108
18594
  port: state.port,
17109
18595
  interactive: state.interactive,
@@ -17115,7 +18601,7 @@ async function run(options) {
17115
18601
  state.port = oc.port;
17116
18602
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
17117
18603
  state.opencodeVersion = oc.version;
17118
- state.opencodeApiVersion = opencodeVersion;
18604
+ state.opencodeApiVersion = opencodeMajor;
17119
18605
  let configuredPlugins = [];
17120
18606
  let resolvedPlugins = [];
17121
18607
  try {
@@ -17137,13 +18623,14 @@ async function run(options) {
17137
18623
  metadata: {
17138
18624
  opencode_version: state.opencodeVersion,
17139
18625
  opencode_api_version: state.opencodeApiVersion,
18626
+ opencode_detection_source: opencodeDetectionSource,
17140
18627
  configured_plugins: JSON.stringify(configuredPlugins),
17141
18628
  resolved_plugins: JSON.stringify(resolvedPlugins)
17142
18629
  },
17143
18630
  source: "cli.runtime-provenance"
17144
18631
  });
17145
18632
  let opencodePassword = null;
17146
- if (opencodeVersion === "v2" && "password" in oc) {
18633
+ if (opencodeMajor === "v2" && "password" in oc) {
17147
18634
  const value = oc.password;
17148
18635
  if (typeof value === "string" || value === null) opencodePassword = value;
17149
18636
  }
@@ -17156,7 +18643,7 @@ async function run(options) {
17156
18643
  state.opencodeClient = openCodeClient;
17157
18644
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
17158
18645
  try {
17159
- writeFileSync7(options.opencodePidFile, `${oc.process.pid}
18646
+ writeFileSync9(options.opencodePidFile, `${oc.process.pid}
17160
18647
  `, { mode: 384 });
17161
18648
  chmodSync3(options.opencodePidFile, 384);
17162
18649
  } catch (error2) {
@@ -17266,7 +18753,7 @@ async function run(options) {
17266
18753
  let existingPid;
17267
18754
  if (existsSync3(options.litestreamPidFile)) {
17268
18755
  try {
17269
- const rawPid = readFileSync9(options.litestreamPidFile, "utf8").trim();
18756
+ const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
17270
18757
  const parsedPid = Number(rawPid);
17271
18758
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
17272
18759
  existingPid = parsedPid;
@@ -17303,7 +18790,7 @@ async function run(options) {
17303
18790
  });
17304
18791
  try {
17305
18792
  if (litestreamProcess.pid !== void 0) {
17306
- writeFileSync7(options.litestreamPidFile, `${litestreamProcess.pid}
18793
+ writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
17307
18794
  `, {
17308
18795
  mode: 384
17309
18796
  });
@@ -17353,6 +18840,15 @@ async function run(options) {
17353
18840
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
17354
18841
  }
17355
18842
  const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
18843
+ const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
18844
+ sessionBindingLedgerPath(homedir8(), process.env),
18845
+ {
18846
+ conversation_id: conversationId,
18847
+ opencode_session_id: sessionId,
18848
+ observed_at: (/* @__PURE__ */ new Date()).toISOString()
18849
+ },
18850
+ (message) => logActivity(state, { type: "info", level: "warn", message })
18851
+ ) : void 0;
17356
18852
  const channelDriver = new ChannelDriver({
17357
18853
  agentId: state.agentId,
17358
18854
  port: state.port,
@@ -17362,6 +18858,7 @@ async function run(options) {
17362
18858
  conversationFilter: state.conversationFilter,
17363
18859
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
17364
18860
  sessionStoreHealth: state.sessionStoreHealth,
18861
+ recordBinding,
17365
18862
  // #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
17366
18863
  // `file_sync_disabled` on the ack, not silently ignored.
17367
18864
  fileSyncDirectories,
@@ -17541,6 +19038,7 @@ async function run(options) {
17541
19038
  }
17542
19039
  if (!state.running) return;
17543
19040
  scheduleSessionCleanup(state, channelDriver, options);
19041
+ scheduleSessionArchive(state);
17544
19042
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
17545
19043
  state.openaiUsageRearm = scheduleUsageReporting(state, {
17546
19044
  label: "OpenAI",
@@ -17636,8 +19134,8 @@ program.command("run").description("Connect to Evident and process messages").op
17636
19134
  "--opencode-start-timeout <seconds>",
17637
19135
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
17638
19136
  ).option(
17639
- "--opencode-version <v1|v2>",
17640
- "Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
19137
+ "--opencode-version <v1|v2|auto>",
19138
+ "OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
17641
19139
  ).option("--json", "Output in JSON format").option(
17642
19140
  "--session-cleanup-max-age <duration>",
17643
19141
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -17650,6 +19148,12 @@ program.command("run").description("Connect to Evident and process messages").op
17650
19148
  ).option(
17651
19149
  "--session-cleanup-interval <duration>",
17652
19150
  "How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
19151
+ ).option(
19152
+ "--session-archive",
19153
+ "Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
19154
+ ).option(
19155
+ "--session-archive-interval <duration>",
19156
+ "How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
17653
19157
  ).option(
17654
19158
  "--claude-usage-reporting <mode>",
17655
19159
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
@@ -17721,6 +19225,8 @@ program.command("run").description("Connect to Evident and process messages").op
17721
19225
  sessionCleanupMaxCount: options.sessionCleanupMaxCount,
17722
19226
  maxActiveSessions: options.maxActiveSessions,
17723
19227
  sessionCleanupInterval: options.sessionCleanupInterval,
19228
+ sessionArchive: options.sessionArchive,
19229
+ sessionArchiveInterval: options.sessionArchiveInterval,
17724
19230
  // Raw string — the resolver in run.ts single-sources parsing
17725
19231
  // (resolveClaudeUsageReportingMode).
17726
19232
  claudeUsageReporting: options.claudeUsageReporting,