@evident-ai/cli 3.6.1-dev.4db2683 → 3.6.1-dev.4f2b317

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.4db2683" : void 0) ?? process.env.npm_package_version ?? "unknown";
1448
+ var CLI_VERSION = (true ? "3.6.1-dev.4f2b317" : 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})`
4003
- );
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)) {
5039
+ `[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
5040
+ );
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(
@@ -6268,130 +7359,23 @@ async function promptOpenCodeInstall(interactive) {
6268
7359
  return action;
6269
7360
  }
6270
7361
 
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
- );
6369
- }
6370
- }
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 };
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,18 +8474,18 @@ 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) {
7502
- const { requestedPath, content, allowedDirectories, homeDir } = request3;
8486
+ const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
7503
8487
  const bytes = content.byteLength;
7504
- if (allowedDirectories.length === 0) {
8488
+ if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
7505
8489
  return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
7506
8490
  path: requestedPath,
7507
8491
  bytes
@@ -7526,10 +8510,10 @@ 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));
7532
- const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
8515
+ const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
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.", {
7535
8519
  path: realTarget,
@@ -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
  }
@@ -7604,6 +8588,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
7604
8588
  }
7605
8589
  return null;
7606
8590
  }
8591
+ async function findMatchingAllowedFile(allowedFiles, realTarget) {
8592
+ for (const entry of allowedFiles) {
8593
+ if (!isAbsolute2(entry)) {
8594
+ log("warn", "file_push_allowed_directory_skipped", {
8595
+ directory: entry,
8596
+ reason: "not_absolute_file"
8597
+ });
8598
+ continue;
8599
+ }
8600
+ const realParent = await realpathCreatingIfMissing(dirname9(entry));
8601
+ if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
8602
+ return realParent;
8603
+ }
8604
+ }
8605
+ return null;
8606
+ }
7607
8607
  async function realpathCreatingIfMissing(directory) {
7608
8608
  try {
7609
8609
  return await realpath(directory);
@@ -7637,13 +8637,13 @@ function contains(realDirectory, realTarget) {
7637
8637
  async function createMissingDirectories(existingAncestor, missingSegments) {
7638
8638
  let current = existingAncestor;
7639
8639
  for (const segment of missingSegments) {
7640
- current = join10(current, segment);
8640
+ current = join12(current, segment);
7641
8641
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
7642
8642
  await chmod(current, DIRECTORY_MODE);
7643
8643
  }
7644
8644
  }
7645
8645
  async function writeAtomically(realTarget, content) {
7646
- const temporaryPath = join10(dirname7(realTarget), `.evident-push-${randomUUID()}.tmp`);
8646
+ const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
7647
8647
  let handle;
7648
8648
  try {
7649
8649
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -7773,16 +8773,16 @@ var NOT_APPLIED = {
7773
8773
  opencodeAuthApplied: false
7774
8774
  };
7775
8775
  function isClaudeCredentialPath(requestedPath, homeDir) {
7776
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7777
- 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);
7778
8778
  }
7779
8779
  function isOpenCodeAuthPath(requestedPath, homeDir) {
7780
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7781
- 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);
7782
8782
  }
7783
8783
  async function applyOne(options, file) {
7784
8784
  const label = `${file.id.slice(0, 8)} (${file.path})`;
7785
- if (options.allowedDirectories.length === 0) {
8785
+ if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
7786
8786
  options.log({
7787
8787
  level: "warn",
7788
8788
  message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
@@ -7809,6 +8809,7 @@ async function applyOne(options, file) {
7809
8809
  requestedPath: file.path,
7810
8810
  content: download.content,
7811
8811
  allowedDirectories: options.allowedDirectories,
8812
+ allowedFiles: options.allowedFiles,
7812
8813
  homeDir: options.homeDir
7813
8814
  });
7814
8815
  } catch (err) {
@@ -7916,7 +8917,7 @@ function describe(err) {
7916
8917
  }
7917
8918
 
7918
8919
  // src/lib/vault-file-sync.ts
7919
- import { createHash } from "node:crypto";
8920
+ import { createHash as createHash2 } from "node:crypto";
7920
8921
  async function syncVaultFiles(options) {
7921
8922
  const files = await listVaultFiles(options);
7922
8923
  if (files === null) return { applied: 0 };
@@ -8012,7 +9013,7 @@ async function reconcileVaultFile(options, file) {
8012
9013
  }
8013
9014
  const content = await downloadVaultFile(options, file);
8014
9015
  if (content === null) return false;
8015
- const actualSha256 = createHash("sha256").update(content).digest("hex");
9016
+ const actualSha256 = createHash2("sha256").update(content).digest("hex");
8016
9017
  if (actualSha256 !== file.content_sha256) {
8017
9018
  options.log({
8018
9019
  level: "warn",
@@ -8060,6 +9061,7 @@ async function applyVaultFile(options, file, content) {
8060
9061
  requestedPath: file.destination_path,
8061
9062
  content,
8062
9063
  allowedDirectories: options.allowedDirectories,
9064
+ allowedFiles: options.allowedFiles,
8063
9065
  homeDir: options.homeDir
8064
9066
  });
8065
9067
  if (!outcome.ok) {
@@ -8212,6 +9214,7 @@ var ChannelDriver = class _ChannelDriver {
8212
9214
  conversationFilter;
8213
9215
  retry;
8214
9216
  log;
9217
+ recordBinding;
8215
9218
  fetchImpl;
8216
9219
  sleep;
8217
9220
  pausedPollIntervalMs;
@@ -8219,6 +9222,7 @@ var ChannelDriver = class _ChannelDriver {
8219
9222
  stuckQueuedMs;
8220
9223
  now;
8221
9224
  fileSyncDirectories;
9225
+ fileSyncExactFiles;
8222
9226
  homeDir;
8223
9227
  maxActiveSessions;
8224
9228
  watcherStallMs;
@@ -8573,6 +9577,7 @@ var ChannelDriver = class _ChannelDriver {
8573
9577
  this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
8574
9578
  this.log = config.log ?? (() => {
8575
9579
  });
9580
+ this.recordBinding = config.recordBinding;
8576
9581
  this.fetchImpl = withRequestTimeout(
8577
9582
  config.fetchImpl ?? fetch,
8578
9583
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
@@ -8588,6 +9593,7 @@ var ChannelDriver = class _ChannelDriver {
8588
9593
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
8589
9594
  this.now = config.now ?? (() => Date.now());
8590
9595
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
9596
+ this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
8591
9597
  this.homeDir = config.homeDir ?? homedir6();
8592
9598
  this.maxActiveSessions = config.maxActiveSessions;
8593
9599
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
@@ -8685,6 +9691,7 @@ var ChannelDriver = class _ChannelDriver {
8685
9691
  getAuthHeader: this.getAuthHeader,
8686
9692
  fetchImpl: this.fetchImpl,
8687
9693
  allowedDirectories: this.fileSyncDirectories,
9694
+ allowedFiles: this.fileSyncExactFiles,
8688
9695
  homeDir: this.homeDir,
8689
9696
  ackFailures: this.fileAckFailures,
8690
9697
  log: this.log
@@ -8695,6 +9702,7 @@ var ChannelDriver = class _ChannelDriver {
8695
9702
  getAuthHeader: this.getAuthHeader,
8696
9703
  fetchImpl: this.fetchImpl,
8697
9704
  allowedDirectories: this.fileSyncDirectories,
9705
+ allowedFiles: this.fileSyncExactFiles,
8698
9706
  homeDir: this.homeDir,
8699
9707
  outcomes: this.vaultFileOutcomes,
8700
9708
  log: this.log
@@ -8757,6 +9765,9 @@ var ChannelDriver = class _ChannelDriver {
8757
9765
  let cappedSkips = 0;
8758
9766
  for (const conv of conversations) {
8759
9767
  if (this.stopped) break;
9768
+ if (conv.opencode_session_id) {
9769
+ this.recordSessionBinding(conv.id, conv.opencode_session_id);
9770
+ }
8760
9771
  if (this.maxActiveSessions !== void 0) {
8761
9772
  const activeSessionIds = this.activeSessionIdsForCap();
8762
9773
  const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
@@ -8839,12 +9850,12 @@ var ChannelDriver = class _ChannelDriver {
8839
9850
  this.sessions.delete(conversationId);
8840
9851
  this.supersede(conversationId, sessionId);
8841
9852
  const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
8842
- 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.`;
8843
- 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);
8844
9855
  this.untrackableAckSessionIds.set(messageId, null);
8845
9856
  this.log({
8846
9857
  level: "error",
8847
- message: errorMessage3,
9858
+ message: errorMessage4,
8848
9859
  conversation_id: conversationId,
8849
9860
  message_id: messageId
8850
9861
  });
@@ -8852,7 +9863,7 @@ var ChannelDriver = class _ChannelDriver {
8852
9863
  conversationId,
8853
9864
  messageId,
8854
9865
  null,
8855
- errorMessage3
9866
+ errorMessage4
8856
9867
  );
8857
9868
  if (reportError !== null) {
8858
9869
  this.log({
@@ -9033,12 +10044,12 @@ var ChannelDriver = class _ChannelDriver {
9033
10044
  conversation_id: conv.id,
9034
10045
  message_id: message.id
9035
10046
  });
9036
- const errorMessage3 = this.untrackableAck.get(message.id);
10047
+ const errorMessage4 = this.untrackableAck.get(message.id);
9037
10048
  const reportError = await this.reportUntrackableAck(
9038
10049
  conv.id,
9039
10050
  message.id,
9040
10051
  null,
9041
- errorMessage3
10052
+ errorMessage4
9042
10053
  );
9043
10054
  if (reportError === null) {
9044
10055
  continue;
@@ -9126,7 +10137,7 @@ var ChannelDriver = class _ChannelDriver {
9126
10137
  const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
9127
10138
  const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
9128
10139
  const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
9129
- 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.`;
9130
10141
  this.log({
9131
10142
  level: "error",
9132
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`,
@@ -9134,7 +10145,7 @@ var ChannelDriver = class _ChannelDriver {
9134
10145
  message_id: message.id
9135
10146
  });
9136
10147
  try {
9137
- await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
10148
+ await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
9138
10149
  classification: {
9139
10150
  kind: "model_auth",
9140
10151
  providerId,
@@ -9179,7 +10190,13 @@ var ChannelDriver = class _ChannelDriver {
9179
10190
  false
9180
10191
  );
9181
10192
  }
9182
- 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(
9183
10200
  sessionId,
9184
10201
  () => sendPromptAsync(
9185
10202
  this.port,
@@ -9206,12 +10223,12 @@ var ChannelDriver = class _ChannelDriver {
9206
10223
  } catch (err) {
9207
10224
  if (err instanceof ChannelAuthError) throw err;
9208
10225
  if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
9209
- const errorMessage4 = err instanceof Error ? err.message : String(err);
9210
- this.untrackableAck.set(message.id, errorMessage4);
10226
+ const errorMessage5 = err instanceof Error ? err.message : String(err);
10227
+ this.untrackableAck.set(message.id, errorMessage5);
9211
10228
  this.untrackableAckSessionIds.set(message.id, null);
9212
10229
  this.log({
9213
10230
  level: "error",
9214
- 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}`,
9215
10232
  conversation_id: conv.id,
9216
10233
  message_id: message.id
9217
10234
  });
@@ -9219,7 +10236,7 @@ var ChannelDriver = class _ChannelDriver {
9219
10236
  conv.id,
9220
10237
  message.id,
9221
10238
  null,
9222
- errorMessage4
10239
+ errorMessage5
9223
10240
  );
9224
10241
  if (markError !== null) {
9225
10242
  this.log({
@@ -9254,7 +10271,11 @@ var ChannelDriver = class _ChannelDriver {
9254
10271
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
9255
10272
  break;
9256
10273
  }
9257
- 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);
9258
10279
  this.sessions.delete(conv.id);
9259
10280
  this.supersede(conv.id, sessionId);
9260
10281
  this.log({
@@ -9263,7 +10284,7 @@ var ChannelDriver = class _ChannelDriver {
9263
10284
  conversation_id: conv.id,
9264
10285
  message_id: message.id
9265
10286
  });
9266
- await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
10287
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
9267
10288
  this.log({
9268
10289
  level: "warn",
9269
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)}`,
@@ -9274,7 +10295,7 @@ var ChannelDriver = class _ChannelDriver {
9274
10295
  });
9275
10296
  this.log({
9276
10297
  level: "error",
9277
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
10298
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
9278
10299
  conversation_id: conv.id,
9279
10300
  message_id: message.id
9280
10301
  });
@@ -9847,6 +10868,14 @@ var ChannelDriver = class _ChannelDriver {
9847
10868
  this.dispatchNotStartedSignalled.set(message.id, branch);
9848
10869
  void this.postSignal(conv.id, message.id, "dispatch_not_started", { branch });
9849
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
+ }
9850
10879
  /**
9851
10880
  * Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
9852
10881
  * but its own PATCH to record it failed. Fires at most once per (message,
@@ -10061,6 +11090,7 @@ var ChannelDriver = class _ChannelDriver {
10061
11090
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
10062
11091
  }
10063
11092
  this.sessions.set(conv.id, bound);
11093
+ this.recordSessionBinding(conv.id, bound);
10064
11094
  return { sessionId: bound, created: false };
10065
11095
  }
10066
11096
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
@@ -10074,6 +11104,7 @@ var ChannelDriver = class _ChannelDriver {
10074
11104
  const directory = await this.resolveOpenCodeDirectory();
10075
11105
  const sessionId = await this.createOpenCodeSession(directory);
10076
11106
  this.sessions.set(conversationId, sessionId);
11107
+ this.recordSessionBinding(conversationId, sessionId);
10077
11108
  await this.persistSession(conversationId, sessionId).catch((err) => {
10078
11109
  this.log({
10079
11110
  level: "warn",
@@ -10083,6 +11114,18 @@ var ChannelDriver = class _ChannelDriver {
10083
11114
  });
10084
11115
  return sessionId;
10085
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
+ }
10086
11129
  /**
10087
11130
  * Lazily resolve (and cache) opencode's root directory via the selected client's
10088
11131
  * location lookup.
@@ -12183,12 +13226,12 @@ var ChannelDriver = class _ChannelDriver {
12183
13226
  conversation_id: row.conversation_id,
12184
13227
  message_id: row.id
12185
13228
  });
12186
- const errorMessage3 = this.untrackableAck.get(row.id);
13229
+ const errorMessage4 = this.untrackableAck.get(row.id);
12187
13230
  const reportError = await this.reportUntrackableAck(
12188
13231
  row.conversation_id,
12189
13232
  row.id,
12190
13233
  this.untrackableAckSessionIds.get(row.id) ?? null,
12191
- errorMessage3
13234
+ errorMessage4
12192
13235
  );
12193
13236
  if (reportError !== null) {
12194
13237
  this.log({
@@ -12473,7 +13516,13 @@ var ChannelDriver = class _ChannelDriver {
12473
13516
  let ocId;
12474
13517
  let preDispatchUserIds = null;
12475
13518
  try {
12476
- 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(
12477
13526
  sessionId,
12478
13527
  () => sendPromptAsync(
12479
13528
  this.port,
@@ -12501,14 +13550,14 @@ var ChannelDriver = class _ChannelDriver {
12501
13550
  this.awaitingReadopt.delete(row.id);
12502
13551
  if (err instanceof ChannelAuthError) throw err;
12503
13552
  if (this.isV2) {
12504
- const errorMessage3 = err instanceof Error ? err.message : String(err);
13553
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
12505
13554
  const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
12506
13555
  if (!invalidPromptAck) {
12507
13556
  const exists = await this.sessionExists(sessionId);
12508
13557
  if (exists === false) {
12509
13558
  this.log({
12510
13559
  level: "warn",
12511
- 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}`,
12512
13561
  conversation_id: row.conversation_id,
12513
13562
  message_id: row.id
12514
13563
  });
@@ -12518,7 +13567,7 @@ var ChannelDriver = class _ChannelDriver {
12518
13567
  if (exists === null) {
12519
13568
  this.log({
12520
13569
  level: "warn",
12521
- 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}`,
12522
13571
  conversation_id: row.conversation_id,
12523
13572
  message_id: row.id
12524
13573
  });
@@ -12526,12 +13575,16 @@ var ChannelDriver = class _ChannelDriver {
12526
13575
  return;
12527
13576
  }
12528
13577
  } else {
12529
- this.untrackableAck.set(row.id, errorMessage3);
13578
+ this.untrackableAck.set(row.id, errorMessage4);
12530
13579
  this.untrackableAckSessionIds.set(row.id, sessionId);
12531
13580
  }
13581
+ if (err instanceof OpenCodeV2ModelSwitchError) {
13582
+ await this.failV2ModelSwitch(readoptConv, readoptMessage, sessionId, err);
13583
+ return;
13584
+ }
12532
13585
  this.log({
12533
13586
  level: "error",
12534
- 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}`,
12535
13588
  conversation_id: row.conversation_id,
12536
13589
  message_id: row.id
12537
13590
  });
@@ -12540,7 +13593,7 @@ var ChannelDriver = class _ChannelDriver {
12540
13593
  row.conversation_id,
12541
13594
  row.id,
12542
13595
  sessionId,
12543
- errorMessage3
13596
+ errorMessage4
12544
13597
  );
12545
13598
  if (markError !== null) {
12546
13599
  this.log({
@@ -12551,7 +13604,7 @@ var ChannelDriver = class _ChannelDriver {
12551
13604
  });
12552
13605
  }
12553
13606
  } else {
12554
- await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
13607
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
12555
13608
  (markErr) => {
12556
13609
  this.log({
12557
13610
  level: "warn",
@@ -13535,9 +14588,9 @@ var ChannelDriver = class _ChannelDriver {
13535
14588
  * trackable. Callers use the returned error to keep their path-specific logging
13536
14589
  * while the same report payload can be retried on a later drain.
13537
14590
  */
13538
- async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage3) {
14591
+ async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
13539
14592
  try {
13540
- await this.markFailed(conversationId, messageId, sessionId, errorMessage3);
14593
+ await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
13541
14594
  return null;
13542
14595
  } catch (err) {
13543
14596
  this.signalUntrackableAck(conversationId, messageId);
@@ -13606,6 +14659,38 @@ var ChannelDriver = class _ChannelDriver {
13606
14659
  }
13607
14660
  this.clearSubagentInvocationCaches(messageId);
13608
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
+ }
13609
14694
  /**
13610
14695
  * Classify an errored turn and extract its bounded error shape for `markFailed`.
13611
14696
  *
@@ -14049,7 +15134,7 @@ import ora3 from "ora";
14049
15134
  import { select as select3 } from "@inquirer/prompts";
14050
15135
  function unknownPasswordError2(port) {
14051
15136
  return new Error(
14052
- `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.`
14053
15138
  );
14054
15139
  }
14055
15140
  var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
@@ -14067,9 +15152,9 @@ async function ensureOpenCode2Running(ctx) {
14067
15152
  password: null
14068
15153
  };
14069
15154
  }
14070
- if (!isOpenCode2Installed()) {
15155
+ if (!ctx.command && !isOpenCode2Installed()) {
14071
15156
  throw new Error(
14072
- `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`."
14073
15158
  );
14074
15159
  }
14075
15160
  let port = ctx.port;
@@ -14097,7 +15182,8 @@ Port ${port} is already in use.`));
14097
15182
  if (!ctx.interactive) {
14098
15183
  ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
14099
15184
  const { child: proc, password } = await startOpenCode2(port, {
14100
- inheritStdio: ctx.inheritStdio
15185
+ inheritStdio: ctx.inheritStdio,
15186
+ command: ctx.command
14101
15187
  });
14102
15188
  const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
14103
15189
  if (!health.healthy) {
@@ -14118,13 +15204,14 @@ Port ${port} is already in use.`));
14118
15204
  password
14119
15205
  };
14120
15206
  }
15207
+ const command = ctx.command ?? "opencode2";
14121
15208
  const action = await select3({
14122
15209
  message: "OpenCode V2 is not running. What would you like to do?",
14123
15210
  choices: [
14124
15211
  {
14125
15212
  name: "Start OpenCode V2 for me",
14126
15213
  value: "start",
14127
- description: `Run 'opencode2 serve --port ${port}'`
15214
+ description: `Run '${command} serve --port ${port}'`
14128
15215
  },
14129
15216
  {
14130
15217
  name: "Show me the command",
@@ -14142,14 +15229,15 @@ Port ${port} is already in use.`));
14142
15229
  blank();
14143
15230
  console.log(chalk6.bold("Run this command in another terminal:"));
14144
15231
  blank();
14145
- console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
15232
+ console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
14146
15233
  blank();
14147
15234
  throw new Error("Please start OpenCode V2 manually");
14148
15235
  }
14149
15236
  if (action === "start") {
14150
15237
  const spinner = ora3("Starting OpenCode V2...").start();
14151
15238
  const { child: proc, password } = await startOpenCode2(port, {
14152
- inheritStdio: ctx.inheritStdio
15239
+ inheritStdio: ctx.inheritStdio,
15240
+ command: ctx.command
14153
15241
  });
14154
15242
  const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
14155
15243
  if (!health.healthy) {
@@ -14175,7 +15263,7 @@ Port ${port} is already in use.`));
14175
15263
  }
14176
15264
 
14177
15265
  // src/lib/runner-credentials.ts
14178
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync5 } from "node:fs";
15266
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
14179
15267
  import { spawn as spawn5 } from "node:child_process";
14180
15268
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
14181
15269
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
@@ -14418,8 +15506,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
14418
15506
  }
14419
15507
  try {
14420
15508
  env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
14421
- writeFileSync5(GIT_CONFIG_GLOBAL, "");
14422
- 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 });
14423
15511
  chmodSync2(GIT_CREDENTIAL_HELPER, 448);
14424
15512
  const config = [
14425
15513
  ["user.name", env.GIT_USER_NAME ?? "evident-bot"],
@@ -14452,7 +15540,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
14452
15540
  // src/lib/opencode/config-overlay.ts
14453
15541
  import { execFileSync as execFileSync2 } from "node:child_process";
14454
15542
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
14455
- 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";
14456
15544
  function parseConfigContent(content) {
14457
15545
  if (!content?.trim()) return {};
14458
15546
  const parsed = JSON.parse(content);
@@ -14482,7 +15570,7 @@ function applyRunnerOpenCodeConfig({
14482
15570
  return;
14483
15571
  }
14484
15572
  const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
14485
- const target = isFile(join12(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
15573
+ const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
14486
15574
  if (!isFile(source)) {
14487
15575
  log3(
14488
15576
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -14490,7 +15578,7 @@ function applyRunnerOpenCodeConfig({
14490
15578
  );
14491
15579
  return;
14492
15580
  }
14493
- copyFileSync(source, join12(cwd, target));
15581
+ copyFileSync(source, join14(cwd, target));
14494
15582
  try {
14495
15583
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
14496
15584
  stdio: "ignore"
@@ -14499,13 +15587,13 @@ function applyRunnerOpenCodeConfig({
14499
15587
  const detail = error2 instanceof Error ? error2.message : String(error2);
14500
15588
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
14501
15589
  }
14502
- log3(`Applied runner OpenCode config ${source} to ${join12(cwd, target)}`);
15590
+ log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
14503
15591
  }
14504
15592
 
14505
15593
  // src/lib/opencode/runtime-provenance.ts
14506
- import { readFileSync as readFileSync8, readdirSync } from "node:fs";
15594
+ import { readFileSync as readFileSync11, readdirSync } from "node:fs";
14507
15595
  import { homedir as homedir7 } from "node:os";
14508
- import { join as join13 } from "node:path";
15596
+ import { join as join15 } from "node:path";
14509
15597
  var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
14510
15598
  var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
14511
15599
  function errorKind(error2) {
@@ -14573,7 +15661,7 @@ function parseJsonc(value) {
14573
15661
  return JSON.parse(output);
14574
15662
  }
14575
15663
  function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14576
- return join13(env.XDG_CACHE_HOME ?? join13(homeDir, ".cache"), "opencode");
15664
+ return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
14577
15665
  }
14578
15666
  function readConfiguredPluginSpecs({
14579
15667
  cwd = process.cwd(),
@@ -14582,10 +15670,10 @@ function readConfiguredPluginSpecs({
14582
15670
  let config;
14583
15671
  try {
14584
15672
  try {
14585
- config = parseJsonc(readFileSync8(join13(cwd, "opencode.jsonc"), "utf8"));
15673
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
14586
15674
  } catch (error2) {
14587
15675
  if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14588
- config = parseJsonc(readFileSync8(join13(cwd, "opencode.json"), "utf8"));
15676
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
14589
15677
  }
14590
15678
  } catch (error2) {
14591
15679
  log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
@@ -14613,7 +15701,7 @@ function resolveInstalledPluginVersions({
14613
15701
  homeDir = homedir7(),
14614
15702
  log: log3
14615
15703
  }) {
14616
- const packagesRoot = join13(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
15704
+ const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14617
15705
  let directories;
14618
15706
  try {
14619
15707
  directories = readdirSync(packagesRoot, { withFileTypes: true });
@@ -14634,8 +15722,8 @@ function resolveInstalledPluginVersions({
14634
15722
  let resolvedVersion = null;
14635
15723
  try {
14636
15724
  const packageJson = JSON.parse(
14637
- readFileSync8(
14638
- join13(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
15725
+ readFileSync11(
15726
+ join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14639
15727
  "utf8"
14640
15728
  )
14641
15729
  );
@@ -14656,7 +15744,7 @@ function resolveInstalledPluginVersions({
14656
15744
  }
14657
15745
 
14658
15746
  // src/lib/credential-sync.ts
14659
- import { renameSync, writeFileSync as writeFileSync6 } from "node:fs";
15747
+ import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
14660
15748
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
14661
15749
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
14662
15750
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -14666,7 +15754,7 @@ var MAX_FLUSH_PASSES = 2;
14666
15754
  function outcomesWith(outcome) {
14667
15755
  return { claude: outcome, opencode: outcome };
14668
15756
  }
14669
- function errorMessage2(error2) {
15757
+ function errorMessage3(error2) {
14670
15758
  return error2 instanceof Error ? error2.message : String(error2);
14671
15759
  }
14672
15760
  function waitForSettlement(promise, timeoutMs) {
@@ -14690,10 +15778,10 @@ function writeMarker(markerPath, outcomes, log3) {
14690
15778
  `;
14691
15779
  const temporaryPath = `${markerPath}.tmp`;
14692
15780
  try {
14693
- writeFileSync6(temporaryPath, body, { mode: 384 });
15781
+ writeFileSync8(temporaryPath, body, { mode: 384 });
14694
15782
  renameSync(temporaryPath, markerPath);
14695
15783
  } catch (error2) {
14696
- log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
15784
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
14697
15785
  }
14698
15786
  }
14699
15787
  function intervalSeconds(env, log3) {
@@ -14725,7 +15813,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
14725
15813
  },
14726
15814
  (error2) => {
14727
15815
  failed = true;
14728
- log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
15816
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
14729
15817
  }
14730
15818
  );
14731
15819
  const abortTimer = setTimeout(() => controller.abort(), remainingMs);
@@ -14785,7 +15873,7 @@ function createCredentialSync({
14785
15873
  outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
14786
15874
  } catch (error2) {
14787
15875
  outcomes[store] = "failed";
14788
- log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
15876
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
14789
15877
  }
14790
15878
  }
14791
15879
  const failed = STORES.some((store) => outcomes[store] === "failed");
@@ -14908,14 +15996,21 @@ function request2(options) {
14908
15996
  function safeFailure(operation, status2) {
14909
15997
  return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
14910
15998
  }
14911
- function logTransportFailure(options, operation, error2, authorization) {
14912
- let detail = error2 instanceof Error ? error2.message : String(error2);
15999
+ function redactAuthorizedText(message, authorization) {
16000
+ let redacted = message;
14913
16001
  const credential = authorization.split(/\s+/, 2)[1];
14914
16002
  for (const secret of [authorization, credential]) {
14915
- if (secret) detail = detail.replaceAll(secret, "[redacted]");
16003
+ if (secret) redacted = redacted.replaceAll(secret, "[redacted]");
14916
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
+ );
14917
16012
  options.log(
14918
- `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${redactDiagnosticText(detail)}`,
16013
+ `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${detail}`,
14919
16014
  "debug"
14920
16015
  );
14921
16016
  }
@@ -14935,6 +16030,18 @@ function readSetup(value) {
14935
16030
  }
14936
16031
  return setup;
14937
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
+ }
14938
16045
  function clearLocalSetup(options) {
14939
16046
  const env = options.env ?? process.env;
14940
16047
  const store = options.store ?? defaultStore;
@@ -14966,7 +16073,14 @@ async function discoverManagedInferenceSetup(options) {
14966
16073
  return { kind: "none" };
14967
16074
  }
14968
16075
  if (!response.ok) {
14969
- 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
+ };
14970
16084
  }
14971
16085
  const body = await responseJson(response);
14972
16086
  const setup = readSetup(body?.setup);
@@ -15066,7 +16180,27 @@ async function reaffirmStored(options, credential) {
15066
16180
  if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
15067
16181
  return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
15068
16182
  }
15069
- 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);
15070
16204
  return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
15071
16205
  }
15072
16206
  async function prepareManagedInferenceSetup(options, discovery, health) {
@@ -15100,10 +16234,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
15100
16234
  const setup = readSetup(body?.setup);
15101
16235
  const token = body?.inference_token;
15102
16236
  const inferenceBaseUrl = body?.inference_base_url;
15103
- const modelDescriptors = body?.model_descriptors;
15104
- if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || !Array.isArray(modelDescriptors) || !modelDescriptors.every(
15105
- (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
15106
- )) {
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) {
15107
16239
  return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
15108
16240
  }
15109
16241
  const credential = {
@@ -15172,33 +16304,49 @@ function resolveLogLevel(options) {
15172
16304
  }
15173
16305
  return "info";
15174
16306
  }
15175
- function resolveFileSyncDirectories(raw, homeDir) {
15176
- const directories = [];
16307
+ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
16308
+ const trimmed = entry.trim();
16309
+ if (trimmed === "") {
16310
+ throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
16311
+ }
16312
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
16313
+ if (!isAbsolute4(expanded)) {
16314
+ throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
16315
+ }
16316
+ const normalized = resolvePath2(expanded);
16317
+ if (parse(normalized).root === normalized) {
16318
+ throw new Error(
16319
+ `${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
16320
+ );
16321
+ }
16322
+ return normalized;
16323
+ }
16324
+ function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
16325
+ const paths = [];
15177
16326
  for (const entry of raw ?? []) {
15178
- const trimmed = entry.trim();
15179
- if (trimmed === "") {
15180
- throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
15181
- }
15182
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join14(homeDir, trimmed.slice(2)) : trimmed;
15183
- if (!isAbsolute4(expanded)) {
15184
- throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
15185
- }
15186
- const normalized = resolvePath2(expanded);
15187
- if (parse(normalized).root === normalized) {
15188
- throw new Error(
15189
- `--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
15190
- );
15191
- }
15192
- if (!directories.includes(normalized)) {
15193
- directories.push(normalized);
15194
- }
16327
+ const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
16328
+ if (!paths.includes(normalized)) paths.push(normalized);
15195
16329
  }
15196
- if (directories.length > MAX_FILE_SYNC_DIRECTORIES) {
16330
+ if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
15197
16331
  throw new Error(
15198
- `--enable-file-sync-to accepts at most ${MAX_FILE_SYNC_DIRECTORIES} directories; got ${directories.length}`
16332
+ `${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
15199
16333
  );
15200
16334
  }
15201
- return directories;
16335
+ return paths;
16336
+ }
16337
+ function resolveFileSyncDirectories(raw, homeDir) {
16338
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
16339
+ noun: "directory",
16340
+ plural: "directories",
16341
+ rootExample: "~/.claude"
16342
+ });
16343
+ }
16344
+ function resolveFileSyncExactFiles(raw, homeDir) {
16345
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
16346
+ noun: "file",
16347
+ plural: "files",
16348
+ rootExample: "~/.local/share/opencode/mcp-auth.json"
16349
+ });
15202
16350
  }
15203
16351
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
15204
16352
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
@@ -15214,13 +16362,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
15214
16362
  raw = env[OPENCODE_VERSION_ENV];
15215
16363
  source = OPENCODE_VERSION_ENV;
15216
16364
  } else {
15217
- return { version: "v1", warnings: [] };
16365
+ return { version: "auto", warnings: [] };
15218
16366
  }
15219
16367
  const normalized = raw.trim().toLowerCase();
15220
- if (normalized !== "v1" && normalized !== "v2") {
16368
+ if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
15221
16369
  return {
15222
- version: "v1",
15223
- 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`]
15224
16372
  };
15225
16373
  }
15226
16374
  return { version: normalized, warnings: [] };
@@ -15730,8 +16878,8 @@ async function driveChannels(state, driver) {
15730
16878
  state.running = false;
15731
16879
  break;
15732
16880
  }
15733
- const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
15734
- 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}` });
15735
16883
  if (state.interactive) displayStatus(state);
15736
16884
  if (driver.hasInFlightWatchers()) {
15737
16885
  consecutiveDrainFailures = 0;
@@ -15821,6 +16969,7 @@ async function driveChannels(state, driver) {
15821
16969
  }
15822
16970
  }
15823
16971
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
16972
+ var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
15824
16973
  var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
15825
16974
  var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
15826
16975
  var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
@@ -15835,7 +16984,7 @@ function shouldWarnForReclaimSkip(reason) {
15835
16984
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
15836
16985
  }
15837
16986
  function sessionDbPath() {
15838
- return join14(homedir8(), ".local", "share", "opencode", "opencode.db");
16987
+ return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
15839
16988
  }
15840
16989
  function logSessionCleanupActivity(state, phase, entry) {
15841
16990
  logActivity(state, {
@@ -15844,6 +16993,63 @@ function logSessionCleanupActivity(state, phase, entry) {
15844
16993
  metadata: { ...entry.metadata, phase }
15845
16994
  });
15846
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
+ }
15847
17053
  async function drainSessionDeletionReports(state) {
15848
17054
  const path = sessionDeletionReportPath(homedir8(), process.env);
15849
17055
  let delivered = 0;
@@ -15941,6 +17147,7 @@ async function runSweep(state, driver, config) {
15941
17147
  let failedOtherStatus = 0;
15942
17148
  let failedThrew = 0;
15943
17149
  let skippedNewlyActive = 0;
17150
+ let skippedArchive = 0;
15944
17151
  let remaining = 0;
15945
17152
  const deletionRecords = [];
15946
17153
  let protectedNow = driver.protectedSessionIds();
@@ -15952,6 +17159,7 @@ async function runSweep(state, driver, config) {
15952
17159
  const lastActivityById = new Map(
15953
17160
  snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
15954
17161
  );
17162
+ const sessionById = new Map(sessions.map((session) => [session.id, session]));
15955
17163
  const toDelete = selectSessionsToDelete(snapshots, {
15956
17164
  maxAgeMs: config.maxAgeMs,
15957
17165
  maxCount: config.maxCount,
@@ -15974,6 +17182,42 @@ async function runSweep(state, driver, config) {
15974
17182
  });
15975
17183
  continue;
15976
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
+ }
15977
17221
  const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
15978
17222
  if (outcome.ok) {
15979
17223
  deleted++;
@@ -16037,10 +17281,11 @@ async function runSweep(state, driver, config) {
16037
17281
  }
16038
17282
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
16039
17283
  const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
17284
+ const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
16040
17285
  const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
16041
17286
  logActivity(state, {
16042
17287
  type: "info",
16043
- 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})`
16044
17289
  });
16045
17290
  }
16046
17291
  if (deletionRecords.length > 0) {
@@ -16078,6 +17323,7 @@ async function runSweep(state, driver, config) {
16078
17323
  sessions_failed_other: failedOtherStatus,
16079
17324
  sessions_failed_threw: failedThrew,
16080
17325
  sessions_skipped_active: skippedNewlyActive,
17326
+ sessions_archive_held: skippedArchive,
16081
17327
  sessions_remaining: remaining,
16082
17328
  ...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
16083
17329
  };
@@ -16209,6 +17455,186 @@ function scheduleSessionCleanup(state, driver, options) {
16209
17455
  );
16210
17456
  state.sessionCleanupTimers.push(interval, firstSweep);
16211
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
+ }
16212
17638
  function scheduleUsageReporting(state, params) {
16213
17639
  const { mode, warnings } = params.resolved;
16214
17640
  for (const warning2 of warnings) {
@@ -16483,6 +17909,12 @@ async function cleanup(state, opts = {}) {
16483
17909
  clearTimeout(timer);
16484
17910
  }
16485
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"));
16486
17918
  state.stopOpenCodeLogTail?.();
16487
17919
  state.stopOpenCodeLogTail = null;
16488
17920
  if (state.claudeUsageTimer) {
@@ -16550,6 +17982,15 @@ async function cleanup(state, opts = {}) {
16550
17982
  await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
16551
17983
  state.connection = null;
16552
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
+ }
16553
17994
  if (state.opencodeProcess) {
16554
17995
  const opencodeProcess = state.opencodeProcess;
16555
17996
  const result = await timeShutdownPhase(
@@ -16621,9 +18062,11 @@ async function run(options) {
16621
18062
  const interactive = isInteractive(options.json);
16622
18063
  let logLevel;
16623
18064
  let fileSyncDirectories;
18065
+ let fileSyncExactFiles;
16624
18066
  try {
16625
18067
  logLevel = resolveLogLevel(options);
16626
18068
  fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
18069
+ fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
16627
18070
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
16628
18071
  throw new Error(
16629
18072
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -16671,6 +18114,18 @@ async function run(options) {
16671
18114
  messageCount: 0,
16672
18115
  lastProxiedActivityAt: null,
16673
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,
16674
18129
  sessionDeleteFailures: /* @__PURE__ */ new Map(),
16675
18130
  claudeUsageTimer: null,
16676
18131
  claudeUsageRearm: null,
@@ -16684,6 +18139,14 @@ async function run(options) {
16684
18139
  };
16685
18140
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
16686
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
+ }
16687
18150
  if (options.credentialSyncMarker) {
16688
18151
  state.credentialSync = createCredentialSync({
16689
18152
  markerPath: options.credentialSyncMarker,
@@ -16699,7 +18162,11 @@ async function run(options) {
16699
18162
  }
16700
18163
  if (fileSyncDirectories.length > 0) {
16701
18164
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
16702
- } else {
18165
+ }
18166
+ if (fileSyncExactFiles.length > 0) {
18167
+ log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
18168
+ }
18169
+ if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
16703
18170
  log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
16704
18171
  }
16705
18172
  if (!options.runner && options.agent) {
@@ -17001,13 +18468,75 @@ async function run(options) {
17001
18468
  for (const warning2 of opencodeStartTimeoutWarnings) {
17002
18469
  logActivity(state, { type: "info", level: "warn", message: warning2 });
17003
18470
  }
17004
- const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
17005
- options,
17006
- process.env
17007
- );
18471
+ const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
17008
18472
  for (const warning2 of opencodeVersionWarnings) {
17009
18473
  logActivity(state, { type: "info", level: "warn", message: warning2 });
17010
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
+ });
17011
18540
  const managedSetupOptions = {
17012
18541
  agentId: state.agentId,
17013
18542
  apiUrl: getApiUrlConfig(),
@@ -17028,7 +18557,7 @@ async function run(options) {
17028
18557
  managedPreparation = managedDiscovery;
17029
18558
  managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
17030
18559
  } else if (managedDiscovery.kind === "setup") {
17031
- 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);
17032
18561
  managedPreparation = await prepareManagedInferenceSetup(
17033
18562
  managedSetupOptions,
17034
18563
  managedDiscovery,
@@ -17053,13 +18582,14 @@ async function run(options) {
17053
18582
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
17054
18583
  const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
17055
18584
  try {
17056
- const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
18585
+ const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
17057
18586
  port: state.port,
17058
18587
  interactive: state.interactive,
17059
18588
  agentId: state.agentId,
17060
18589
  log: (message) => log2(state, message),
17061
18590
  startTimeoutMs: opencodeStartTimeoutMs,
17062
- inheritStdio: Boolean(options.opencodePidFile)
18591
+ inheritStdio: Boolean(options.opencodePidFile),
18592
+ command: opencodeV2Command
17063
18593
  }) : await ensureOpenCodeRunning({
17064
18594
  port: state.port,
17065
18595
  interactive: state.interactive,
@@ -17071,7 +18601,7 @@ async function run(options) {
17071
18601
  state.port = oc.port;
17072
18602
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
17073
18603
  state.opencodeVersion = oc.version;
17074
- state.opencodeApiVersion = opencodeVersion;
18604
+ state.opencodeApiVersion = opencodeMajor;
17075
18605
  let configuredPlugins = [];
17076
18606
  let resolvedPlugins = [];
17077
18607
  try {
@@ -17093,13 +18623,14 @@ async function run(options) {
17093
18623
  metadata: {
17094
18624
  opencode_version: state.opencodeVersion,
17095
18625
  opencode_api_version: state.opencodeApiVersion,
18626
+ opencode_detection_source: opencodeDetectionSource,
17096
18627
  configured_plugins: JSON.stringify(configuredPlugins),
17097
18628
  resolved_plugins: JSON.stringify(resolvedPlugins)
17098
18629
  },
17099
18630
  source: "cli.runtime-provenance"
17100
18631
  });
17101
18632
  let opencodePassword = null;
17102
- if (opencodeVersion === "v2" && "password" in oc) {
18633
+ if (opencodeMajor === "v2" && "password" in oc) {
17103
18634
  const value = oc.password;
17104
18635
  if (typeof value === "string" || value === null) opencodePassword = value;
17105
18636
  }
@@ -17112,7 +18643,7 @@ async function run(options) {
17112
18643
  state.opencodeClient = openCodeClient;
17113
18644
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
17114
18645
  try {
17115
- writeFileSync7(options.opencodePidFile, `${oc.process.pid}
18646
+ writeFileSync9(options.opencodePidFile, `${oc.process.pid}
17116
18647
  `, { mode: 384 });
17117
18648
  chmodSync3(options.opencodePidFile, 384);
17118
18649
  } catch (error2) {
@@ -17222,7 +18753,7 @@ async function run(options) {
17222
18753
  let existingPid;
17223
18754
  if (existsSync3(options.litestreamPidFile)) {
17224
18755
  try {
17225
- const rawPid = readFileSync9(options.litestreamPidFile, "utf8").trim();
18756
+ const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
17226
18757
  const parsedPid = Number(rawPid);
17227
18758
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
17228
18759
  existingPid = parsedPid;
@@ -17259,7 +18790,7 @@ async function run(options) {
17259
18790
  });
17260
18791
  try {
17261
18792
  if (litestreamProcess.pid !== void 0) {
17262
- writeFileSync7(options.litestreamPidFile, `${litestreamProcess.pid}
18793
+ writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
17263
18794
  `, {
17264
18795
  mode: 384
17265
18796
  });
@@ -17309,6 +18840,15 @@ async function run(options) {
17309
18840
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
17310
18841
  }
17311
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;
17312
18852
  const channelDriver = new ChannelDriver({
17313
18853
  agentId: state.agentId,
17314
18854
  port: state.port,
@@ -17318,9 +18858,11 @@ async function run(options) {
17318
18858
  conversationFilter: state.conversationFilter,
17319
18859
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
17320
18860
  sessionStoreHealth: state.sessionStoreHealth,
17321
- // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
17322
- // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
18861
+ recordBinding,
18862
+ // #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
18863
+ // `file_sync_disabled` on the ack, not silently ignored.
17323
18864
  fileSyncDirectories,
18865
+ fileSyncExactFiles,
17324
18866
  homeDir: homedir8(),
17325
18867
  maxActiveSessions,
17326
18868
  log: (entry) => (
@@ -17496,6 +19038,7 @@ async function run(options) {
17496
19038
  }
17497
19039
  if (!state.running) return;
17498
19040
  scheduleSessionCleanup(state, channelDriver, options);
19041
+ scheduleSessionArchive(state);
17499
19042
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
17500
19043
  state.openaiUsageRearm = scheduleUsageReporting(state, {
17501
19044
  label: "OpenAI",
@@ -17591,8 +19134,8 @@ program.command("run").description("Connect to Evident and process messages").op
17591
19134
  "--opencode-start-timeout <seconds>",
17592
19135
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
17593
19136
  ).option(
17594
- "--opencode-version <v1|v2>",
17595
- "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"
17596
19139
  ).option("--json", "Output in JSON format").option(
17597
19140
  "--session-cleanup-max-age <duration>",
17598
19141
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -17605,6 +19148,12 @@ program.command("run").description("Connect to Evident and process messages").op
17605
19148
  ).option(
17606
19149
  "--session-cleanup-interval <duration>",
17607
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"
17608
19157
  ).option(
17609
19158
  "--claude-usage-reporting <mode>",
17610
19159
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
@@ -17619,6 +19168,11 @@ program.command("run").description("Connect to Evident and process messages").op
17619
19168
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
17620
19169
  (value, previous) => previous.concat([value]),
17621
19170
  []
19171
+ ).option(
19172
+ "--enable-file-sync-to-exact-file <path>",
19173
+ "Allow Evident to write files to exactly this path (repeatable).",
19174
+ (value, previous) => previous.concat([value]),
19175
+ []
17622
19176
  ).option(
17623
19177
  "--tunnel-ready-file <path>",
17624
19178
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
@@ -17671,6 +19225,8 @@ program.command("run").description("Connect to Evident and process messages").op
17671
19225
  sessionCleanupMaxCount: options.sessionCleanupMaxCount,
17672
19226
  maxActiveSessions: options.maxActiveSessions,
17673
19227
  sessionCleanupInterval: options.sessionCleanupInterval,
19228
+ sessionArchive: options.sessionArchive,
19229
+ sessionArchiveInterval: options.sessionArchiveInterval,
17674
19230
  // Raw string — the resolver in run.ts single-sources parsing
17675
19231
  // (resolveClaudeUsageReportingMode).
17676
19232
  claudeUsageReporting: options.claudeUsageReporting,
@@ -17681,6 +19237,9 @@ program.command("run").description("Connect to Evident and process messages").op
17681
19237
  // Raw values — expansion/validation is single-sourced in run.ts's
17682
19238
  // resolveFileSyncDirectories.
17683
19239
  enableFileSyncTo: options.enableFileSyncTo,
19240
+ // Raw values — expansion/validation is single-sourced in run.ts's
19241
+ // resolveFileSyncExactFiles.
19242
+ enableFileSyncToExactFile: options.enableFileSyncToExactFile,
17684
19243
  tunnelReadyFile: options.tunnelReadyFile,
17685
19244
  litestreamConfig: options.litestreamConfig,
17686
19245
  opencodePidFile: options.opencodePidFile,