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

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
@@ -1256,6 +1256,36 @@ function effectiveCorrelatedRun(messages, userMessageId) {
1256
1256
  function extractReplyTextForMessage(messages, opencodeUserMessageId) {
1257
1257
  return lastText(effectiveCorrelatedRun(messages, opencodeUserMessageId));
1258
1258
  }
1259
+ function isRecord(value) {
1260
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1261
+ }
1262
+ function projectV2ToolContent(content) {
1263
+ if (!isRecord(content) || content.type !== "tool" || typeof content.name !== "string" || !isRecord(content.state)) {
1264
+ return null;
1265
+ }
1266
+ const { state } = content;
1267
+ const status2 = state.status;
1268
+ if (status2 !== "streaming" && status2 !== "running" && status2 !== "completed" && status2 !== "error") {
1269
+ return null;
1270
+ }
1271
+ const projectedState = {
1272
+ status: status2 === "streaming" ? "pending" : status2
1273
+ };
1274
+ if (status2 !== "streaming" && isRecord(state.input)) projectedState.input = state.input;
1275
+ const output = Array.isArray(state.content) ? state.content.filter(
1276
+ (entry) => isRecord(entry) && entry.type === "text" && typeof entry.text === "string"
1277
+ ).map((entry) => entry.text) : [];
1278
+ if (output.length > 0) projectedState.output = output.join("\n");
1279
+ if (status2 === "error" && isRecord(state.error) && typeof state.error.message === "string") {
1280
+ projectedState.error = state.error.message;
1281
+ }
1282
+ return {
1283
+ type: "tool",
1284
+ tool: content.name,
1285
+ ...typeof content.id === "string" ? { callID: content.id } : {},
1286
+ state: projectedState
1287
+ };
1288
+ }
1259
1289
 
1260
1290
  // ../../packages/types/src/telemetry/index.ts
1261
1291
  var TelemetryEventTypes = {
@@ -1445,7 +1475,7 @@ import ora4 from "ora";
1445
1475
  import { select as select4 } from "@inquirer/prompts";
1446
1476
 
1447
1477
  // src/lib/telemetry.ts
1448
- var CLI_VERSION = (true ? "3.6.1-dev.4db2683" : void 0) ?? process.env.npm_package_version ?? "unknown";
1478
+ var CLI_VERSION = (true ? "3.6.1-dev.5d5fa2a" : void 0) ?? process.env.npm_package_version ?? "unknown";
1449
1479
  function getCliVersion() {
1450
1480
  return CLI_VERSION;
1451
1481
  }
@@ -2195,6 +2225,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
2195
2225
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
2196
2226
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
2197
2227
  "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2228
+ "cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2198
2229
  "cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
2199
2230
  };
2200
2231
  var SEVERITY_BY_LEVEL = {
@@ -2757,153 +2788,1347 @@ function boundedError(error2) {
2757
2788
  return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
2758
2789
  }
2759
2790
 
2760
- // src/lib/opencode/auth.ts
2761
- function buildOpenCodeBasicAuthHeader(password) {
2762
- return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
2791
+ // src/lib/opencode/session-binding-ledger.ts
2792
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2793
+ import { dirname as dirname5, join as join5 } from "node:path";
2794
+ var SESSION_BINDING_LEDGER_MAX_RECORDS = 1e3;
2795
+ var MAX_DIAGNOSTIC_LENGTH2 = 200;
2796
+ function sessionBindingLedgerPath(homeDir, env) {
2797
+ const override = env.EVIDENT_SESSION_BINDING_LEDGER?.trim();
2798
+ return override || join5(homeDir, ".local", "state", "evident", "session-bindings.jsonl");
2799
+ }
2800
+ function appendSessionBinding(path, record, log3) {
2801
+ mkdirSync2(dirname5(path), { recursive: true });
2802
+ const current = readSessionBindingRecords(path, log3);
2803
+ if (current.readError !== null) {
2804
+ throw new Error(`cannot enforce the ledger limit after reading ${path}: ${current.readError}`);
2805
+ }
2806
+ if (current.records.some(
2807
+ (existing) => existing.conversation_id === record.conversation_id && existing.opencode_session_id === record.opencode_session_id
2808
+ )) {
2809
+ return;
2810
+ }
2811
+ const kept = [...current.records, record].slice(-SESSION_BINDING_LEDGER_MAX_RECORDS);
2812
+ if (kept.length > current.records.length) {
2813
+ appendFileSync2(path, `${JSON.stringify(record)}
2814
+ `, "utf8");
2815
+ } else {
2816
+ writeFileSync2(path, `${kept.map((entry) => JSON.stringify(entry)).join("\n")}
2817
+ `, "utf8");
2818
+ }
2763
2819
  }
2764
-
2765
- // src/lib/opencode/health.ts
2766
- async function checkOpenCodeHealth(port) {
2820
+ function readSessionBindings(path, log3) {
2821
+ const result = readSessionBindingRecords(path, log3);
2822
+ if (result.readError !== null) {
2823
+ log3(`[session-binding-ledger] could not read ${path}: ${result.readError}`);
2824
+ }
2825
+ return new Map(
2826
+ result.records.map((record) => [record.opencode_session_id, record.conversation_id])
2827
+ );
2828
+ }
2829
+ function readSessionBindingRecords(path, log3) {
2830
+ let content;
2767
2831
  try {
2768
- const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
2769
- signal: AbortSignal.timeout(2e3)
2770
- // 2 second timeout
2771
- });
2772
- if (response.status === 401) {
2773
- return { healthy: false, authFailed: true, error: "HTTP 401" };
2774
- }
2775
- if (!response.ok) {
2776
- return { healthy: false, error: `HTTP ${response.status}` };
2777
- }
2778
- const data = await response.json().catch(() => ({}));
2779
- return { healthy: true, version: data.version };
2832
+ content = readFileSync4(path, "utf8");
2780
2833
  } catch (error2) {
2781
- const message = error2 instanceof Error ? error2.message : "Unknown error";
2782
- return { healthy: false, error: message };
2834
+ if (isMissingFile2(error2)) return { records: [], readError: null };
2835
+ const readError = boundedError2(error2);
2836
+ return { records: [], readError };
2783
2837
  }
2838
+ const records = content.split("\n").flatMap((line) => {
2839
+ if (!line.trim()) return [];
2840
+ const record = parseRecord2(line, path, log3);
2841
+ return record ? [record] : [];
2842
+ });
2843
+ return { records, readError: null };
2784
2844
  }
2785
- function bodyExcerpt(body) {
2786
- const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
2787
- return excerpt ? ` (body="${excerpt}")` : " (empty body)";
2788
- }
2789
- async function checkOpenCode2Health(port, password) {
2845
+ function parseRecord2(line, path, log3) {
2790
2846
  try {
2791
- const headers = {};
2792
- if (password !== void 0) {
2793
- headers.Authorization = buildOpenCodeBasicAuthHeader(password);
2794
- }
2795
- const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
2796
- headers,
2797
- signal: AbortSignal.timeout(2e3)
2798
- });
2799
- if (response.status === 401) {
2800
- return { healthy: false, authFailed: true, error: "HTTP 401" };
2801
- }
2802
- if (!response.ok) {
2803
- return { healthy: false, error: `HTTP ${response.status}` };
2804
- }
2805
- const body = await response.text();
2806
- const excerpt = bodyExcerpt(body);
2807
- let data;
2808
- try {
2809
- data = JSON.parse(body);
2810
- } catch (error2) {
2811
- return {
2812
- healthy: false,
2813
- error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
2814
- };
2815
- }
2816
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
2817
- return {
2818
- healthy: false,
2819
- error: `Invalid health response${excerpt}: expected healthy=true`
2820
- };
2821
- }
2822
- const health = data;
2823
- if (health.healthy !== true) {
2824
- return {
2825
- healthy: false,
2826
- error: `Invalid health response${excerpt}: expected healthy=true`
2827
- };
2828
- }
2829
- return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
2847
+ const value = JSON.parse(line);
2848
+ if (isSessionBindingRecord(value)) return value;
2849
+ log3(`[session-binding-ledger] skipped malformed record in ${path}: invalid shape`);
2830
2850
  } catch (error2) {
2831
- const message = error2 instanceof Error ? error2.message : "Unknown error";
2832
- return { healthy: false, error: message };
2851
+ log3(`[session-binding-ledger] skipped malformed record in ${path}: ${boundedError2(error2)}`);
2833
2852
  }
2853
+ return null;
2834
2854
  }
2835
- async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
2836
- const startTime = Date.now();
2837
- while (Date.now() - startTime < timeoutMs) {
2838
- const health = await checkOpenCodeHealth(port);
2839
- if (health.healthy || health.authFailed) {
2840
- return health;
2841
- }
2842
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
2855
+ function isSessionBindingRecord(value) {
2856
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2857
+ const record = value;
2858
+ 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";
2859
+ }
2860
+ function isMissingFile2(error2) {
2861
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
2862
+ }
2863
+ function boundedError2(error2) {
2864
+ const message = error2 instanceof Error ? error2.message : String(error2);
2865
+ return message.slice(0, MAX_DIAGNOSTIC_LENGTH2);
2866
+ }
2867
+
2868
+ // src/lib/opencode/session-archive-config.ts
2869
+ import { readFileSync as readFileSync5 } from "node:fs";
2870
+
2871
+ // src/lib/opencode/session-cleanup.ts
2872
+ var DURATION_UNIT_MS = {
2873
+ s: 1e3,
2874
+ m: 60 * 1e3,
2875
+ h: 60 * 60 * 1e3,
2876
+ d: 24 * 60 * 60 * 1e3
2877
+ };
2878
+ function parseDurationMs(input) {
2879
+ const trimmed = input.trim();
2880
+ const match = /^(\d+)([smhd])$/.exec(trimmed);
2881
+ if (!match) {
2882
+ throw new Error(
2883
+ `Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
2884
+ );
2843
2885
  }
2844
- return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
2886
+ const value = Number(match[1]);
2887
+ if (value <= 0) {
2888
+ throw new Error(`Invalid duration "${input}": must be a positive value.`);
2889
+ }
2890
+ return value * DURATION_UNIT_MS[match[2]];
2845
2891
  }
2846
- async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
2847
- const startTime = Date.now();
2848
- while (Date.now() - startTime < timeoutMs) {
2849
- const health = await checkOpenCode2Health(port, password);
2850
- if (health.healthy || health.authFailed) {
2851
- return health;
2892
+ function selectSessionsToDelete(sessions, opts) {
2893
+ const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
2894
+ if (maxAgeMs === void 0 && maxCount === void 0) return [];
2895
+ const ageEligible = (s) => {
2896
+ if (maxAgeMs === void 0) return false;
2897
+ if (s.lastActivityMs === null) return true;
2898
+ return nowMs - s.lastActivityMs > maxAgeMs;
2899
+ };
2900
+ const countEligibleIds = /* @__PURE__ */ new Set();
2901
+ if (maxCount !== void 0) {
2902
+ const byActivityDesc = [...sessions].sort(
2903
+ (a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
2904
+ );
2905
+ for (const s of byActivityDesc.slice(maxCount)) {
2906
+ countEligibleIds.add(s.id);
2852
2907
  }
2853
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
2854
2908
  }
2855
- return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
2909
+ const toDelete = [];
2910
+ for (const s of sessions) {
2911
+ if (protectedIds.has(s.id)) continue;
2912
+ if (ageEligible(s) || countEligibleIds.has(s.id)) {
2913
+ toDelete.push(s.id);
2914
+ }
2915
+ }
2916
+ const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
2917
+ return toDelete.sort(
2918
+ (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
2919
+ );
2856
2920
  }
2857
-
2858
- // src/lib/http-timeout.ts
2859
- var REQUEST_TIMEOUT_MS = 6e4;
2860
- var RequestTimeoutError = class extends Error {
2861
- constructor(operation, timeoutMs, elapsedMs) {
2862
- super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
2863
- this.operation = operation;
2864
- this.timeoutMs = timeoutMs;
2865
- this.elapsedMs = elapsedMs;
2866
- this.name = "TimeoutError";
2921
+ var DEFAULT_INTERVAL = "1h";
2922
+ function resolve2(flag, envValue, fallback) {
2923
+ return flag ?? envValue ?? fallback;
2924
+ }
2925
+ function parseMaxCount(input) {
2926
+ const trimmed = input.trim();
2927
+ if (!/^\d+$/.test(trimmed)) {
2928
+ throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
2867
2929
  }
2868
- };
2869
- function describeRequestOperation(input, init) {
2870
- const method = (init?.method ?? input.method ?? "GET").toUpperCase();
2871
- const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2872
- let path;
2873
- try {
2874
- path = new URL(rawUrl).pathname;
2875
- } catch (error2) {
2876
- if (!(error2 instanceof TypeError)) throw error2;
2877
- path = rawUrl.split(/[?#]/, 1)[0];
2930
+ const value = Number(trimmed);
2931
+ if (value <= 0) {
2932
+ throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
2878
2933
  }
2879
- const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2880
- const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
2881
- const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
2882
- return `${method} ${maskedPath}`;
2934
+ return value;
2883
2935
  }
2884
- function withRequestTimeout(fetchImpl, timeoutMs) {
2885
- return (async (input, init) => {
2886
- const startedAt = Date.now();
2936
+ function resolveSessionCleanupConfig(flags, env = process.env) {
2937
+ const warnings = [];
2938
+ const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
2939
+ const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
2940
+ const intervalRaw = resolve2(
2941
+ flags.interval,
2942
+ env.EVIDENT_SESSION_CLEANUP_INTERVAL,
2943
+ DEFAULT_INTERVAL
2944
+ );
2945
+ let maxAgeMs;
2946
+ if (maxAgeRaw !== void 0) {
2887
2947
  try {
2888
- return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
2889
- } catch (error2) {
2890
- if (error2?.name === "TimeoutError") {
2891
- throw new RequestTimeoutError(
2892
- describeRequestOperation(input, init),
2893
- timeoutMs,
2894
- Date.now() - startedAt
2895
- );
2896
- }
2897
- throw error2;
2948
+ maxAgeMs = parseDurationMs(maxAgeRaw);
2949
+ } catch (err) {
2950
+ warnings.push(
2951
+ `Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
2952
+ );
2898
2953
  }
2899
- });
2954
+ }
2955
+ let maxCount;
2956
+ if (maxCountRaw !== void 0) {
2957
+ try {
2958
+ maxCount = parseMaxCount(maxCountRaw);
2959
+ } catch (err) {
2960
+ warnings.push(
2961
+ `Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
2962
+ );
2963
+ }
2964
+ }
2965
+ let intervalMs;
2966
+ try {
2967
+ intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
2968
+ } catch (err) {
2969
+ warnings.push(
2970
+ `Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
2971
+ );
2972
+ intervalMs = parseDurationMs(DEFAULT_INTERVAL);
2973
+ }
2974
+ const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
2975
+ return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
2900
2976
  }
2901
2977
 
2902
- // src/lib/opencode/client.ts
2903
- function redactPassword(message, password) {
2904
- return message.replaceAll(password, "[redacted]");
2978
+ // src/lib/opencode/session-archive-config.ts
2979
+ var DEFAULT_INTERVAL2 = "1h";
2980
+ var DEFAULT_FLUSH_BUDGET_MS = 2e3;
2981
+ var MAX_SESSION_ARCHIVE_TIMER_DELAY_MS = 2147483647;
2982
+ var STATE_PREFIX_FILE = "/dev/shm/evident-state-prefix";
2983
+ var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
2984
+ var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
2985
+ function nonEmpty(value) {
2986
+ const trimmed = value?.trim();
2987
+ return trimmed ? trimmed : null;
2988
+ }
2989
+ function parseFlushBudgetMs(value) {
2990
+ const trimmed = value.trim();
2991
+ const budgetMs = Number(trimmed);
2992
+ if (!/^\d+$/.test(trimmed) || !Number.isSafeInteger(budgetMs) || budgetMs <= 0 || budgetMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
2993
+ throw new Error(
2994
+ `Invalid flush budget "${value}": expected a positive integer number of milliseconds no greater than ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}.`
2995
+ );
2996
+ }
2997
+ return budgetMs;
2905
2998
  }
2906
- function createOpenCodeClient(options) {
2999
+ function resolveSessionArchiveConfig(flags, env = process.env) {
3000
+ const warnings = [];
3001
+ const archiveRaw = env.EVIDENT_SESSION_ARCHIVE;
3002
+ let enabled = flags.enabled ?? false;
3003
+ if (flags.enabled === void 0 && archiveRaw !== void 0 && archiveRaw.trim() !== "") {
3004
+ const normalized = archiveRaw.trim().toLowerCase();
3005
+ if (ENABLED_VALUES.has(normalized)) {
3006
+ enabled = true;
3007
+ } else if (DISABLED_VALUES.has(normalized)) {
3008
+ enabled = false;
3009
+ } else {
3010
+ warnings.push(
3011
+ `Ignoring invalid EVIDENT_SESSION_ARCHIVE "${archiveRaw}": expected on or off; session archiving remains off.`
3012
+ );
3013
+ }
3014
+ }
3015
+ let intervalMs;
3016
+ const intervalRaw = flags.interval ?? env.EVIDENT_SESSION_ARCHIVE_INTERVAL ?? DEFAULT_INTERVAL2;
3017
+ try {
3018
+ intervalMs = parseDurationMs(intervalRaw);
3019
+ if (intervalMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
3020
+ throw new Error(
3021
+ `Duration "${intervalRaw}" exceeds Node's maximum timer delay of ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}ms.`
3022
+ );
3023
+ }
3024
+ } catch (err) {
3025
+ warnings.push(
3026
+ `Ignoring invalid session archive interval, using default ${DEFAULT_INTERVAL2}: ${err instanceof Error ? err.message : String(err)}`
3027
+ );
3028
+ intervalMs = parseDurationMs(DEFAULT_INTERVAL2);
3029
+ }
3030
+ let flushBudgetMs;
3031
+ const flushBudgetRaw = env.EVIDENT_SESSION_ARCHIVE_FLUSH_MS ?? String(DEFAULT_FLUSH_BUDGET_MS);
3032
+ try {
3033
+ flushBudgetMs = parseFlushBudgetMs(flushBudgetRaw);
3034
+ } catch (err) {
3035
+ warnings.push(
3036
+ `Ignoring invalid EVIDENT_SESSION_ARCHIVE_FLUSH_MS, using default ${DEFAULT_FLUSH_BUDGET_MS}: ${err instanceof Error ? err.message : String(err)}`
3037
+ );
3038
+ flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
3039
+ }
3040
+ const bucket = nonEmpty(env.PERSISTENCE_BUCKET);
3041
+ let keyPrefix = nonEmpty(env.LITESTREAM_PREFIX);
3042
+ if (enabled && bucket === null) {
3043
+ warnings.push("Session archiving is off because PERSISTENCE_BUCKET is unset or empty.");
3044
+ enabled = false;
3045
+ }
3046
+ if (enabled && keyPrefix === null) {
3047
+ let fallbackReadError = null;
3048
+ try {
3049
+ keyPrefix = nonEmpty(readFileSync5(STATE_PREFIX_FILE, "utf8"));
3050
+ } catch (err) {
3051
+ fallbackReadError = err instanceof Error ? err.message : String(err);
3052
+ }
3053
+ if (keyPrefix === null) {
3054
+ const readFailure = fallbackReadError ? ` Could not read ${STATE_PREFIX_FILE}: ${fallbackReadError}.` : "";
3055
+ warnings.push(
3056
+ `Session archiving is off because neither LITESTREAM_PREFIX nor ${STATE_PREFIX_FILE} contains a prefix.${readFailure}`
3057
+ );
3058
+ enabled = false;
3059
+ }
3060
+ }
3061
+ return { enabled, bucket, keyPrefix, intervalMs, flushBudgetMs, warnings };
3062
+ }
3063
+
3064
+ // src/lib/opencode/session-archive-snapshot.ts
3065
+ import { createHash } from "node:crypto";
3066
+ var SessionArchiveExportError = class extends Error {
3067
+ constructor(code, message) {
3068
+ super(message);
3069
+ this.code = code;
3070
+ this.name = "SessionArchiveExportError";
3071
+ }
3072
+ };
3073
+ function canonicalize(value) {
3074
+ if (Array.isArray(value)) return value.map(canonicalize);
3075
+ if (typeof value !== "object" || value === null) return value;
3076
+ return Object.fromEntries(
3077
+ Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, canonicalize(nested)])
3078
+ );
3079
+ }
3080
+ function contentForFingerprint(archive) {
3081
+ return JSON.stringify(
3082
+ canonicalize({
3083
+ opencode_api_version: archive.opencode_api_version,
3084
+ opencode_session_id: archive.opencode_session_id,
3085
+ evident_conversation_id: archive.evident_conversation_id,
3086
+ messages: archive.messages
3087
+ })
3088
+ );
3089
+ }
3090
+ function fingerprintArchive(archive) {
3091
+ return createHash("sha256").update(contentForFingerprint(archive)).digest("hex");
3092
+ }
3093
+ function buildSessionArchive(input) {
3094
+ const archive = {
3095
+ schema_version: 1,
3096
+ opencode_api_version: input.apiVersion,
3097
+ opencode_session_id: input.session.id,
3098
+ evident_conversation_id: input.conversationId,
3099
+ evident_runner_id: input.runnerId,
3100
+ binding_source: input.bindingSource,
3101
+ session: input.session,
3102
+ messages: input.messages,
3103
+ message_count: input.messages.length,
3104
+ archived_at: input.archivedAt
3105
+ };
3106
+ return { ...archive, content_fingerprint: fingerprintArchive(archive) };
3107
+ }
3108
+ function isRecord2(value) {
3109
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3110
+ }
3111
+ function errorMessage2(error2) {
3112
+ return error2 instanceof Error ? error2.message : String(error2);
3113
+ }
3114
+ function invalidResponse(version2, sessionId, path, reason) {
3115
+ return new SessionArchiveExportError(
3116
+ "invalid-response",
3117
+ `OpenCode ${version2} session ${sessionId} returned an invalid response from GET ${path}: ${reason}`
3118
+ );
3119
+ }
3120
+ async function requestPage(client, sessionId, path, signal) {
3121
+ let response;
3122
+ try {
3123
+ response = await client.request(path, signal ? { signal } : void 0, {
3124
+ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100)
3125
+ });
3126
+ } catch (error2) {
3127
+ throw new SessionArchiveExportError(
3128
+ "request-failed",
3129
+ `OpenCode ${client.version} session ${sessionId} request GET ${path} failed: ${errorMessage2(error2)}`
3130
+ );
3131
+ }
3132
+ if (response.status === 404) {
3133
+ throw new SessionArchiveExportError(
3134
+ "not-found",
3135
+ `OpenCode ${client.version} session ${sessionId} was not found at GET ${path} (HTTP 404)`
3136
+ );
3137
+ }
3138
+ if (!response.ok) {
3139
+ throw new SessionArchiveExportError(
3140
+ "request-failed",
3141
+ `OpenCode ${client.version} session ${sessionId} request GET ${path} returned HTTP ${response.status}`
3142
+ );
3143
+ }
3144
+ let bodyText;
3145
+ try {
3146
+ bodyText = await response.text();
3147
+ } catch (error2) {
3148
+ throw new SessionArchiveExportError(
3149
+ "request-failed",
3150
+ `OpenCode ${client.version} session ${sessionId} response body from GET ${path} could not be read: ${errorMessage2(error2)}`
3151
+ );
3152
+ }
3153
+ try {
3154
+ return { response, body: JSON.parse(bodyText) };
3155
+ } catch (error2) {
3156
+ if (!(error2 instanceof SyntaxError)) throw error2;
3157
+ const contentType = response.headers.get("content-type") ?? "";
3158
+ const looksLikeHtml = contentType.toLowerCase().includes("text/html") || /^\s*<!doctype\s+html|^\s*<html/i.test(bodyText);
3159
+ throw invalidResponse(
3160
+ client.version,
3161
+ sessionId,
3162
+ path,
3163
+ looksLikeHtml ? "received text/html, expected JSON" : "received non-JSON content, expected JSON"
3164
+ );
3165
+ }
3166
+ }
3167
+ function v1NextCursor(response, sessionId, path) {
3168
+ const headerCursor = response.headers.get("x-next-cursor")?.trim() || null;
3169
+ const linkHeader = response.headers.get("link");
3170
+ const nextLink = linkHeader?.split(",").map((link) => link.trim()).find((link) => /;\s*rel\s*=\s*"?next"?/i.test(link));
3171
+ let linkCursor = null;
3172
+ if (nextLink) {
3173
+ const target = nextLink.match(/<([^>]+)>/)?.[1];
3174
+ if (!target) {
3175
+ throw new SessionArchiveExportError(
3176
+ "pagination-failed",
3177
+ `OpenCode v1 session ${sessionId} returned a next Link without a URL for GET ${path}`
3178
+ );
3179
+ }
3180
+ try {
3181
+ linkCursor = new URL(target, "http://127.0.0.1").searchParams.get("before");
3182
+ } catch (error2) {
3183
+ throw new SessionArchiveExportError(
3184
+ "pagination-failed",
3185
+ `OpenCode v1 session ${sessionId} returned an invalid next Link for GET ${path}: ${errorMessage2(error2)}`
3186
+ );
3187
+ }
3188
+ }
3189
+ if (headerCursor && linkCursor && headerCursor !== linkCursor) {
3190
+ throw new SessionArchiveExportError(
3191
+ "pagination-failed",
3192
+ `OpenCode v1 session ${sessionId} returned conflicting next cursors for GET ${path}`
3193
+ );
3194
+ }
3195
+ const cursor = headerCursor ?? linkCursor;
3196
+ if ((headerCursor || nextLink) && !cursor) {
3197
+ throw new SessionArchiveExportError(
3198
+ "pagination-failed",
3199
+ `OpenCode v1 session ${sessionId} returned a next page without a usable cursor for GET ${path}`
3200
+ );
3201
+ }
3202
+ return cursor;
3203
+ }
3204
+ function v1Messages(body, sessionId, path) {
3205
+ if (!Array.isArray(body)) {
3206
+ throw invalidResponse("v1", sessionId, path, "expected an array of messages");
3207
+ }
3208
+ return body;
3209
+ }
3210
+ async function exportV1SessionMessages(client, sessionId, signal) {
3211
+ const basePath = `/session/${encodeURIComponent(sessionId)}/message`;
3212
+ const firstPage = await requestPage(client, sessionId, basePath, signal);
3213
+ let messages = v1Messages(firstPage.body, sessionId, basePath);
3214
+ let cursor = v1NextCursor(firstPage.response, sessionId, basePath);
3215
+ const seenCursors = /* @__PURE__ */ new Set();
3216
+ while (cursor !== null) {
3217
+ if (seenCursors.has(cursor)) {
3218
+ throw new SessionArchiveExportError(
3219
+ "pagination-failed",
3220
+ `OpenCode v1 session ${sessionId} repeated cursor while reading GET ${basePath}`
3221
+ );
3222
+ }
3223
+ seenCursors.add(cursor);
3224
+ const path = `${basePath}?${new URLSearchParams({ before: cursor }).toString()}`;
3225
+ const page = await requestPage(client, sessionId, path, signal);
3226
+ messages = [...v1Messages(page.body, sessionId, path), ...messages];
3227
+ cursor = v1NextCursor(page.response, sessionId, path);
3228
+ }
3229
+ return messages;
3230
+ }
3231
+ async function exportV2SessionMessages(client, sessionId, signal) {
3232
+ const basePath = `/api/session/${encodeURIComponent(sessionId)}/message`;
3233
+ const messages = [];
3234
+ const seenCursors = /* @__PURE__ */ new Set();
3235
+ let cursor;
3236
+ let hasNextPage = true;
3237
+ while (hasNextPage) {
3238
+ const params = new URLSearchParams({ limit: "100" });
3239
+ if (cursor === void 0) {
3240
+ params.set("order", "asc");
3241
+ } else {
3242
+ params.set("cursor", cursor);
3243
+ }
3244
+ const path = `${basePath}?${params.toString()}`;
3245
+ const { body } = await requestPage(client, sessionId, path, signal);
3246
+ if (!isRecord2(body) || !Array.isArray(body.data) || !isRecord2(body.cursor)) {
3247
+ throw invalidResponse("v2", sessionId, path, "expected a {data: [], cursor: {\u2026}} envelope");
3248
+ }
3249
+ messages.push(...body.data);
3250
+ const next = body.cursor.next;
3251
+ if (next === void 0 || next === null) {
3252
+ hasNextPage = false;
3253
+ continue;
3254
+ }
3255
+ if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
3256
+ throw new SessionArchiveExportError(
3257
+ "pagination-failed",
3258
+ `OpenCode v2 session ${sessionId} returned an invalid or repeated cursor from GET ${path}`
3259
+ );
3260
+ }
3261
+ seenCursors.add(next);
3262
+ cursor = next;
3263
+ }
3264
+ return messages;
3265
+ }
3266
+ async function exportSessionMessages(client, sessionId, signal) {
3267
+ if (client.version === "v1") return exportV1SessionMessages(client, sessionId, signal);
3268
+ if (client.version === "v2") return exportV2SessionMessages(client, sessionId, signal);
3269
+ throw new SessionArchiveExportError(
3270
+ "unsupported-dialect",
3271
+ `OpenCode session ${sessionId} cannot be archived: unsupported API dialect ${String(client.version)}`
3272
+ );
3273
+ }
3274
+
3275
+ // src/lib/opencode/session-archive-watermarks.ts
3276
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
3277
+ import { dirname as dirname6, join as join6 } from "node:path";
3278
+ function sessionArchiveWatermarkPath(homeDir, env) {
3279
+ const override = env.EVIDENT_SESSION_ARCHIVE_WATERMARK?.trim();
3280
+ return override || join6(homeDir, ".local", "state", "evident", "session-archive-watermarks.json");
3281
+ }
3282
+ function readWatermarks(path, log3) {
3283
+ let content;
3284
+ try {
3285
+ content = readFileSync6(path, "utf8");
3286
+ } catch (error2) {
3287
+ if (isMissingFile3(error2)) return /* @__PURE__ */ new Map();
3288
+ log3(`[session-archive-watermarks] could not read ${path}; treating every session as changed`);
3289
+ return /* @__PURE__ */ new Map();
3290
+ }
3291
+ try {
3292
+ const parsed = JSON.parse(content);
3293
+ if (!isRecord3(parsed)) throw new Error("invalid watermark object");
3294
+ const entries = Object.entries(parsed);
3295
+ if (!entries.every(([sessionId, value]) => sessionId.length > 0 && isWatermark(value))) {
3296
+ throw new Error("invalid watermark entry");
3297
+ }
3298
+ return new Map(entries);
3299
+ } catch (error2) {
3300
+ log3(`[session-archive-watermarks] could not parse ${path}; treating every session as changed`);
3301
+ return /* @__PURE__ */ new Map();
3302
+ }
3303
+ }
3304
+ function recordUpload(path, sessionId, entry, watermarks) {
3305
+ const updated = new Map(watermarks);
3306
+ updated.set(sessionId, entry);
3307
+ mkdirSync3(dirname6(path), { recursive: true });
3308
+ writeFileSync3(path, `${JSON.stringify(Object.fromEntries(updated))}
3309
+ `, {
3310
+ encoding: "utf8",
3311
+ mode: 384
3312
+ });
3313
+ watermarks.clear();
3314
+ for (const [id, watermark] of updated) watermarks.set(id, watermark);
3315
+ }
3316
+ function isRecord3(value) {
3317
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3318
+ }
3319
+ function isWatermark(value) {
3320
+ if (!isRecord3(value)) return false;
3321
+ return typeof value.fingerprint === "string" && typeof value.uploaded_at === "string" && typeof value.key === "string" && (!("conversation_id" in value) || value.conversation_id === null || typeof value.conversation_id === "string");
3322
+ }
3323
+ function isMissingFile3(error2) {
3324
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
3325
+ }
3326
+
3327
+ // src/lib/runner-synchroniser.ts
3328
+ import { spawn } from "node:child_process";
3329
+
3330
+ // ../../runner/synchroniser/src/shell-contract.json
3331
+ var shell_contract_default = {
3332
+ commands: [
3333
+ "env",
3334
+ "litestream-config",
3335
+ "restore",
3336
+ "sync-once",
3337
+ "model-auth-ready",
3338
+ "self-stop",
3339
+ "session-db-classify",
3340
+ "session-db-verify",
3341
+ "archive-put"
3342
+ ],
3343
+ archiveStdinMaxBytes: 33554432
3344
+ };
3345
+
3346
+ // src/lib/runner-synchroniser.ts
3347
+ var MAX_ARCHIVE_STDIN_BYTES = shell_contract_default.archiveStdinMaxBytes;
3348
+ var ERROR_CLOSE_FALLBACK_MS = 1e3;
3349
+ function isPipeClosed(error2) {
3350
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPIPE";
3351
+ }
3352
+ function appendError(stderr, error2) {
3353
+ const message = error2 instanceof Error ? error2.message : String(error2);
3354
+ return stderr === "" ? message : `${stderr}
3355
+ ${message}`;
3356
+ }
3357
+ function runSynchroniser(args, opts) {
3358
+ return new Promise((resolve5) => {
3359
+ let child;
3360
+ let stdout = "";
3361
+ let stderr = "";
3362
+ let settled = false;
3363
+ let shuttingDown = false;
3364
+ let timedOut = false;
3365
+ let stdinFailure;
3366
+ let childFailure;
3367
+ const timer = {};
3368
+ const closeFallbackTimer = {};
3369
+ let abortListener;
3370
+ let spawnListener;
3371
+ const finish = (result) => {
3372
+ if (settled) return;
3373
+ settled = true;
3374
+ if (timer.handle) clearTimeout(timer.handle);
3375
+ if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
3376
+ if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
3377
+ if (spawnListener) child.removeListener("spawn", spawnListener);
3378
+ resolve5(result);
3379
+ };
3380
+ const armErrorCloseFallback = () => {
3381
+ if (closeFallbackTimer.handle) return;
3382
+ closeFallbackTimer.handle = setTimeout(() => {
3383
+ const failure = stdinFailure ?? childFailure;
3384
+ finish({
3385
+ code: null,
3386
+ stdout,
3387
+ stderr: failure === void 0 ? stderr : appendError(stderr, failure),
3388
+ timedOut
3389
+ });
3390
+ }, ERROR_CLOSE_FALLBACK_MS);
3391
+ };
3392
+ const terminateChild = () => {
3393
+ if (!shuttingDown) {
3394
+ shuttingDown = true;
3395
+ child.stdin?.destroy();
3396
+ }
3397
+ if (child.pid !== void 0) {
3398
+ child.kill("SIGKILL");
3399
+ } else if (!spawnListener) {
3400
+ spawnListener = () => {
3401
+ if (child.pid !== void 0) child.kill("SIGKILL");
3402
+ };
3403
+ child.once("spawn", spawnListener);
3404
+ }
3405
+ };
3406
+ try {
3407
+ child = spawn("runner-synchroniser", args, {
3408
+ env: opts.env ?? process.env,
3409
+ stdio: [opts.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
3410
+ });
3411
+ } catch (error2) {
3412
+ finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
3413
+ return;
3414
+ }
3415
+ child.stdout?.setEncoding("utf8");
3416
+ child.stdout?.on("data", (chunk) => {
3417
+ stdout += chunk;
3418
+ });
3419
+ child.stderr?.setEncoding("utf8");
3420
+ child.stderr?.on("data", (chunk) => {
3421
+ stderr += chunk;
3422
+ });
3423
+ child.once("error", (error2) => {
3424
+ childFailure = error2;
3425
+ if (child.pid !== void 0) terminateChild();
3426
+ armErrorCloseFallback();
3427
+ });
3428
+ child.once("close", (code) => {
3429
+ const failure = stdinFailure ?? childFailure;
3430
+ finish({
3431
+ code: failure === void 0 ? code : null,
3432
+ stdout,
3433
+ stderr: failure === void 0 ? stderr : appendError(stderr, failure),
3434
+ timedOut
3435
+ });
3436
+ });
3437
+ const stdin = child.stdin;
3438
+ const failStdin = (error2) => {
3439
+ if (settled || shuttingDown || isPipeClosed(error2)) return;
3440
+ stdinFailure = error2;
3441
+ terminateChild();
3442
+ armErrorCloseFallback();
3443
+ };
3444
+ stdin?.once("error", failStdin);
3445
+ if (opts.stdin !== void 0 && stdin === null) {
3446
+ failStdin(new Error("synchroniser stdin is unavailable"));
3447
+ } else if (opts.stdin !== void 0 && stdin !== null) {
3448
+ const drained = stdin.write(opts.stdin, (error2) => {
3449
+ if (error2) failStdin(error2);
3450
+ });
3451
+ if (!drained) {
3452
+ stdin.once("drain", () => stdin.end());
3453
+ } else {
3454
+ stdin.end();
3455
+ }
3456
+ }
3457
+ if (opts.signal) {
3458
+ const killChild = () => {
3459
+ terminateChild();
3460
+ };
3461
+ abortListener = killChild;
3462
+ if (opts.signal.aborted) {
3463
+ abortListener();
3464
+ } else {
3465
+ opts.signal.addEventListener("abort", abortListener, { once: true });
3466
+ if (opts.signal.aborted) abortListener();
3467
+ }
3468
+ }
3469
+ timer.handle = setTimeout(
3470
+ () => {
3471
+ timedOut = true;
3472
+ terminateChild();
3473
+ },
3474
+ Math.max(0, opts.timeoutMs)
3475
+ );
3476
+ });
3477
+ }
3478
+
3479
+ // src/lib/opencode/session-archive-upload.ts
3480
+ var MAX_SESSION_ARCHIVE_BYTES = MAX_ARCHIVE_STDIN_BYTES;
3481
+ var EXIT_ARCHIVE_CONFIG_MISSING = 50;
3482
+ var EXIT_ARCHIVE_ACCESS_DENIED = 51;
3483
+ function transportFailure(bytes, reason, exitCode) {
3484
+ 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.";
3485
+ return {
3486
+ kind: "transport-failed",
3487
+ bytes,
3488
+ reason,
3489
+ message,
3490
+ ...exitCode !== void 0 ? { exitCode } : {}
3491
+ };
3492
+ }
3493
+ function isJson(body) {
3494
+ try {
3495
+ JSON.parse(body.toString("utf8"));
3496
+ return true;
3497
+ } catch (error2) {
3498
+ void error2;
3499
+ return false;
3500
+ }
3501
+ }
3502
+ async function uploadSessionArchive(key, body, options) {
3503
+ const bytes = Buffer.from(body);
3504
+ if (bytes.byteLength > MAX_SESSION_ARCHIVE_BYTES) {
3505
+ return transportFailure(bytes.byteLength, "too-large");
3506
+ }
3507
+ if (!isJson(bytes)) return transportFailure(bytes.byteLength, "invalid-json");
3508
+ const run2 = options.runSynchroniser ?? runSynchroniser;
3509
+ let result;
3510
+ try {
3511
+ result = await run2(["archive-put", key], {
3512
+ timeoutMs: options.timeoutMs,
3513
+ env: options.env,
3514
+ signal: options.signal,
3515
+ stdin: bytes
3516
+ });
3517
+ } catch (error2) {
3518
+ return transportFailure(
3519
+ bytes.byteLength,
3520
+ error2 instanceof Error && /ENOENT|runner-synchroniser/i.test(error2.message) ? "missing-executable" : "spawn-failed"
3521
+ );
3522
+ }
3523
+ if (result.timedOut) return transportFailure(bytes.byteLength, "timeout");
3524
+ if (result.code === 0) return { kind: "ok", bytes: bytes.byteLength };
3525
+ if (result.code === EXIT_ARCHIVE_CONFIG_MISSING) {
3526
+ return { kind: "config-missing", bytes: bytes.byteLength };
3527
+ }
3528
+ if (result.code === EXIT_ARCHIVE_ACCESS_DENIED) {
3529
+ return { kind: "access-denied", bytes: bytes.byteLength };
3530
+ }
3531
+ if (result.code === null && /ENOENT|runner-synchroniser/i.test(result.stderr)) {
3532
+ return transportFailure(bytes.byteLength, "missing-executable");
3533
+ }
3534
+ return transportFailure(
3535
+ bytes.byteLength,
3536
+ result.code === null ? "spawn-failed" : "process-failed",
3537
+ result.code ?? void 0
3538
+ );
3539
+ }
3540
+
3541
+ // src/lib/opencode/session-binding-resolution.ts
3542
+ var SESSION_BINDING_PROJECTION_CHUNK_SIZE = 500;
3543
+ var SESSION_BINDING_PROJECTION_TIMEOUT_MS = 1e4;
3544
+ function isRecord4(value) {
3545
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3546
+ }
3547
+ function isProjectionSource(value) {
3548
+ return value === "current" || value === "invocation" || value === "history";
3549
+ }
3550
+ function emptySessionBindingProjection() {
3551
+ return {
3552
+ current: /* @__PURE__ */ new Map(),
3553
+ invocation: /* @__PURE__ */ new Map(),
3554
+ history: /* @__PURE__ */ new Map(),
3555
+ conflicts: /* @__PURE__ */ new Set()
3556
+ };
3557
+ }
3558
+ function requestedSessionIds(sessions) {
3559
+ const ids = /* @__PURE__ */ new Set();
3560
+ for (const session of sessions) {
3561
+ if (session.id.length > 0) ids.add(session.id);
3562
+ if (typeof session.parentID === "string" && session.parentID.length > 0) {
3563
+ ids.add(session.parentID);
3564
+ }
3565
+ }
3566
+ return [...ids];
3567
+ }
3568
+ function parseProjectionResponse(value, requestedIds) {
3569
+ if (!isRecord4(value) || !Array.isArray(value.bindings) || !Array.isArray(value.conflicts)) {
3570
+ return null;
3571
+ }
3572
+ const projection = emptySessionBindingProjection();
3573
+ for (const candidate of value.bindings) {
3574
+ if (!isRecord4(candidate) || typeof candidate.opencode_session_id !== "string" || typeof candidate.conversation_id !== "string" || !isProjectionSource(candidate.source) || !requestedIds.has(candidate.opencode_session_id) || projection.conflicts.has(candidate.opencode_session_id)) {
3575
+ return null;
3576
+ }
3577
+ const bindings = projection[candidate.source];
3578
+ if (bindings.has(candidate.opencode_session_id)) return null;
3579
+ bindings.set(candidate.opencode_session_id, candidate.conversation_id);
3580
+ }
3581
+ for (const conflict of value.conflicts) {
3582
+ if (typeof conflict !== "string" || !requestedIds.has(conflict) || projection.conflicts.has(conflict) || projection.current.has(conflict) || projection.invocation.has(conflict) || projection.history.has(conflict)) {
3583
+ return null;
3584
+ }
3585
+ projection.conflicts.add(conflict);
3586
+ }
3587
+ return projection;
3588
+ }
3589
+ async function fetchSessionBindingProjection({
3590
+ apiUrl,
3591
+ runnerId,
3592
+ authHeader,
3593
+ sessions,
3594
+ signal,
3595
+ fetchImpl = fetch
3596
+ }) {
3597
+ const ids = requestedSessionIds(sessions);
3598
+ const projection = emptySessionBindingProjection();
3599
+ const failures = [];
3600
+ if (ids.length === 0) return { projection, complete: true, failures };
3601
+ for (let offset = 0; offset < ids.length; offset += SESSION_BINDING_PROJECTION_CHUNK_SIZE) {
3602
+ const chunk = ids.slice(offset, offset + SESSION_BINDING_PROJECTION_CHUNK_SIZE);
3603
+ const chunkIndex = Math.floor(offset / SESSION_BINDING_PROJECTION_CHUNK_SIZE) + 1;
3604
+ try {
3605
+ const response = await fetchImpl(
3606
+ `${apiUrl}/runners/${runnerId}/conversations/session-bindings`,
3607
+ {
3608
+ method: "POST",
3609
+ headers: {
3610
+ Authorization: authHeader,
3611
+ "Content-Type": "application/json"
3612
+ },
3613
+ body: JSON.stringify({ opencode_session_ids: chunk }),
3614
+ signal: signal ?? AbortSignal.timeout(SESSION_BINDING_PROJECTION_TIMEOUT_MS)
3615
+ }
3616
+ );
3617
+ if (!response.ok) {
3618
+ failures.push({ chunk: chunkIndex, kind: "http", status: response.status });
3619
+ continue;
3620
+ }
3621
+ const result = parseProjectionResponse(await response.json(), new Set(chunk));
3622
+ if (result === null) {
3623
+ failures.push({ chunk: chunkIndex, kind: "invalid-response" });
3624
+ continue;
3625
+ }
3626
+ for (const source of ["current", "invocation", "history"]) {
3627
+ for (const [sessionId, conversationId] of result[source]) {
3628
+ projection[source].set(sessionId, conversationId);
3629
+ }
3630
+ }
3631
+ for (const conflict of result.conflicts) projection.conflicts.add(conflict);
3632
+ } catch (error2) {
3633
+ failures.push({
3634
+ chunk: chunkIndex,
3635
+ kind: "network",
3636
+ message: error2 instanceof Error ? error2.message : String(error2)
3637
+ });
3638
+ }
3639
+ }
3640
+ return { projection, complete: failures.length === 0, failures };
3641
+ }
3642
+ function directResolution(sessionId, sources) {
3643
+ const localBinding = sources.ledger.get(sessionId);
3644
+ if (localBinding) return { conversation_id: localBinding, source: "ledger" };
3645
+ const watermarkedBinding = sources.watermarks.get(sessionId)?.conversation_id;
3646
+ if (typeof watermarkedBinding === "string" && watermarkedBinding.length > 0) {
3647
+ return { conversation_id: watermarkedBinding, source: "watermark" };
3648
+ }
3649
+ if (sources.projection.conflicts.has(sessionId)) return { reason: "conflict" };
3650
+ for (const source of ["current", "invocation", "history"]) {
3651
+ const conversationId = sources.projection[source].get(sessionId);
3652
+ if (conversationId) return { conversation_id: conversationId, source };
3653
+ }
3654
+ return null;
3655
+ }
3656
+ function isResolved(resolution) {
3657
+ return "conversation_id" in resolution;
3658
+ }
3659
+ function resolveSessionBindings(sessions, sources) {
3660
+ const resolutions = /* @__PURE__ */ new Map();
3661
+ for (const session of sessions) {
3662
+ resolutions.set(session.id, directResolution(session.id, sources) ?? { reason: "unmapped" });
3663
+ }
3664
+ let assignedParent = true;
3665
+ while (assignedParent) {
3666
+ assignedParent = false;
3667
+ for (const session of sessions) {
3668
+ const resolution = resolutions.get(session.id);
3669
+ if (isResolved(resolution) || resolution.reason !== "unmapped") continue;
3670
+ if (typeof session.parentID !== "string" || session.parentID.length === 0) continue;
3671
+ const parent = resolutions.get(session.parentID) ?? directResolution(session.parentID, sources) ?? { reason: "unmapped" };
3672
+ if (isResolved(parent)) {
3673
+ resolutions.set(session.id, {
3674
+ conversation_id: parent.conversation_id,
3675
+ source: "parent"
3676
+ });
3677
+ assignedParent = true;
3678
+ } else if (parent.reason === "conflict" || parent.reason === "parent-conflict") {
3679
+ resolutions.set(session.id, { reason: "parent-conflict" });
3680
+ assignedParent = true;
3681
+ }
3682
+ }
3683
+ }
3684
+ return resolutions;
3685
+ }
3686
+
3687
+ // src/lib/opencode/session-archive-run.ts
3688
+ var SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS = 6e4;
3689
+ function updatedAt(session) {
3690
+ const updated = session.time?.updated;
3691
+ return typeof updated === "number" && Number.isFinite(updated) ? updated : -Infinity;
3692
+ }
3693
+ function sessionArchiveKey(prefix, sessionId) {
3694
+ return `${prefix.replace(/\/+$/, "")}/conversation-archives/${encodeURIComponent(sessionId)}.json`;
3695
+ }
3696
+ function failureMessage(error2) {
3697
+ return error2 instanceof Error ? error2.message : String(error2);
3698
+ }
3699
+ function countMessageParts(messages) {
3700
+ return messages.reduce((count, message) => {
3701
+ if (typeof message !== "object" || message === null || !("parts" in message)) return count;
3702
+ return count + (Array.isArray(message.parts) ? message.parts.length : 0);
3703
+ }, 0);
3704
+ }
3705
+ function isBudgetExpired(deadline, now, signal) {
3706
+ return signal?.aborted === true || now() >= deadline;
3707
+ }
3708
+ function createBudgetSignal(parent, timeoutMs) {
3709
+ const controller = new AbortController();
3710
+ const forwardAbort = () => controller.abort(parent?.reason);
3711
+ if (parent?.aborted) forwardAbort();
3712
+ else parent?.addEventListener("abort", forwardAbort, { once: true });
3713
+ const timer = setTimeout(
3714
+ () => controller.abort(new Error("session archive budget exceeded")),
3715
+ timeoutMs
3716
+ );
3717
+ return {
3718
+ signal: controller.signal,
3719
+ dispose: () => {
3720
+ clearTimeout(timer);
3721
+ parent?.removeEventListener("abort", forwardAbort);
3722
+ }
3723
+ };
3724
+ }
3725
+ async function runSessionArchive({
3726
+ sessions,
3727
+ deps,
3728
+ budgetMs,
3729
+ now = Date.now
3730
+ }) {
3731
+ const startedAt = now();
3732
+ const deadline = startedAt + Math.max(0, budgetMs);
3733
+ const summary = {
3734
+ inspected: sessions.length,
3735
+ uploaded: 0,
3736
+ unchanged: 0,
3737
+ failed: 0,
3738
+ uploadAttempts: 0,
3739
+ uploadFailures: 0,
3740
+ skippedBudget: 0,
3741
+ skippedBudgetIds: [],
3742
+ neverWatermarkedFailures: 0,
3743
+ unmappedIds: [],
3744
+ conflictedIds: [],
3745
+ unresolved: 0,
3746
+ conflicts: 0,
3747
+ reconciliationComplete: deps.reconciliationComplete ?? true,
3748
+ reconciliationIncomplete: 0,
3749
+ bindingSourceCounts: {
3750
+ ledger: 0,
3751
+ watermark: 0,
3752
+ current: 0,
3753
+ invocation: 0,
3754
+ history: 0,
3755
+ parent: 0
3756
+ },
3757
+ archivedSessionIds: [],
3758
+ failures: [],
3759
+ staleSince: null
3760
+ };
3761
+ const ordered = [...sessions].sort((left, right) => updatedAt(right) - updatedAt(left));
3762
+ const upload = deps.upload ?? uploadSessionArchive;
3763
+ const exportMessages = deps.exportMessages ?? exportSessionMessages;
3764
+ const writeWatermark = deps.recordWatermark ?? recordUpload;
3765
+ const resolutions = resolveSessionBindings(sessions, {
3766
+ ledger: deps.bindings,
3767
+ watermarks: deps.watermarks,
3768
+ projection: deps.projection ?? emptySessionBindingProjection()
3769
+ });
3770
+ for (const [sessionId, resolution] of resolutions) {
3771
+ if ("conversation_id" in resolution) {
3772
+ summary.bindingSourceCounts[resolution.source]++;
3773
+ } else if (resolution.reason === "conflict" || resolution.reason === "parent-conflict") {
3774
+ summary.conflictedIds.push(sessionId);
3775
+ } else {
3776
+ summary.unmappedIds.push(sessionId);
3777
+ }
3778
+ }
3779
+ summary.unresolved = summary.unmappedIds.length + summary.conflictedIds.length;
3780
+ summary.conflicts = summary.conflictedIds.length;
3781
+ const failedWatermarks = [];
3782
+ const recordFailedWatermark = (watermark) => {
3783
+ if (watermark) failedWatermarks.push(watermark.uploaded_at);
3784
+ else summary.neverWatermarkedFailures++;
3785
+ };
3786
+ for (let index = 0; index < ordered.length; index++) {
3787
+ const session = ordered[index];
3788
+ const remainingMs = Math.floor(deadline - now());
3789
+ if (remainingMs <= 0 || deps.signal?.aborted) {
3790
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3791
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3792
+ break;
3793
+ }
3794
+ const resolution = resolutions.get(session.id) ?? { reason: "unmapped" };
3795
+ const resolved = "conversation_id" in resolution;
3796
+ const conversationId = resolved ? resolution.conversation_id : null;
3797
+ const bindingSource = resolved ? resolution.source : "unmapped";
3798
+ if (!resolved && !summary.reconciliationComplete) {
3799
+ summary.failed++;
3800
+ summary.reconciliationIncomplete++;
3801
+ summary.failures.push({
3802
+ sessionId: session.id,
3803
+ outcome: "reconciliation-incomplete",
3804
+ reason: resolution.reason,
3805
+ archived: false
3806
+ });
3807
+ recordFailedWatermark(deps.watermarks.get(session.id));
3808
+ continue;
3809
+ }
3810
+ const exportBudget = createBudgetSignal(deps.signal, remainingMs);
3811
+ let messages;
3812
+ try {
3813
+ messages = await exportMessages(deps.client, session.id, exportBudget.signal);
3814
+ } catch (error2) {
3815
+ if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
3816
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3817
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3818
+ break;
3819
+ }
3820
+ summary.failed++;
3821
+ const reason = error2 instanceof SessionArchiveExportError ? error2.code : "export-failed";
3822
+ summary.failures.push({
3823
+ sessionId: session.id,
3824
+ outcome: "export-failed",
3825
+ reason,
3826
+ message: failureMessage(error2),
3827
+ archived: false
3828
+ });
3829
+ recordFailedWatermark(deps.watermarks.get(session.id));
3830
+ continue;
3831
+ } finally {
3832
+ exportBudget.dispose();
3833
+ }
3834
+ if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
3835
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3836
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3837
+ break;
3838
+ }
3839
+ try {
3840
+ const archivedAt = new Date(now()).toISOString();
3841
+ const snapshot = buildSessionArchive({
3842
+ session,
3843
+ messages,
3844
+ apiVersion: deps.client.version,
3845
+ conversationId,
3846
+ bindingSource,
3847
+ runnerId: deps.runnerId,
3848
+ archivedAt
3849
+ });
3850
+ const key = sessionArchiveKey(deps.keyPrefix, session.id);
3851
+ const existing = deps.watermarks.get(session.id);
3852
+ if (existing?.fingerprint === snapshot.content_fingerprint) {
3853
+ summary.unchanged++;
3854
+ summary.archivedSessionIds.push(session.id);
3855
+ continue;
3856
+ }
3857
+ const uploadRemainingMs = Math.floor(deadline - now());
3858
+ if (uploadRemainingMs <= 0 || deps.signal?.aborted) {
3859
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3860
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3861
+ break;
3862
+ }
3863
+ summary.uploadAttempts++;
3864
+ const uploadBudget = createBudgetSignal(deps.signal, uploadRemainingMs);
3865
+ let outcome;
3866
+ try {
3867
+ outcome = await upload(key, JSON.stringify(snapshot), {
3868
+ timeoutMs: Math.min(uploadRemainingMs, SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS),
3869
+ signal: uploadBudget.signal
3870
+ });
3871
+ } finally {
3872
+ uploadBudget.dispose();
3873
+ }
3874
+ if (outcome.kind !== "ok") {
3875
+ summary.failed++;
3876
+ summary.uploadFailures++;
3877
+ const reason = "reason" in outcome ? outcome.reason : void 0;
3878
+ summary.failures.push({
3879
+ sessionId: session.id,
3880
+ outcome: outcome.kind,
3881
+ ...reason !== void 0 ? { reason } : {},
3882
+ ..."message" in outcome ? { message: outcome.message } : {},
3883
+ bytes: outcome.bytes,
3884
+ byteLimit: MAX_SESSION_ARCHIVE_BYTES,
3885
+ messageCount: messages.length,
3886
+ partCount: countMessageParts(messages),
3887
+ ..."exitCode" in outcome && outcome.exitCode !== void 0 ? { exitCode: outcome.exitCode } : {},
3888
+ archived: false
3889
+ });
3890
+ recordFailedWatermark(existing);
3891
+ if (uploadBudget.signal.aborted) {
3892
+ summary.skippedBudgetIds.push(...ordered.slice(index + 1).map(({ id }) => id));
3893
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3894
+ break;
3895
+ }
3896
+ continue;
3897
+ }
3898
+ summary.uploaded++;
3899
+ summary.archivedSessionIds.push(session.id);
3900
+ const watermark = {
3901
+ fingerprint: snapshot.content_fingerprint,
3902
+ uploaded_at: new Date(now()).toISOString(),
3903
+ key,
3904
+ conversation_id: conversationId
3905
+ };
3906
+ try {
3907
+ writeWatermark(deps.watermarkPath, session.id, watermark, deps.watermarks);
3908
+ } catch (error2) {
3909
+ summary.failures.push({
3910
+ sessionId: session.id,
3911
+ outcome: "watermark-write-failed",
3912
+ message: failureMessage(error2),
3913
+ archived: true
3914
+ });
3915
+ }
3916
+ } catch (error2) {
3917
+ if (isBudgetExpired(deadline, now, deps.signal)) {
3918
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3919
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3920
+ break;
3921
+ }
3922
+ summary.failed++;
3923
+ summary.failures.push({
3924
+ sessionId: session.id,
3925
+ outcome: "archive-failed",
3926
+ message: failureMessage(error2),
3927
+ archived: false
3928
+ });
3929
+ recordFailedWatermark(deps.watermarks.get(session.id));
3930
+ }
3931
+ }
3932
+ summary.staleSince = failedWatermarks.sort()[0] ?? null;
3933
+ return summary;
3934
+ }
3935
+
3936
+ // src/lib/opencode/auth.ts
3937
+ function buildOpenCodeBasicAuthHeader(password) {
3938
+ return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
3939
+ }
3940
+
3941
+ // src/lib/opencode/health.ts
3942
+ async function checkOpenCodeHealth(port) {
3943
+ try {
3944
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
3945
+ signal: AbortSignal.timeout(2e3)
3946
+ // 2 second timeout
3947
+ });
3948
+ if (response.status === 401) {
3949
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
3950
+ }
3951
+ if (!response.ok) {
3952
+ return { healthy: false, error: `HTTP ${response.status}` };
3953
+ }
3954
+ const body = await response.text();
3955
+ let data;
3956
+ try {
3957
+ data = JSON.parse(body);
3958
+ } catch (error2) {
3959
+ return {
3960
+ healthy: false,
3961
+ okWithNonJsonBody: true,
3962
+ error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
3963
+ };
3964
+ }
3965
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
3966
+ return {
3967
+ healthy: false,
3968
+ okWithNonJsonBody: true,
3969
+ error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
3970
+ };
3971
+ }
3972
+ const version2 = data.version;
3973
+ return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
3974
+ } catch (error2) {
3975
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
3976
+ return { healthy: false, error: message };
3977
+ }
3978
+ }
3979
+ function bodyExcerpt(body) {
3980
+ const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
3981
+ return excerpt ? ` (body="${excerpt}")` : " (empty body)";
3982
+ }
3983
+ async function checkOpenCode2Health(port, password) {
3984
+ try {
3985
+ const headers = {};
3986
+ if (password !== void 0) {
3987
+ headers.Authorization = buildOpenCodeBasicAuthHeader(password);
3988
+ }
3989
+ let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
3990
+ headers,
3991
+ signal: AbortSignal.timeout(2e3)
3992
+ });
3993
+ const usesHealthFallback = response.status === 404;
3994
+ if (usesHealthFallback) {
3995
+ response = await fetch(`http://127.0.0.1:${port}/api/health`, {
3996
+ headers,
3997
+ signal: AbortSignal.timeout(2e3)
3998
+ });
3999
+ }
4000
+ if (response.status === 401) {
4001
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
4002
+ }
4003
+ if (!response.ok) {
4004
+ return { healthy: false, error: `HTTP ${response.status}` };
4005
+ }
4006
+ const body = await response.text();
4007
+ const excerpt = bodyExcerpt(body);
4008
+ let data;
4009
+ try {
4010
+ data = JSON.parse(body);
4011
+ } catch (error2) {
4012
+ return {
4013
+ healthy: false,
4014
+ error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
4015
+ };
4016
+ }
4017
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
4018
+ return {
4019
+ healthy: false,
4020
+ error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
4021
+ };
4022
+ }
4023
+ const bodyObject = data;
4024
+ if (usesHealthFallback && bodyObject.healthy !== true) {
4025
+ return {
4026
+ healthy: false,
4027
+ error: `Invalid health response${excerpt}: expected healthy=true`
4028
+ };
4029
+ }
4030
+ if (!usesHealthFallback) {
4031
+ return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
4032
+ healthy: false,
4033
+ error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
4034
+ };
4035
+ }
4036
+ return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
4037
+ } catch (error2) {
4038
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
4039
+ return { healthy: false, error: message };
4040
+ }
4041
+ }
4042
+ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
4043
+ const startTime = Date.now();
4044
+ while (Date.now() - startTime < timeoutMs) {
4045
+ const health = await checkOpenCodeHealth(port);
4046
+ if (health.healthy || health.authFailed) {
4047
+ return health;
4048
+ }
4049
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
4050
+ }
4051
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
4052
+ }
4053
+ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
4054
+ const startTime = Date.now();
4055
+ while (Date.now() - startTime < timeoutMs) {
4056
+ const health = await checkOpenCode2Health(port, password);
4057
+ if (health.healthy || health.authFailed) {
4058
+ return health;
4059
+ }
4060
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
4061
+ }
4062
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
4063
+ }
4064
+
4065
+ // src/lib/http-timeout.ts
4066
+ var REQUEST_TIMEOUT_MS = 6e4;
4067
+ var RequestTimeoutError = class extends Error {
4068
+ constructor(operation, timeoutMs, elapsedMs) {
4069
+ super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
4070
+ this.operation = operation;
4071
+ this.timeoutMs = timeoutMs;
4072
+ this.elapsedMs = elapsedMs;
4073
+ this.name = "TimeoutError";
4074
+ }
4075
+ };
4076
+ function describeRequestOperation(input, init) {
4077
+ const method = (init?.method ?? input.method ?? "GET").toUpperCase();
4078
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
4079
+ let path;
4080
+ try {
4081
+ path = new URL(rawUrl).pathname;
4082
+ } catch (error2) {
4083
+ if (!(error2 instanceof TypeError)) throw error2;
4084
+ path = rawUrl.split(/[?#]/, 1)[0];
4085
+ }
4086
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4087
+ const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
4088
+ const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
4089
+ return `${method} ${maskedPath}`;
4090
+ }
4091
+ function withRequestTimeout(fetchImpl, timeoutMs) {
4092
+ return (async (input, init) => {
4093
+ const startedAt = Date.now();
4094
+ const controller = new AbortController();
4095
+ const abortFromCaller = () => controller.abort(init?.signal?.reason);
4096
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
4097
+ const abortFromTimeout = () => controller.abort(timeoutSignal.reason);
4098
+ if (init?.signal?.aborted) abortFromCaller();
4099
+ else init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
4100
+ if (timeoutSignal.aborted) abortFromTimeout();
4101
+ else timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
4102
+ try {
4103
+ return await fetchImpl(input, { ...init, signal: controller.signal });
4104
+ } catch (error2) {
4105
+ if (timeoutSignal.aborted) {
4106
+ throw new RequestTimeoutError(
4107
+ describeRequestOperation(input, init),
4108
+ timeoutMs,
4109
+ Date.now() - startedAt
4110
+ );
4111
+ }
4112
+ throw error2;
4113
+ } finally {
4114
+ init?.signal?.removeEventListener("abort", abortFromCaller);
4115
+ timeoutSignal.removeEventListener("abort", abortFromTimeout);
4116
+ }
4117
+ });
4118
+ }
4119
+
4120
+ // src/lib/opencode/client.ts
4121
+ var OpenCodeRequestError = class extends Error {
4122
+ constructor(statusCode, message) {
4123
+ super(message);
4124
+ this.statusCode = statusCode;
4125
+ this.name = "OpenCodeRequestError";
4126
+ }
4127
+ };
4128
+ function redactPassword(message, password) {
4129
+ return message.replaceAll(password, "[redacted]");
4130
+ }
4131
+ function createOpenCodeClient(options) {
2907
4132
  const password = options.password ?? null;
2908
4133
  const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
2909
4134
  const baseUrl = `http://127.0.0.1:${options.port}`;
@@ -2921,7 +4146,8 @@ function createOpenCodeClient(options) {
2921
4146
  const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
2922
4147
  if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
2923
4148
  const body = await response.text();
2924
- throw new Error(
4149
+ throw new OpenCodeRequestError(
4150
+ response.status,
2925
4151
  `OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
2926
4152
  );
2927
4153
  }
@@ -2929,7 +4155,11 @@ function createOpenCodeClient(options) {
2929
4155
  } catch (error2) {
2930
4156
  if (options.version === "v2" && password !== null) {
2931
4157
  const message = error2 instanceof Error ? error2.message : String(error2);
2932
- throw new Error(redactPassword(message, password));
4158
+ const redactedMessage = redactPassword(message, password);
4159
+ if (error2 instanceof OpenCodeRequestError) {
4160
+ throw new OpenCodeRequestError(error2.statusCode, redactedMessage);
4161
+ }
4162
+ throw new Error(redactedMessage);
2933
4163
  }
2934
4164
  throw error2;
2935
4165
  }
@@ -3078,28 +4308,38 @@ function sessionListPath(options) {
3078
4308
  async function listSessionPage(port, client, options) {
3079
4309
  const path = sessionListPath(options);
3080
4310
  try {
3081
- const res = await requestWithClient(port, client, path);
3082
- if (!res.ok) return null;
4311
+ const res = await requestWithClient(
4312
+ port,
4313
+ client,
4314
+ path,
4315
+ options?.signal ? { signal: options.signal } : void 0,
4316
+ { allowStatuses: ALL_HTTP_STATUSES }
4317
+ );
4318
+ if (!res.ok) {
4319
+ console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
4320
+ return null;
4321
+ }
3083
4322
  const body = await res.json();
3084
4323
  return Array.isArray(body) ? body : null;
3085
4324
  } catch (error2) {
3086
- console.error(
3087
- `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3088
- );
4325
+ if (options?.signal?.aborted) return null;
4326
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4327
+ console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
3089
4328
  return null;
3090
4329
  }
3091
4330
  }
3092
4331
  async function listSessions(port, client, options) {
3093
- if (client?.version === "v2") return listV2Sessions(client);
4332
+ if (client?.version === "v2") return listV2Sessions(client, options?.signal);
3094
4333
  return listSessionPage(port, client, options);
3095
4334
  }
3096
- async function listAllSessions(port, client) {
4335
+ async function listAllSessions(port, client, signal) {
3097
4336
  if (client?.version === "v2") {
3098
- const sessions2 = await listV2Sessions(client);
4337
+ const sessions2 = await listV2Sessions(client, signal);
3099
4338
  return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
3100
4339
  }
3101
4340
  const sessions = await listSessionPage(port, client, {
3102
- limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
4341
+ limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
4342
+ signal
3103
4343
  });
3104
4344
  if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
3105
4345
  if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
@@ -3595,28 +4835,38 @@ function findLastAssistantReplyFor(messages, userMessageId) {
3595
4835
  }
3596
4836
  return last;
3597
4837
  }
4838
+ function assistantMessagesInTurnBlock(messages, userMessageId) {
4839
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
4840
+ if (userIndex === -1) return [];
4841
+ const assistants = [];
4842
+ for (let i = userIndex + 1; i < messages.length; i++) {
4843
+ const message = messages[i];
4844
+ const role = roleOf2(message);
4845
+ if (role === "user") break;
4846
+ if (role === "assistant") assistants.push(message);
4847
+ }
4848
+ return assistants;
4849
+ }
3598
4850
  function collectSubagentSessions(messages, userMessageId) {
3599
4851
  if (!messages || messages.length === 0) return [];
3600
4852
  const byParent = messages.filter(
3601
4853
  (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
3602
4854
  );
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
- }
4855
+ const assistants = byParent.length > 0 ? byParent : assistantMessagesInTurnBlock(messages, userMessageId);
3613
4856
  const refs = [];
3614
4857
  const seen = /* @__PURE__ */ new Set();
3615
4858
  for (const message of assistants) {
3616
4859
  const parts = Array.isArray(message.parts) ? message.parts : [];
3617
4860
  for (const part of parts) {
3618
- if (!part || typeof part !== "object" || part.type !== "tool" || part.tool !== "task")
4861
+ if (!part || typeof part !== "object" || part.type !== "tool") continue;
4862
+ const childSession = part.childSession;
4863
+ if (childSession) {
4864
+ if (seen.has(childSession.sessionId)) continue;
4865
+ seen.add(childSession.sessionId);
4866
+ refs.push(childSession);
3619
4867
  continue;
4868
+ }
4869
+ if (part.tool !== "task") continue;
3620
4870
  const state = part.state;
3621
4871
  if (!state || typeof state !== "object") continue;
3622
4872
  const metadata = state.metadata;
@@ -3764,8 +5014,7 @@ function messageUsage(messages, userMessageId) {
3764
5014
  if (byParentAll.length > 0) {
3765
5015
  correlated = byParentAll;
3766
5016
  } else {
3767
- const reply = findAssistantReplyAfter(messages, userMessageId);
3768
- correlated = reply ? [reply] : [];
5017
+ correlated = assistantMessagesInTurnBlock(messages, userMessageId);
3769
5018
  }
3770
5019
  if (correlated.length === 0) return null;
3771
5020
  return sumAssistantUsage(correlated);
@@ -3900,8 +5149,8 @@ function classifyReplyAuthError(reply) {
3900
5149
  };
3901
5150
  }
3902
5151
  }
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))) {
5152
+ const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
5153
+ if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
3905
5154
  return {
3906
5155
  kind: "model_auth",
3907
5156
  providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
@@ -3987,39 +5236,85 @@ async function hasAnyConfiguredProvider(port, client) {
3987
5236
  );
3988
5237
  return null;
3989
5238
  }
3990
- const path = `/api/integration?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
5239
+ const query = `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
5240
+ const integrationPath = `/api/integration${query}`;
5241
+ let integrationReady = null;
3991
5242
  try {
3992
- const res = await client.request(path);
5243
+ const res = await client.request(integrationPath);
3993
5244
  if (!res.ok) {
3994
5245
  console.error(
3995
- `[hasAnyConfiguredProvider] GET ${path} returned HTTP ${res.status} (port ${port})`
5246
+ `[hasAnyConfiguredProvider] GET ${integrationPath} returned HTTP ${res.status} (port ${port})`
3996
5247
  );
3997
- return null;
5248
+ } else {
5249
+ const body = await res.json();
5250
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
5251
+ console.error(
5252
+ `[hasAnyConfiguredProvider] GET ${integrationPath} body had no integration data array (port ${port})`
5253
+ );
5254
+ } else {
5255
+ let valid = true;
5256
+ for (const integration of body.data) {
5257
+ if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
5258
+ console.error(
5259
+ `[hasAnyConfiguredProvider] GET ${integrationPath} body contained an invalid integration (port ${port})`
5260
+ );
5261
+ valid = false;
5262
+ break;
5263
+ }
5264
+ }
5265
+ if (valid) {
5266
+ integrationReady = body.data.some(
5267
+ (integration) => integration.connections.length > 0
5268
+ );
5269
+ }
5270
+ }
3998
5271
  }
3999
- const body = await res.json();
4000
- if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
5272
+ } catch (error2) {
5273
+ console.error(
5274
+ `[hasAnyConfiguredProvider] GET ${integrationPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
5275
+ );
5276
+ }
5277
+ if (integrationReady === true) return true;
5278
+ const providerPath = `/api/provider${query}`;
5279
+ let providerReady = null;
5280
+ try {
5281
+ const res = await client.request(providerPath);
5282
+ if (!res.ok) {
4001
5283
  console.error(
4002
- `[hasAnyConfiguredProvider] GET ${path} body had no integration data array (port ${port})`
5284
+ `[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
4003
5285
  );
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)) {
5286
+ } else {
5287
+ const body = await res.json();
5288
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
4008
5289
  console.error(
4009
- `[hasAnyConfiguredProvider] GET ${path} body contained an invalid integration (port ${port})`
5290
+ `[hasAnyConfiguredProvider] GET ${providerPath} body had no provider data array (port ${port})`
4010
5291
  );
4011
- return null;
5292
+ } else {
5293
+ let valid = true;
5294
+ for (const provider of body.data) {
5295
+ if (!provider || typeof provider !== "object" || Array.isArray(provider) || typeof provider.id !== "string") {
5296
+ console.error(
5297
+ `[hasAnyConfiguredProvider] GET ${providerPath} body contained an invalid provider (port ${port})`
5298
+ );
5299
+ valid = false;
5300
+ break;
5301
+ }
5302
+ }
5303
+ if (valid) {
5304
+ providerReady = body.data.some(
5305
+ (provider) => provider.activation === "enabled"
5306
+ );
5307
+ }
4012
5308
  }
4013
5309
  }
4014
- return body.data.some(
4015
- (integration) => Array.isArray(integration.connections) && integration.connections.length > 0
4016
- );
4017
5310
  } catch (error2) {
4018
5311
  console.error(
4019
- `[hasAnyConfiguredProvider] GET ${path} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
5312
+ `[hasAnyConfiguredProvider] GET ${providerPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
4020
5313
  );
4021
- return null;
4022
5314
  }
5315
+ if (providerReady === true) return true;
5316
+ if (integrationReady === null || providerReady === null) return null;
5317
+ return false;
4023
5318
  }
4024
5319
  try {
4025
5320
  const res = await requestWithClient(port, client, "/config/providers");
@@ -4172,14 +5467,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
4172
5467
  }
4173
5468
 
4174
5469
  // src/lib/opencode/session-v2.ts
4175
- function isRecord(value) {
5470
+ var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
5471
+ function isRecord5(value) {
4176
5472
  return typeof value === "object" && value !== null && !Array.isArray(value);
4177
5473
  }
4178
5474
  function finiteNumber2(value) {
4179
5475
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4180
5476
  }
4181
5477
  function adaptTime(value) {
4182
- if (!isRecord(value)) return void 0;
5478
+ if (!isRecord5(value)) return void 0;
4183
5479
  const created = finiteNumber2(value.created);
4184
5480
  const completed = finiteNumber2(value.completed);
4185
5481
  if (created === void 0 && completed === void 0) return void 0;
@@ -4189,11 +5485,11 @@ function adaptTime(value) {
4189
5485
  };
4190
5486
  }
4191
5487
  function adaptTokens(value) {
4192
- if (!isRecord(value)) return void 0;
5488
+ if (!isRecord5(value)) return void 0;
4193
5489
  const input = finiteNumber2(value.input);
4194
5490
  const output = finiteNumber2(value.output);
4195
5491
  const reasoning = finiteNumber2(value.reasoning);
4196
- const cache = isRecord(value.cache) ? {
5492
+ const cache = isRecord5(value.cache) ? {
4197
5493
  ...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
4198
5494
  ...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
4199
5495
  } : void 0;
@@ -4217,7 +5513,7 @@ function adaptMessageInfo(value, role) {
4217
5513
  if (typeof value.finish === "string") info.finish = value.finish;
4218
5514
  if ("error" in value) info.error = value.error;
4219
5515
  if (typeof value.agent === "string") info.agent = value.agent;
4220
- if (isRecord(value.model)) {
5516
+ if (isRecord5(value.model)) {
4221
5517
  if (typeof value.model.id === "string") info.modelID = value.model.id;
4222
5518
  if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
4223
5519
  }
@@ -4226,8 +5522,16 @@ function adaptMessageInfo(value, role) {
4226
5522
  if (tokens) info.tokens = tokens;
4227
5523
  return info;
4228
5524
  }
5525
+ function v2ChildSessionEvidence(content) {
5526
+ const state = isRecord5(content.state) ? content.state : null;
5527
+ const metadata = state && isRecord5(state.metadata) ? state.metadata : null;
5528
+ const sessionId = typeof metadata?.sessionID === "string" && metadata.sessionID.length > 0 ? metadata.sessionID : typeof metadata?.sessionId === "string" && metadata.sessionId.length > 0 ? metadata.sessionId : null;
5529
+ if (sessionId === null) return null;
5530
+ const time = isRecord5(content.time) ? content.time : null;
5531
+ return { sessionId, startedAtMs: finiteNumber2(time?.created) ?? null };
5532
+ }
4229
5533
  function adaptV2Message(value) {
4230
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
5534
+ if (!isRecord5(value) || typeof value.id !== "string" || typeof value.type !== "string") {
4231
5535
  return null;
4232
5536
  }
4233
5537
  if (value.type === "user") {
@@ -4240,7 +5544,13 @@ function adaptV2Message(value) {
4240
5544
  if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
4241
5545
  const parts = [];
4242
5546
  for (const content of value.content) {
4243
- if (!isRecord(content) || typeof content.type !== "string") return null;
5547
+ if (!isRecord5(content) || typeof content.type !== "string") return null;
5548
+ const toolPart = projectV2ToolContent(content);
5549
+ if (toolPart) {
5550
+ const childSession = v2ChildSessionEvidence(content);
5551
+ parts.push(childSession ? { ...toolPart, childSession } : toolPart);
5552
+ continue;
5553
+ }
4244
5554
  if (content.type === "text") {
4245
5555
  if (typeof content.text !== "string") return null;
4246
5556
  parts.push({ type: "text", text: content.text });
@@ -4254,23 +5564,23 @@ function adaptV2Message(value) {
4254
5564
  };
4255
5565
  }
4256
5566
  function adaptFormTool(value) {
4257
- if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
5567
+ if (!isRecord5(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
4258
5568
  return void 0;
4259
5569
  }
4260
5570
  return { messageID: value.messageID, callID: value.id };
4261
5571
  }
4262
5572
  function adaptV2FormWire(value) {
4263
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5573
+ if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
4264
5574
  return null;
4265
5575
  }
4266
5576
  return value;
4267
5577
  }
4268
5578
  function adaptV2FormField(value, header) {
4269
- if (!isRecord(value)) return null;
5579
+ if (!isRecord5(value)) return null;
4270
5580
  const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
4271
5581
  if (!question) return null;
4272
5582
  const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
4273
- if (!isRecord(option)) return [];
5583
+ if (!isRecord5(option)) return [];
4274
5584
  const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
4275
5585
  if (!label) return [];
4276
5586
  return [
@@ -4288,7 +5598,7 @@ function adaptV2Form(value) {
4288
5598
  const header = typeof form.title === "string" ? form.title : "";
4289
5599
  const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
4290
5600
  if (questions.length === 0) return null;
4291
- const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
5601
+ const tool = isRecord5(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
4292
5602
  return {
4293
5603
  id: form.id,
4294
5604
  sessionID: form.sessionID,
@@ -4298,7 +5608,7 @@ function adaptV2Form(value) {
4298
5608
  };
4299
5609
  }
4300
5610
  function adaptV2FormList(value) {
4301
- if (!isRecord(value) || !Array.isArray(value.data)) return null;
5611
+ if (!isRecord5(value) || !Array.isArray(value.data)) return null;
4302
5612
  return value.data.map(adaptV2Form).filter((form) => form !== null);
4303
5613
  }
4304
5614
  function adaptPattern(value) {
@@ -4309,7 +5619,7 @@ function adaptPattern(value) {
4309
5619
  return void 0;
4310
5620
  }
4311
5621
  function adaptV2PermissionWire(value) {
4312
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5622
+ if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
4313
5623
  return null;
4314
5624
  }
4315
5625
  if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
@@ -4321,12 +5631,12 @@ function adaptV2Permission(value) {
4321
5631
  const type = permission.permission ?? permission.action;
4322
5632
  if (!type) return null;
4323
5633
  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;
5634
+ const time = isRecord5(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
4325
5635
  return {
4326
5636
  id: permission.id,
4327
5637
  type,
4328
5638
  sessionID: permission.sessionID,
4329
- metadata: isRecord(permission.metadata) ? permission.metadata : {},
5639
+ metadata: isRecord5(permission.metadata) ? permission.metadata : {},
4330
5640
  raw: permission,
4331
5641
  ...pattern !== void 0 ? { pattern } : {},
4332
5642
  ...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
@@ -4336,12 +5646,12 @@ function adaptV2Permission(value) {
4336
5646
  };
4337
5647
  }
4338
5648
  function adaptV2PermissionList(value) {
4339
- if (!isRecord(value) || !Array.isArray(value.data)) return null;
5649
+ if (!isRecord5(value) || !Array.isArray(value.data)) return null;
4340
5650
  return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
4341
5651
  }
4342
5652
  function adaptV2Session(value) {
4343
- if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
4344
- const time = isRecord(value.time) ? {
5653
+ if (!isRecord5(value) || typeof value.id !== "string" || value.id.length === 0) return null;
5654
+ const time = isRecord5(value.time) ? {
4345
5655
  ...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
4346
5656
  ...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
4347
5657
  } : void 0;
@@ -4353,7 +5663,7 @@ function adaptV2Session(value) {
4353
5663
  };
4354
5664
  }
4355
5665
  function adaptV2SessionList(value) {
4356
- if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
5666
+ if (!isRecord5(value) || !Array.isArray(value.data) || !isRecord5(value.cursor)) return null;
4357
5667
  return {
4358
5668
  data: value.data.map(adaptV2Session).filter((session) => session !== null),
4359
5669
  cursor: value.cursor
@@ -4362,18 +5672,18 @@ function adaptV2SessionList(value) {
4362
5672
  function adaptV2Location(value) {
4363
5673
  const candidates = [
4364
5674
  value,
4365
- isRecord(value) ? value.data : void 0,
4366
- isRecord(value) ? value.location : void 0
5675
+ isRecord5(value) ? value.data : void 0,
5676
+ isRecord5(value) ? value.location : void 0
4367
5677
  ];
4368
5678
  for (const candidate of candidates) {
4369
- if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
5679
+ if (!isRecord5(candidate) || typeof candidate.directory !== "string") continue;
4370
5680
  const directory = candidate.directory.trim();
4371
5681
  if (directory) return directory;
4372
5682
  }
4373
5683
  return null;
4374
5684
  }
4375
5685
  function adaptV2Messages(value) {
4376
- if (!isRecord(value) || !Array.isArray(value.data)) return [];
5686
+ if (!isRecord5(value) || !Array.isArray(value.data)) return [];
4377
5687
  return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
4378
5688
  }
4379
5689
  async function readJson(response) {
@@ -4388,7 +5698,7 @@ async function readJson(response) {
4388
5698
  async function readData(client, path, init) {
4389
5699
  const response = await client.request(path, init);
4390
5700
  const body = await readJson(response);
4391
- if (!isRecord(body) || !("data" in body)) {
5701
+ if (!isRecord5(body) || !("data" in body)) {
4392
5702
  throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
4393
5703
  }
4394
5704
  return body.data;
@@ -4399,6 +5709,14 @@ var OpenCodeV2PromptAckError = class extends Error {
4399
5709
  this.name = "OpenCodeV2PromptAckError";
4400
5710
  }
4401
5711
  };
5712
+ var OpenCodeV2ModelSwitchError = class extends Error {
5713
+ statusCode;
5714
+ constructor(error2) {
5715
+ super(error2 instanceof Error ? error2.message : String(error2));
5716
+ this.name = "OpenCodeV2ModelSwitchError";
5717
+ this.statusCode = error2 instanceof OpenCodeRequestError ? error2.statusCode : null;
5718
+ }
5719
+ };
4402
5720
  async function getOpenCodeDirectoryV2(client) {
4403
5721
  try {
4404
5722
  return adaptV2Location(await readJson(await client.request("/api/location")));
@@ -4415,7 +5733,7 @@ async function createV2Session(client, directory) {
4415
5733
  headers: { "Content-Type": "application/json" },
4416
5734
  body: JSON.stringify({ location: { directory } })
4417
5735
  });
4418
- if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
5736
+ if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
4419
5737
  throw new Error("OpenCode V2 create session response was missing data.id");
4420
5738
  }
4421
5739
  return data.id;
@@ -4426,25 +5744,35 @@ async function getV2Session(client, sessionId) {
4426
5744
  if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
4427
5745
  return session;
4428
5746
  }
4429
- async function listV2SessionPage(client, cursor) {
5747
+ async function listV2SessionPage(client, cursor, signal) {
5748
+ const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
4430
5749
  const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
4431
5750
  try {
4432
- return adaptV2SessionList(await readJson(await client.request(path)));
5751
+ const response = await client.request(path, signal ? { signal } : void 0, {
5752
+ allowStatuses: ALL_HTTP_STATUSES2
5753
+ });
5754
+ if (!response.ok) {
5755
+ console.error(
5756
+ `[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
5757
+ );
5758
+ return null;
5759
+ }
5760
+ return adaptV2SessionList(await readJson(response));
4433
5761
  } catch (error2) {
4434
- console.error(
4435
- `[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
4436
- );
5762
+ if (signal?.aborted) return null;
5763
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
5764
+ console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
4437
5765
  return null;
4438
5766
  }
4439
5767
  }
4440
- async function listV2Sessions(client) {
5768
+ async function listV2Sessions(client, signal) {
4441
5769
  const sessions = [];
4442
5770
  const seenCursors = /* @__PURE__ */ new Set();
4443
5771
  let cursor;
4444
5772
  let hasNextPage = true;
4445
5773
  try {
4446
5774
  while (hasNextPage) {
4447
- const page = await listV2SessionPage(client, cursor);
5775
+ const page = await listV2SessionPage(client, cursor, signal);
4448
5776
  if (!page) return null;
4449
5777
  sessions.push(...page.data);
4450
5778
  const next = page.cursor.next;
@@ -4460,8 +5788,9 @@ async function listV2Sessions(client) {
4460
5788
  }
4461
5789
  return sessions;
4462
5790
  } catch (error2) {
5791
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4463
5792
  console.error(
4464
- `[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
5793
+ `[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
4465
5794
  );
4466
5795
  return null;
4467
5796
  }
@@ -4471,7 +5800,7 @@ async function deleteV2Session(client, sessionId) {
4471
5800
  const response = await client.request(
4472
5801
  `/api/session/${encodeURIComponent(sessionId)}`,
4473
5802
  { method: "DELETE" },
4474
- { allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
5803
+ { allowStatuses: ALL_HTTP_STATUSES2 }
4475
5804
  );
4476
5805
  if (response.status >= 200 && response.status < 300) {
4477
5806
  return { ok: true, status: response.status };
@@ -4507,7 +5836,34 @@ async function v2SessionExists(client, sessionId) {
4507
5836
  return null;
4508
5837
  }
4509
5838
  }
4510
- async function sendV2Prompt(client, sessionId, text) {
5839
+ async function sendV2Prompt(client, sessionId, text, options, onInvalidModelSelection) {
5840
+ const modelSelection = options?.model?.trim();
5841
+ const { model, variant } = splitModelVariant(modelSelection);
5842
+ const separator = model?.indexOf("/") ?? -1;
5843
+ const modelReference = model && separator > 0 && separator < model.length - 1 ? {
5844
+ id: model.slice(separator + 1),
5845
+ providerID: model.slice(0, separator),
5846
+ ...variant ? { variant } : {}
5847
+ } : void 0;
5848
+ if (modelSelection && !modelReference) onInvalidModelSelection?.();
5849
+ if (options?.agent) {
5850
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/agent`, {
5851
+ method: "POST",
5852
+ headers: { "Content-Type": "application/json" },
5853
+ body: JSON.stringify({ agent: options.agent })
5854
+ });
5855
+ }
5856
+ if (modelReference) {
5857
+ try {
5858
+ await client.request(`/api/session/${encodeURIComponent(sessionId)}/model`, {
5859
+ method: "POST",
5860
+ headers: { "Content-Type": "application/json" },
5861
+ body: JSON.stringify({ model: modelReference })
5862
+ });
5863
+ } catch (error2) {
5864
+ throw new OpenCodeV2ModelSwitchError(error2);
5865
+ }
5866
+ }
4511
5867
  const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
4512
5868
  const response = await client.request(path, {
4513
5869
  method: "POST",
@@ -4522,8 +5878,8 @@ async function sendV2Prompt(client, sessionId, text) {
4522
5878
  `OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
4523
5879
  );
4524
5880
  }
4525
- const data = isRecord(body) && "data" in body ? body.data : void 0;
4526
- if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
5881
+ const data = isRecord5(body) && "data" in body ? body.data : void 0;
5882
+ if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
4527
5883
  throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
4528
5884
  }
4529
5885
  return data.id;
@@ -4532,7 +5888,7 @@ async function getV2SessionMessages(client, sessionId) {
4532
5888
  const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
4533
5889
  try {
4534
5890
  const body = await readJson(await client.request(path));
4535
- if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
5891
+ if (!isRecord5(body) || !Array.isArray(body.data) || !isRecord5(body.cursor)) return null;
4536
5892
  return adaptV2Messages(body);
4537
5893
  } catch (error2) {
4538
5894
  console.error(
@@ -4571,7 +5927,7 @@ async function interruptV2Session(client, sessionId) {
4571
5927
  async function getV2ActiveSessions(client) {
4572
5928
  try {
4573
5929
  const body = await readJson(await client.request("/api/session/active"));
4574
- if (!isRecord(body) || !isRecord(body.data)) return null;
5930
+ if (!isRecord5(body) || !isRecord5(body.data)) return null;
4575
5931
  return body.data;
4576
5932
  } catch (error2) {
4577
5933
  console.error(
@@ -4587,8 +5943,8 @@ async function isV2SessionOngoing(client, sessionId) {
4587
5943
  }
4588
5944
  function sessionErrorReason2(value) {
4589
5945
  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;
5946
+ if (isRecord5(value)) {
5947
+ const data = isRecord5(value.data) ? value.data : void 0;
4592
5948
  const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
4593
5949
  if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
4594
5950
  }
@@ -4604,17 +5960,17 @@ function adaptV2SessionErrorEvent(value) {
4604
5960
  return null;
4605
5961
  }
4606
5962
  }
4607
- if (!isRecord(parsed)) return null;
5963
+ if (!isRecord5(parsed)) return null;
4608
5964
  try {
4609
5965
  const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
4610
5966
  if (establishedShape) return establishedShape;
4611
5967
  } catch (error2) {
4612
5968
  void error2;
4613
5969
  }
4614
- const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
5970
+ const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord5);
4615
5971
  for (const event of candidates) {
4616
5972
  if (event.type !== "session.error") continue;
4617
- const properties = [event.properties, event.data, event].find(isRecord);
5973
+ const properties = [event.properties, event.data, event].find(isRecord5);
4618
5974
  if (!properties) continue;
4619
5975
  const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
4620
5976
  if (!sessionId) continue;
@@ -4664,13 +6020,13 @@ async function readV2SessionErrorStream(client, options) {
4664
6020
  }
4665
6021
 
4666
6022
  // src/lib/opencode/credential-import-v2.ts
4667
- import { readFileSync as readFileSync5 } from "node:fs";
4668
- import { join as join6 } from "node:path";
6023
+ import { readFileSync as readFileSync8 } from "node:fs";
6024
+ import { join as join8 } from "node:path";
4669
6025
 
4670
6026
  // src/lib/openai-usage.ts
4671
- import { readFileSync as readFileSync4 } from "node:fs";
6027
+ import { readFileSync as readFileSync7 } from "node:fs";
4672
6028
  import { homedir as homedir3 } from "node:os";
4673
- import { join as join5 } from "node:path";
6029
+ import { join as join7 } from "node:path";
4674
6030
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4675
6031
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4676
6032
  var OpenAiUsageError = class extends Error {
@@ -4684,7 +6040,7 @@ function isLocalCredentialProblem2(err) {
4684
6040
  }
4685
6041
  function readOpenCodeChatGptCredentials() {
4686
6042
  try {
4687
- const raw = readFileSync4(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
6043
+ const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4688
6044
  let parsed;
4689
6045
  try {
4690
6046
  parsed = JSON.parse(raw);
@@ -4750,7 +6106,7 @@ function parseCodexUsageHeaders(headers) {
4750
6106
  function normalizeProbeModel(model) {
4751
6107
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4752
6108
  }
4753
- function isRecord2(value) {
6109
+ function isRecord6(value) {
4754
6110
  return typeof value === "object" && value !== null && !Array.isArray(value);
4755
6111
  }
4756
6112
  function unsupportedProbeModels(reason, port) {
@@ -4761,14 +6117,14 @@ async function resolveV1ProbeModels(client, port) {
4761
6117
  try {
4762
6118
  const response = await client.request("/config/providers");
4763
6119
  const body = await response.json();
4764
- if (!isRecord2(body) || !Array.isArray(body.providers)) {
6120
+ if (!isRecord6(body) || !Array.isArray(body.providers)) {
4765
6121
  return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
4766
6122
  }
4767
6123
  const provider = body.providers.find(
4768
- (candidate) => isRecord2(candidate) && candidate.id === "openai"
6124
+ (candidate) => isRecord6(candidate) && candidate.id === "openai"
4769
6125
  );
4770
- if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
4771
- const defaults2 = isRecord2(body.default) ? body.default : void 0;
6126
+ if (!provider || !isRecord6(provider.models)) return { status: "supported", models: [] };
6127
+ const defaults2 = isRecord6(body.default) ? body.default : void 0;
4772
6128
  const candidates = [
4773
6129
  ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4774
6130
  ...Object.keys(provider.models)
@@ -4790,14 +6146,14 @@ async function resolveV2ProbeModels(client, port) {
4790
6146
  try {
4791
6147
  const response = await client.request(path);
4792
6148
  const body = await response.json();
4793
- if (!isRecord2(body) || !Array.isArray(body.data)) {
6149
+ if (!isRecord6(body) || !Array.isArray(body.data)) {
4794
6150
  return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
4795
6151
  }
4796
6152
  const provider = body.data.find(
4797
- (candidate) => isRecord2(candidate) && candidate.id === "openai"
6153
+ (candidate) => isRecord6(candidate) && candidate.id === "openai"
4798
6154
  );
4799
6155
  if (!provider) return { status: "supported", models: [] };
4800
- if (!isRecord2(provider.models)) {
6156
+ if (!isRecord6(provider.models)) {
4801
6157
  return unsupportedProbeModels(
4802
6158
  "V2 provider response has no safe OpenAI model catalogue",
4803
6159
  port
@@ -4907,7 +6263,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
4907
6263
  var CONNECT_KEY_RETRY_DELAY_MS = 250;
4908
6264
  var INTEGRATION_LIST_MAX_READS = 10;
4909
6265
  var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
4910
- function isRecord3(value) {
6266
+ function isRecord7(value) {
4911
6267
  return typeof value === "object" && value !== null && !Array.isArray(value);
4912
6268
  }
4913
6269
  var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
@@ -4916,14 +6272,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
4916
6272
  }
4917
6273
  function readOpenCodeCredentials(homeDir, log3) {
4918
6274
  try {
4919
- const raw = readFileSync5(join6(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
6275
+ const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4920
6276
  let parsed;
4921
6277
  try {
4922
6278
  parsed = JSON.parse(raw);
4923
6279
  } catch (error2) {
4924
6280
  return null;
4925
6281
  }
4926
- return isRecord3(parsed) ? parsed : null;
6282
+ return isRecord7(parsed) ? parsed : null;
4927
6283
  } catch (error2) {
4928
6284
  const code = error2.code;
4929
6285
  if (code !== "ENOENT" && code !== "ENOTDIR") {
@@ -4936,7 +6292,7 @@ function classifyOpenCodeCredentials(credentials2) {
4936
6292
  const importable = [];
4937
6293
  const unsupported = [];
4938
6294
  for (const [integrationId, value] of Object.entries(credentials2)) {
4939
- if (!isRecord3(value) || typeof value.type !== "string") {
6295
+ if (!isRecord7(value) || typeof value.type !== "string") {
4940
6296
  unsupported.push({ integrationId, reason: "unrecognised credential entry" });
4941
6297
  continue;
4942
6298
  }
@@ -4951,7 +6307,7 @@ function classifyOpenCodeCredentials(credentials2) {
4951
6307
  unsupported.push({ integrationId, reason: "unrecognised credential entry" });
4952
6308
  continue;
4953
6309
  }
4954
- if (value.metadata === void 0 || isRecord3(value.metadata) && Object.keys(value.metadata).length === 0) {
6310
+ if (value.metadata === void 0 || isRecord7(value.metadata) && Object.keys(value.metadata).length === 0) {
4955
6311
  importable.push({ integrationId, key: value.key });
4956
6312
  } else {
4957
6313
  unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
@@ -4964,7 +6320,7 @@ function statusFromRequestError(error2) {
4964
6320
  const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
4965
6321
  return match ? Number(match[1]) : void 0;
4966
6322
  }
4967
- function failureMessage(status2) {
6323
+ function failureMessage2(status2) {
4968
6324
  return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
4969
6325
  }
4970
6326
  function isRetryableStatus(status2) {
@@ -4991,18 +6347,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
4991
6347
  const failure = {
4992
6348
  integrationId: credential.integrationId,
4993
6349
  status: response.status,
4994
- message: failureMessage(response.status)
6350
+ message: failureMessage2(response.status)
4995
6351
  };
4996
- log3(`${failureMessage(failure.status)} for ${failure.integrationId}`, "warn");
6352
+ log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
4997
6353
  return { ok: false, failure };
4998
6354
  } catch (error2) {
4999
6355
  const status2 = statusFromRequestError(error2);
5000
6356
  const failure = {
5001
6357
  integrationId: credential.integrationId,
5002
6358
  ...status2 === void 0 ? {} : { status: status2 },
5003
- message: failureMessage(status2)
6359
+ message: failureMessage2(status2)
5004
6360
  };
5005
- log3(`${failureMessage(status2)} for ${failure.integrationId}`, "warn");
6361
+ log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
5006
6362
  return { ok: false, failure };
5007
6363
  }
5008
6364
  }
@@ -5028,10 +6384,10 @@ async function importOpenCodeCredentials({
5028
6384
  return result;
5029
6385
  }
5030
6386
  function parseIntegrationList(value) {
5031
- if (!isRecord3(value) || !Array.isArray(value.data)) return null;
6387
+ if (!isRecord7(value) || !Array.isArray(value.data)) return null;
5032
6388
  const integrationIds = /* @__PURE__ */ new Set();
5033
6389
  for (const integration of value.data) {
5034
- if (!isRecord3(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
6390
+ if (!isRecord7(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
5035
6391
  return null;
5036
6392
  }
5037
6393
  if (integration.connections.length > 0) integrationIds.add(integration.id);
@@ -5116,87 +6472,9 @@ async function importOpenCodeV2Credentials({
5116
6472
 
5117
6473
  // src/lib/opencode/session-db-boot.ts
5118
6474
  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";
6475
+ import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
5120
6476
  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
6477
+ import { dirname as dirname7, resolve as resolvePath } from "node:path";
5200
6478
  var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
5201
6479
  var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
5202
6480
  var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
@@ -5246,8 +6524,8 @@ function clearMarker(options) {
5246
6524
  function markNoReplicate(options, message) {
5247
6525
  if (options.noReplicateMarker) {
5248
6526
  try {
5249
- mkdirSync2(dirname5(options.noReplicateMarker), { recursive: true });
5250
- writeFileSync2(options.noReplicateMarker, "");
6527
+ mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
6528
+ writeFileSync4(options.noReplicateMarker, "");
5251
6529
  } catch (error2) {
5252
6530
  options.log(
5253
6531
  `Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5401,8 +6679,8 @@ async function ensureLitestreamConfig(options, env) {
5401
6679
  return null;
5402
6680
  }
5403
6681
  try {
5404
- mkdirSync2(dirname5(configPath), { recursive: true });
5405
- writeFileSync2(configPath, rendered.stdout);
6682
+ mkdirSync4(dirname7(configPath), { recursive: true });
6683
+ writeFileSync4(configPath, rendered.stdout);
5406
6684
  } catch (error2) {
5407
6685
  options.log(
5408
6686
  `Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5667,8 +6945,8 @@ async function restoreAndVerifySessionDb(options) {
5667
6945
 
5668
6946
  // src/lib/opencode/session-db-provenance.ts
5669
6947
  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";
6948
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
6949
+ import { dirname as dirname8, join as join9 } from "node:path";
5672
6950
  var require2 = createRequire(import.meta.url);
5673
6951
  function readSessionDbMigrationIds(dbPath) {
5674
6952
  let db;
@@ -5705,12 +6983,12 @@ function readSessionDbMigrationIds(dbPath) {
5705
6983
  }
5706
6984
  function sessionDbProvenanceStatePath(homeDir, env) {
5707
6985
  const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
5708
- return override || join7(homeDir, ".local", "state", "evident", "session-db-provenance.json");
6986
+ return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5709
6987
  }
5710
6988
  function loadSessionDbProvenanceState(path) {
5711
6989
  let value;
5712
6990
  try {
5713
- value = JSON.parse(readFileSync6(path, "utf8"));
6991
+ value = JSON.parse(readFileSync9(path, "utf8"));
5714
6992
  } catch (error2) {
5715
6993
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
5716
6994
  console.error(
@@ -5734,8 +7012,8 @@ function loadSessionDbProvenanceState(path) {
5734
7012
  }
5735
7013
  function saveSessionDbProvenanceState(path, state) {
5736
7014
  try {
5737
- mkdirSync3(dirname6(path), { recursive: true });
5738
- writeFileSync3(path, `${JSON.stringify(state, null, 2)}
7015
+ mkdirSync5(dirname8(path), { recursive: true });
7016
+ writeFileSync5(path, `${JSON.stringify(state, null, 2)}
5739
7017
  `, "utf8");
5740
7018
  } catch (error2) {
5741
7019
  console.error(
@@ -5790,6 +7068,26 @@ function isSessionDbProvenanceRecord(value) {
5790
7068
  return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
5791
7069
  }
5792
7070
 
7071
+ // src/lib/opencode/detect.ts
7072
+ function describeProbe(route, result) {
7073
+ if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
7074
+ if (result.error) return `${route}: no HTTP response (${result.error})`;
7075
+ return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
7076
+ }
7077
+ async function classifyRunningOpenCode(port) {
7078
+ const v1 = await checkOpenCodeHealth(port);
7079
+ if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
7080
+ const v2 = await checkOpenCode2Health(port);
7081
+ if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
7082
+ const detail = [
7083
+ describeProbe("V1 /global/health", v1),
7084
+ describeProbe("V2 /api/info (or /api/health fallback)", v2)
7085
+ ].join("; ");
7086
+ if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
7087
+ if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
7088
+ return { kind: "no-server-identified", detail };
7089
+ }
7090
+
5793
7091
  // src/lib/opencode/opencode2-package.ts
5794
7092
  var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
5795
7093
 
@@ -6098,26 +7396,28 @@ async function startOpenCode(port, options = {}) {
6098
7396
  }
6099
7397
  async function startOpenCode2(port, options = {}) {
6100
7398
  const password = randomBytes(24).toString("hex");
6101
- let command = "opencode2";
7399
+ let command = options.command ?? "opencode2";
6102
7400
  const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
6103
7401
  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
- ];
7402
+ if (options.command === void 0) {
7403
+ try {
7404
+ execSync("which opencode2", { stdio: "ignore" });
7405
+ } catch {
7406
+ command = "npx";
7407
+ args = [
7408
+ "-y",
7409
+ "-p",
7410
+ OPENCODE2_PACKAGE_SPEC,
7411
+ "--",
7412
+ "opencode2",
7413
+ "serve",
7414
+ "--port",
7415
+ port.toString(),
7416
+ "--hostname",
7417
+ "127.0.0.1",
7418
+ ...logLevel
7419
+ ];
7420
+ }
6121
7421
  }
6122
7422
  const child = spawn3(command, args, {
6123
7423
  env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
@@ -6174,6 +7474,56 @@ function isOpenCode2Installed() {
6174
7474
  return false;
6175
7475
  }
6176
7476
  }
7477
+ function readVersionOutput(command) {
7478
+ try {
7479
+ return execSync2(`${command} --version`, {
7480
+ encoding: "utf-8",
7481
+ stdio: ["ignore", "pipe", "pipe"]
7482
+ }).trim();
7483
+ } catch (error2) {
7484
+ const output = typeof error2 === "object" && error2 !== null ? [
7485
+ "stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
7486
+ "stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
7487
+ ].filter(Boolean).join("\n") : "";
7488
+ return output || (error2 instanceof Error ? error2.message : String(error2));
7489
+ }
7490
+ }
7491
+ function parseOpenCodeVersion(rawVersion) {
7492
+ const output = rawVersion.trim();
7493
+ const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
7494
+ const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
7495
+ if (prefix === "opencode2") return { major: "v2", version: version2 };
7496
+ const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
7497
+ if (!major) return null;
7498
+ if (Number(major) === 1) return { major: "v1", version: version2 };
7499
+ if (Number(major) >= 2) return { major: "v2", version: version2 };
7500
+ return null;
7501
+ }
7502
+ function resolveInstalledOpenCode() {
7503
+ let opencode2Available = false;
7504
+ if (isOpenCodeInstalled()) {
7505
+ const rawVersion = readVersionOutput("opencode");
7506
+ const parsed = parseOpenCodeVersion(rawVersion);
7507
+ if (parsed?.major === "v1") {
7508
+ return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
7509
+ }
7510
+ if (parsed?.major === "v2") {
7511
+ return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
7512
+ }
7513
+ opencode2Available = isOpenCode2Installed();
7514
+ if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
7515
+ }
7516
+ if (opencode2Available || isOpenCode2Installed()) {
7517
+ const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
7518
+ return {
7519
+ kind: "installed",
7520
+ major: "v2",
7521
+ command: "opencode2",
7522
+ version: parsed?.version
7523
+ };
7524
+ }
7525
+ return { kind: "none" };
7526
+ }
6177
7527
  async function promptOpenCodeInstall(interactive) {
6178
7528
  if (!interactive) {
6179
7529
  console.log(
@@ -6274,124 +7624,17 @@ function buildNoProviderWarning(hasProvider) {
6274
7624
  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
7625
  }
6276
7626
 
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 };
6382
- }
6383
-
6384
7627
  // src/lib/opencode/session-db-size.ts
6385
7628
  import { statSync as statSync4 } from "node:fs";
6386
- import { join as join8 } from "node:path";
7629
+ import { join as join10 } from "node:path";
6387
7630
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
6388
7631
  function statSessionDbBytes(homeDir) {
6389
- const dbPath = join8(homeDir, ".local", "share", "opencode", "opencode.db");
7632
+ const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
6390
7633
  try {
6391
7634
  return statSync4(dbPath).size;
6392
7635
  } catch (err) {
6393
- const isMissingFile2 = err instanceof Error && "code" in err && err.code === "ENOENT";
6394
- if (!isMissingFile2) {
7636
+ const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
7637
+ if (!isMissingFile4) {
6395
7638
  console.error(
6396
7639
  `[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
6397
7640
  );
@@ -6416,12 +7659,12 @@ function buildSessionStoreSizeWarning(input) {
6416
7659
  // src/lib/opencode/log-tail.ts
6417
7660
  import { statSync as statSync5 } from "node:fs";
6418
7661
  import { homedir as homedir5 } from "node:os";
6419
- import { join as join9 } from "node:path";
7662
+ import { join as join11 } from "node:path";
6420
7663
  import { open as open2, stat } from "node:fs/promises";
6421
7664
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
6422
7665
  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");
7666
+ const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
7667
+ return join11(dataDir, "opencode", "log", "opencode.log");
6425
7668
  }
6426
7669
  function isEnoent(error2) {
6427
7670
  return error2?.code === "ENOENT";
@@ -6906,8 +8149,8 @@ function connectTunnel(options) {
6906
8149
  }
6907
8150
  forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
6908
8151
  } catch (error2) {
6909
- const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
6910
- onError?.(`Failed to handle binary message: ${errorMessage3}`);
8152
+ const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
8153
+ onError?.(`Failed to handle binary message: ${errorMessage4}`);
6911
8154
  }
6912
8155
  return;
6913
8156
  }
@@ -6915,8 +8158,8 @@ function connectTunnel(options) {
6915
8158
  try {
6916
8159
  message = JSON.parse(data.toString());
6917
8160
  } catch (error2) {
6918
- const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
6919
- onError?.(`Failed to handle message: ${errorMessage3}`);
8161
+ const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
8162
+ onError?.(`Failed to handle message: ${errorMessage4}`);
6920
8163
  return;
6921
8164
  }
6922
8165
  if (isStreamFrame(message)) {
@@ -7088,10 +8331,10 @@ var RunnerConnection = class _RunnerConnection {
7088
8331
  };
7089
8332
 
7090
8333
  // src/lib/tunnel/ready-marker.ts
7091
- import { writeFileSync as writeFileSync4 } from "node:fs";
8334
+ import { writeFileSync as writeFileSync6 } from "node:fs";
7092
8335
  function writeTunnelReadyMarker(path, agentId) {
7093
8336
  try {
7094
- writeFileSync4(path, `${agentId}
8337
+ writeFileSync6(path, `${agentId}
7095
8338
  `);
7096
8339
  return { ok: true };
7097
8340
  } catch (error2) {
@@ -7116,7 +8359,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
7116
8359
  }
7117
8360
 
7118
8361
  // src/lib/process-liveness.ts
7119
- import { readFileSync as readFileSync7 } from "node:fs";
8362
+ import { readFileSync as readFileSync10 } from "node:fs";
7120
8363
  function isProcessAlive(pid) {
7121
8364
  try {
7122
8365
  process.kill(pid, 0);
@@ -7131,7 +8374,7 @@ function isProcessAlive(pid) {
7131
8374
  }
7132
8375
  if (process.platform !== "linux") return true;
7133
8376
  try {
7134
- const status2 = readFileSync7(`/proc/${pid}/status`, "utf8");
8377
+ const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
7135
8378
  return !/^State:\s+Z(?:\s|$)/m.test(status2);
7136
8379
  } catch (error2) {
7137
8380
  console.error(
@@ -7198,8 +8441,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
7198
8441
  }
7199
8442
 
7200
8443
  // 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"]);
8444
+ var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
8445
+ var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
7203
8446
  function resolveResourceUsageReportingEnabled(flagValue, env) {
7204
8447
  if (flagValue === false) {
7205
8448
  return { enabled: false, warnings: [] };
@@ -7209,10 +8452,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
7209
8452
  return { enabled: true, warnings: [] };
7210
8453
  }
7211
8454
  const normalized = raw.trim().toLowerCase();
7212
- if (DISABLED_VALUES.has(normalized)) {
8455
+ if (DISABLED_VALUES2.has(normalized)) {
7213
8456
  return { enabled: false, warnings: [] };
7214
8457
  }
7215
- if (ENABLED_VALUES.has(normalized)) {
8458
+ if (ENABLED_VALUES2.has(normalized)) {
7216
8459
  return { enabled: true, warnings: [] };
7217
8460
  }
7218
8461
  return {
@@ -7490,18 +8733,18 @@ function createSessionStoreHealth(options = {}) {
7490
8733
  import { homedir as homedir6 } from "node:os";
7491
8734
 
7492
8735
  // src/lib/runner-file-sync.ts
7493
- import { join as join11 } from "node:path";
8736
+ import { join as join13 } from "node:path";
7494
8737
 
7495
8738
  // src/lib/file-push.ts
7496
8739
  import { randomUUID } from "node:crypto";
7497
8740
  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";
8741
+ 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
8742
  var FILE_MODE = 384;
7500
8743
  var DIRECTORY_MODE = 448;
7501
8744
  async function writePushedFile(request3) {
7502
- const { requestedPath, content, allowedDirectories, homeDir } = request3;
8745
+ const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
7503
8746
  const bytes = content.byteLength;
7504
- if (allowedDirectories.length === 0) {
8747
+ if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
7505
8748
  return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
7506
8749
  path: requestedPath,
7507
8750
  bytes
@@ -7526,10 +8769,10 @@ async function writePushedFile(request3) {
7526
8769
  }
7527
8770
  try {
7528
8771
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
7529
- dirname7(candidate)
8772
+ dirname9(candidate)
7530
8773
  );
7531
- const realTarget = join10(existingAncestor, ...missingSegments, basename2(candidate));
7532
- const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
8774
+ const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
8775
+ const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
7533
8776
  if (allowedDirectory === null) {
7534
8777
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7535
8778
  path: realTarget,
@@ -7538,8 +8781,8 @@ async function writePushedFile(request3) {
7538
8781
  }
7539
8782
  if (missingSegments.length > 0) {
7540
8783
  await createMissingDirectories(existingAncestor, missingSegments);
7541
- const realParent = await realpath(dirname7(realTarget));
7542
- if (realParent !== dirname7(realTarget) || !contains(allowedDirectory, realTarget)) {
8784
+ const realParent = await realpath(dirname9(realTarget));
8785
+ if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
7543
8786
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7544
8787
  path: realTarget,
7545
8788
  bytes,
@@ -7564,7 +8807,7 @@ function expandAndValidate(requestedPath, homeDir) {
7564
8807
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
7565
8808
  return null;
7566
8809
  }
7567
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
8810
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
7568
8811
  if (expanded.split(/[/\\]/).includes("..")) {
7569
8812
  return null;
7570
8813
  }
@@ -7582,7 +8825,7 @@ async function resolveNearestExistingAncestor(directory) {
7582
8825
  try {
7583
8826
  return { existingAncestor: await realpath(current), missingSegments };
7584
8827
  } catch (err) {
7585
- const parent = dirname7(current);
8828
+ const parent = dirname9(current);
7586
8829
  if (err.code !== "ENOENT" || parent === current) {
7587
8830
  throw err;
7588
8831
  }
@@ -7597,9 +8840,25 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
7597
8840
  log("warn", "file_push_allowed_directory_skipped", { directory, reason: "not_absolute" });
7598
8841
  continue;
7599
8842
  }
7600
- const realDirectory = await realpathCreatingIfMissing(directory);
7601
- if (realDirectory !== null && contains(realDirectory, realTarget)) {
7602
- return realDirectory;
8843
+ const realDirectory = await realpathCreatingIfMissing(directory);
8844
+ if (realDirectory !== null && contains(realDirectory, realTarget)) {
8845
+ return realDirectory;
8846
+ }
8847
+ }
8848
+ return null;
8849
+ }
8850
+ async function findMatchingAllowedFile(allowedFiles, realTarget) {
8851
+ for (const entry of allowedFiles) {
8852
+ if (!isAbsolute2(entry)) {
8853
+ log("warn", "file_push_allowed_directory_skipped", {
8854
+ directory: entry,
8855
+ reason: "not_absolute_file"
8856
+ });
8857
+ continue;
8858
+ }
8859
+ const realParent = await realpathCreatingIfMissing(dirname9(entry));
8860
+ if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
8861
+ return realParent;
7603
8862
  }
7604
8863
  }
7605
8864
  return null;
@@ -7637,13 +8896,13 @@ function contains(realDirectory, realTarget) {
7637
8896
  async function createMissingDirectories(existingAncestor, missingSegments) {
7638
8897
  let current = existingAncestor;
7639
8898
  for (const segment of missingSegments) {
7640
- current = join10(current, segment);
8899
+ current = join12(current, segment);
7641
8900
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
7642
8901
  await chmod(current, DIRECTORY_MODE);
7643
8902
  }
7644
8903
  }
7645
8904
  async function writeAtomically(realTarget, content) {
7646
- const temporaryPath = join10(dirname7(realTarget), `.evident-push-${randomUUID()}.tmp`);
8905
+ const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
7647
8906
  let handle;
7648
8907
  try {
7649
8908
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -7773,16 +9032,16 @@ var NOT_APPLIED = {
7773
9032
  opencodeAuthApplied: false
7774
9033
  };
7775
9034
  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);
9035
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
9036
+ return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7778
9037
  }
7779
9038
  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);
9039
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
9040
+ return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7782
9041
  }
7783
9042
  async function applyOne(options, file) {
7784
9043
  const label = `${file.id.slice(0, 8)} (${file.path})`;
7785
- if (options.allowedDirectories.length === 0) {
9044
+ if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
7786
9045
  options.log({
7787
9046
  level: "warn",
7788
9047
  message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
@@ -7809,6 +9068,7 @@ async function applyOne(options, file) {
7809
9068
  requestedPath: file.path,
7810
9069
  content: download.content,
7811
9070
  allowedDirectories: options.allowedDirectories,
9071
+ allowedFiles: options.allowedFiles,
7812
9072
  homeDir: options.homeDir
7813
9073
  });
7814
9074
  } catch (err) {
@@ -7916,7 +9176,7 @@ function describe(err) {
7916
9176
  }
7917
9177
 
7918
9178
  // src/lib/vault-file-sync.ts
7919
- import { createHash } from "node:crypto";
9179
+ import { createHash as createHash2 } from "node:crypto";
7920
9180
  async function syncVaultFiles(options) {
7921
9181
  const files = await listVaultFiles(options);
7922
9182
  if (files === null) return { applied: 0 };
@@ -8012,7 +9272,7 @@ async function reconcileVaultFile(options, file) {
8012
9272
  }
8013
9273
  const content = await downloadVaultFile(options, file);
8014
9274
  if (content === null) return false;
8015
- const actualSha256 = createHash("sha256").update(content).digest("hex");
9275
+ const actualSha256 = createHash2("sha256").update(content).digest("hex");
8016
9276
  if (actualSha256 !== file.content_sha256) {
8017
9277
  options.log({
8018
9278
  level: "warn",
@@ -8060,6 +9320,7 @@ async function applyVaultFile(options, file, content) {
8060
9320
  requestedPath: file.destination_path,
8061
9321
  content,
8062
9322
  allowedDirectories: options.allowedDirectories,
9323
+ allowedFiles: options.allowedFiles,
8063
9324
  homeDir: options.homeDir
8064
9325
  });
8065
9326
  if (!outcome.ok) {
@@ -8212,6 +9473,7 @@ var ChannelDriver = class _ChannelDriver {
8212
9473
  conversationFilter;
8213
9474
  retry;
8214
9475
  log;
9476
+ recordBinding;
8215
9477
  fetchImpl;
8216
9478
  sleep;
8217
9479
  pausedPollIntervalMs;
@@ -8219,6 +9481,7 @@ var ChannelDriver = class _ChannelDriver {
8219
9481
  stuckQueuedMs;
8220
9482
  now;
8221
9483
  fileSyncDirectories;
9484
+ fileSyncExactFiles;
8222
9485
  homeDir;
8223
9486
  maxActiveSessions;
8224
9487
  watcherStallMs;
@@ -8573,6 +9836,7 @@ var ChannelDriver = class _ChannelDriver {
8573
9836
  this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
8574
9837
  this.log = config.log ?? (() => {
8575
9838
  });
9839
+ this.recordBinding = config.recordBinding;
8576
9840
  this.fetchImpl = withRequestTimeout(
8577
9841
  config.fetchImpl ?? fetch,
8578
9842
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
@@ -8588,6 +9852,7 @@ var ChannelDriver = class _ChannelDriver {
8588
9852
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
8589
9853
  this.now = config.now ?? (() => Date.now());
8590
9854
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
9855
+ this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
8591
9856
  this.homeDir = config.homeDir ?? homedir6();
8592
9857
  this.maxActiveSessions = config.maxActiveSessions;
8593
9858
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
@@ -8685,6 +9950,7 @@ var ChannelDriver = class _ChannelDriver {
8685
9950
  getAuthHeader: this.getAuthHeader,
8686
9951
  fetchImpl: this.fetchImpl,
8687
9952
  allowedDirectories: this.fileSyncDirectories,
9953
+ allowedFiles: this.fileSyncExactFiles,
8688
9954
  homeDir: this.homeDir,
8689
9955
  ackFailures: this.fileAckFailures,
8690
9956
  log: this.log
@@ -8695,6 +9961,7 @@ var ChannelDriver = class _ChannelDriver {
8695
9961
  getAuthHeader: this.getAuthHeader,
8696
9962
  fetchImpl: this.fetchImpl,
8697
9963
  allowedDirectories: this.fileSyncDirectories,
9964
+ allowedFiles: this.fileSyncExactFiles,
8698
9965
  homeDir: this.homeDir,
8699
9966
  outcomes: this.vaultFileOutcomes,
8700
9967
  log: this.log
@@ -8757,6 +10024,9 @@ var ChannelDriver = class _ChannelDriver {
8757
10024
  let cappedSkips = 0;
8758
10025
  for (const conv of conversations) {
8759
10026
  if (this.stopped) break;
10027
+ if (conv.opencode_session_id) {
10028
+ this.recordSessionBinding(conv.id, conv.opencode_session_id);
10029
+ }
8760
10030
  if (this.maxActiveSessions !== void 0) {
8761
10031
  const activeSessionIds = this.activeSessionIdsForCap();
8762
10032
  const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
@@ -8839,12 +10109,12 @@ var ChannelDriver = class _ChannelDriver {
8839
10109
  this.sessions.delete(conversationId);
8840
10110
  this.supersede(conversationId, sessionId);
8841
10111
  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);
10112
+ 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.`;
10113
+ this.untrackableAck.set(messageId, errorMessage4);
8844
10114
  this.untrackableAckSessionIds.set(messageId, null);
8845
10115
  this.log({
8846
10116
  level: "error",
8847
- message: errorMessage3,
10117
+ message: errorMessage4,
8848
10118
  conversation_id: conversationId,
8849
10119
  message_id: messageId
8850
10120
  });
@@ -8852,7 +10122,7 @@ var ChannelDriver = class _ChannelDriver {
8852
10122
  conversationId,
8853
10123
  messageId,
8854
10124
  null,
8855
- errorMessage3
10125
+ errorMessage4
8856
10126
  );
8857
10127
  if (reportError !== null) {
8858
10128
  this.log({
@@ -9033,12 +10303,12 @@ var ChannelDriver = class _ChannelDriver {
9033
10303
  conversation_id: conv.id,
9034
10304
  message_id: message.id
9035
10305
  });
9036
- const errorMessage3 = this.untrackableAck.get(message.id);
10306
+ const errorMessage4 = this.untrackableAck.get(message.id);
9037
10307
  const reportError = await this.reportUntrackableAck(
9038
10308
  conv.id,
9039
10309
  message.id,
9040
10310
  null,
9041
- errorMessage3
10311
+ errorMessage4
9042
10312
  );
9043
10313
  if (reportError === null) {
9044
10314
  continue;
@@ -9126,7 +10396,7 @@ var ChannelDriver = class _ChannelDriver {
9126
10396
  const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
9127
10397
  const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
9128
10398
  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.`;
10399
+ const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
9130
10400
  this.log({
9131
10401
  level: "error",
9132
10402
  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 +10404,7 @@ var ChannelDriver = class _ChannelDriver {
9134
10404
  message_id: message.id
9135
10405
  });
9136
10406
  try {
9137
- await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
10407
+ await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
9138
10408
  classification: {
9139
10409
  kind: "model_auth",
9140
10410
  providerId,
@@ -9179,7 +10449,13 @@ var ChannelDriver = class _ChannelDriver {
9179
10449
  false
9180
10450
  );
9181
10451
  }
9182
- opencodeMessageId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, message.content) : await this.dispatchLocked(
10452
+ opencodeMessageId = this.isV2 ? await sendV2Prompt(
10453
+ this.openCodeClient,
10454
+ sessionId,
10455
+ message.content,
10456
+ options,
10457
+ () => this.logInvalidV2ModelSelection(conv.id, message.id)
10458
+ ) : await this.dispatchLocked(
9183
10459
  sessionId,
9184
10460
  () => sendPromptAsync(
9185
10461
  this.port,
@@ -9206,12 +10482,12 @@ var ChannelDriver = class _ChannelDriver {
9206
10482
  } catch (err) {
9207
10483
  if (err instanceof ChannelAuthError) throw err;
9208
10484
  if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
9209
- const errorMessage4 = err instanceof Error ? err.message : String(err);
9210
- this.untrackableAck.set(message.id, errorMessage4);
10485
+ const errorMessage5 = err instanceof Error ? err.message : String(err);
10486
+ this.untrackableAck.set(message.id, errorMessage5);
9211
10487
  this.untrackableAckSessionIds.set(message.id, null);
9212
10488
  this.log({
9213
10489
  level: "error",
9214
- message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
10490
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
9215
10491
  conversation_id: conv.id,
9216
10492
  message_id: message.id
9217
10493
  });
@@ -9219,7 +10495,7 @@ var ChannelDriver = class _ChannelDriver {
9219
10495
  conv.id,
9220
10496
  message.id,
9221
10497
  null,
9222
- errorMessage4
10498
+ errorMessage5
9223
10499
  );
9224
10500
  if (markError !== null) {
9225
10501
  this.log({
@@ -9254,7 +10530,11 @@ var ChannelDriver = class _ChannelDriver {
9254
10530
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
9255
10531
  break;
9256
10532
  }
9257
- const errorMessage3 = err instanceof Error ? err.message : String(err);
10533
+ if (this.isV2 && err instanceof OpenCodeV2ModelSwitchError) {
10534
+ await this.failV2ModelSwitch(conv, message, sessionId, err);
10535
+ break;
10536
+ }
10537
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
9258
10538
  this.sessions.delete(conv.id);
9259
10539
  this.supersede(conv.id, sessionId);
9260
10540
  this.log({
@@ -9263,7 +10543,7 @@ var ChannelDriver = class _ChannelDriver {
9263
10543
  conversation_id: conv.id,
9264
10544
  message_id: message.id
9265
10545
  });
9266
- await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
10546
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
9267
10547
  this.log({
9268
10548
  level: "warn",
9269
10549
  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 +10554,7 @@ var ChannelDriver = class _ChannelDriver {
9274
10554
  });
9275
10555
  this.log({
9276
10556
  level: "error",
9277
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
10557
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
9278
10558
  conversation_id: conv.id,
9279
10559
  message_id: message.id
9280
10560
  });
@@ -9847,6 +11127,14 @@ var ChannelDriver = class _ChannelDriver {
9847
11127
  this.dispatchNotStartedSignalled.set(message.id, branch);
9848
11128
  void this.postSignal(conv.id, message.id, "dispatch_not_started", { branch });
9849
11129
  }
11130
+ logInvalidV2ModelSelection(conversationId, messageId) {
11131
+ this.log({
11132
+ level: "warn",
11133
+ message: `Skipping an invalid OpenCode V2 model selection for message ${messageId.slice(0, 8)}; expected provider/model format`,
11134
+ conversation_id: conversationId,
11135
+ message_id: messageId
11136
+ });
11137
+ }
9850
11138
  /**
9851
11139
  * Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
9852
11140
  * but its own PATCH to record it failed. Fires at most once per (message,
@@ -10061,6 +11349,7 @@ var ChannelDriver = class _ChannelDriver {
10061
11349
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
10062
11350
  }
10063
11351
  this.sessions.set(conv.id, bound);
11352
+ this.recordSessionBinding(conv.id, bound);
10064
11353
  return { sessionId: bound, created: false };
10065
11354
  }
10066
11355
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
@@ -10074,6 +11363,7 @@ var ChannelDriver = class _ChannelDriver {
10074
11363
  const directory = await this.resolveOpenCodeDirectory();
10075
11364
  const sessionId = await this.createOpenCodeSession(directory);
10076
11365
  this.sessions.set(conversationId, sessionId);
11366
+ this.recordSessionBinding(conversationId, sessionId);
10077
11367
  await this.persistSession(conversationId, sessionId).catch((err) => {
10078
11368
  this.log({
10079
11369
  level: "warn",
@@ -10083,6 +11373,18 @@ var ChannelDriver = class _ChannelDriver {
10083
11373
  });
10084
11374
  return sessionId;
10085
11375
  }
11376
+ recordSessionBinding(conversationId, sessionId) {
11377
+ if (!this.recordBinding) return;
11378
+ try {
11379
+ this.recordBinding(conversationId, sessionId);
11380
+ } catch (error2) {
11381
+ this.log({
11382
+ level: "warn",
11383
+ message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
11384
+ conversation_id: conversationId
11385
+ });
11386
+ }
11387
+ }
10086
11388
  /**
10087
11389
  * Lazily resolve (and cache) opencode's root directory via the selected client's
10088
11390
  * location lookup.
@@ -12183,12 +13485,12 @@ var ChannelDriver = class _ChannelDriver {
12183
13485
  conversation_id: row.conversation_id,
12184
13486
  message_id: row.id
12185
13487
  });
12186
- const errorMessage3 = this.untrackableAck.get(row.id);
13488
+ const errorMessage4 = this.untrackableAck.get(row.id);
12187
13489
  const reportError = await this.reportUntrackableAck(
12188
13490
  row.conversation_id,
12189
13491
  row.id,
12190
13492
  this.untrackableAckSessionIds.get(row.id) ?? null,
12191
- errorMessage3
13493
+ errorMessage4
12192
13494
  );
12193
13495
  if (reportError !== null) {
12194
13496
  this.log({
@@ -12473,7 +13775,13 @@ var ChannelDriver = class _ChannelDriver {
12473
13775
  let ocId;
12474
13776
  let preDispatchUserIds = null;
12475
13777
  try {
12476
- ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
13778
+ ocId = this.isV2 ? await sendV2Prompt(
13779
+ this.openCodeClient,
13780
+ sessionId,
13781
+ row.content,
13782
+ options,
13783
+ () => this.logInvalidV2ModelSelection(row.conversation_id, row.id)
13784
+ ) : await this.dispatchLocked(
12477
13785
  sessionId,
12478
13786
  () => sendPromptAsync(
12479
13787
  this.port,
@@ -12501,14 +13809,14 @@ var ChannelDriver = class _ChannelDriver {
12501
13809
  this.awaitingReadopt.delete(row.id);
12502
13810
  if (err instanceof ChannelAuthError) throw err;
12503
13811
  if (this.isV2) {
12504
- const errorMessage3 = err instanceof Error ? err.message : String(err);
13812
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
12505
13813
  const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
12506
13814
  if (!invalidPromptAck) {
12507
13815
  const exists = await this.sessionExists(sessionId);
12508
13816
  if (exists === false) {
12509
13817
  this.log({
12510
13818
  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}`,
13819
+ 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
13820
  conversation_id: row.conversation_id,
12513
13821
  message_id: row.id
12514
13822
  });
@@ -12518,7 +13826,7 @@ var ChannelDriver = class _ChannelDriver {
12518
13826
  if (exists === null) {
12519
13827
  this.log({
12520
13828
  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}`,
13829
+ 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
13830
  conversation_id: row.conversation_id,
12523
13831
  message_id: row.id
12524
13832
  });
@@ -12526,12 +13834,16 @@ var ChannelDriver = class _ChannelDriver {
12526
13834
  return;
12527
13835
  }
12528
13836
  } else {
12529
- this.untrackableAck.set(row.id, errorMessage3);
13837
+ this.untrackableAck.set(row.id, errorMessage4);
12530
13838
  this.untrackableAckSessionIds.set(row.id, sessionId);
12531
13839
  }
13840
+ if (err instanceof OpenCodeV2ModelSwitchError) {
13841
+ await this.failV2ModelSwitch(readoptConv, readoptMessage, sessionId, err);
13842
+ return;
13843
+ }
12532
13844
  this.log({
12533
13845
  level: "error",
12534
- message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
13846
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
12535
13847
  conversation_id: row.conversation_id,
12536
13848
  message_id: row.id
12537
13849
  });
@@ -12540,7 +13852,7 @@ var ChannelDriver = class _ChannelDriver {
12540
13852
  row.conversation_id,
12541
13853
  row.id,
12542
13854
  sessionId,
12543
- errorMessage3
13855
+ errorMessage4
12544
13856
  );
12545
13857
  if (markError !== null) {
12546
13858
  this.log({
@@ -12551,7 +13863,7 @@ var ChannelDriver = class _ChannelDriver {
12551
13863
  });
12552
13864
  }
12553
13865
  } else {
12554
- await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
13866
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
12555
13867
  (markErr) => {
12556
13868
  this.log({
12557
13869
  level: "warn",
@@ -13535,9 +14847,9 @@ var ChannelDriver = class _ChannelDriver {
13535
14847
  * trackable. Callers use the returned error to keep their path-specific logging
13536
14848
  * while the same report payload can be retried on a later drain.
13537
14849
  */
13538
- async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage3) {
14850
+ async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
13539
14851
  try {
13540
- await this.markFailed(conversationId, messageId, sessionId, errorMessage3);
14852
+ await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
13541
14853
  return null;
13542
14854
  } catch (err) {
13543
14855
  this.signalUntrackableAck(conversationId, messageId);
@@ -13606,6 +14918,38 @@ var ChannelDriver = class _ChannelDriver {
13606
14918
  }
13607
14919
  this.clearSubagentInvocationCaches(messageId);
13608
14920
  }
14921
+ async failV2ModelSwitch(conv, message, sessionId, error2) {
14922
+ const { model } = splitModelVariant(message.opencode_model ?? void 0);
14923
+ const separator = model?.indexOf("/") ?? -1;
14924
+ const classification = error2.statusCode === 404 && model && separator > 0 && separator < model.length - 1 ? {
14925
+ kind: "model_auth",
14926
+ providerId: model.slice(0, separator),
14927
+ modelId: model.slice(separator + 1),
14928
+ reason: "missing"
14929
+ } : null;
14930
+ this.log({
14931
+ level: "error",
14932
+ message: `OpenCode could not apply the selected model for message ${message.id.slice(0, 8)}: ${error2.message}`,
14933
+ conversation_id: conv.id,
14934
+ message_id: message.id
14935
+ });
14936
+ try {
14937
+ await this.markFailed(conv.id, message.id, sessionId, error2.message, void 0, {
14938
+ classification,
14939
+ errorShape: { name: null, statusCode: error2.statusCode }
14940
+ });
14941
+ } catch (markErr) {
14942
+ this.log({
14943
+ level: "warn",
14944
+ 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)}`,
14945
+ conversation_id: conv.id,
14946
+ message_id: message.id
14947
+ });
14948
+ this.signalDispatchNotStarted(conv, message, "failure_unreported");
14949
+ return;
14950
+ }
14951
+ if (classification) await this.repairProviderRegistry(conv.id, message.id);
14952
+ }
13609
14953
  /**
13610
14954
  * Classify an errored turn and extract its bounded error shape for `markFailed`.
13611
14955
  *
@@ -14049,7 +15393,7 @@ import ora3 from "ora";
14049
15393
  import { select as select3 } from "@inquirer/prompts";
14050
15394
  function unknownPasswordError2(port) {
14051
15395
  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.`
15396
+ `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
15397
  );
14054
15398
  }
14055
15399
  var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
@@ -14067,9 +15411,9 @@ async function ensureOpenCode2Running(ctx) {
14067
15411
  password: null
14068
15412
  };
14069
15413
  }
14070
- if (!isOpenCode2Installed()) {
15414
+ if (!ctx.command && !isOpenCode2Installed()) {
14071
15415
  throw new Error(
14072
- `OpenCode V2 (opencode2) is not installed. Install it with: npm install -g ${OPENCODE2_PACKAGE_SPEC}`
15416
+ "OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
14073
15417
  );
14074
15418
  }
14075
15419
  let port = ctx.port;
@@ -14097,7 +15441,8 @@ Port ${port} is already in use.`));
14097
15441
  if (!ctx.interactive) {
14098
15442
  ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
14099
15443
  const { child: proc, password } = await startOpenCode2(port, {
14100
- inheritStdio: ctx.inheritStdio
15444
+ inheritStdio: ctx.inheritStdio,
15445
+ command: ctx.command
14101
15446
  });
14102
15447
  const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
14103
15448
  if (!health.healthy) {
@@ -14118,13 +15463,14 @@ Port ${port} is already in use.`));
14118
15463
  password
14119
15464
  };
14120
15465
  }
15466
+ const command = ctx.command ?? "opencode2";
14121
15467
  const action = await select3({
14122
15468
  message: "OpenCode V2 is not running. What would you like to do?",
14123
15469
  choices: [
14124
15470
  {
14125
15471
  name: "Start OpenCode V2 for me",
14126
15472
  value: "start",
14127
- description: `Run 'opencode2 serve --port ${port}'`
15473
+ description: `Run '${command} serve --port ${port}'`
14128
15474
  },
14129
15475
  {
14130
15476
  name: "Show me the command",
@@ -14142,14 +15488,15 @@ Port ${port} is already in use.`));
14142
15488
  blank();
14143
15489
  console.log(chalk6.bold("Run this command in another terminal:"));
14144
15490
  blank();
14145
- console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
15491
+ console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
14146
15492
  blank();
14147
15493
  throw new Error("Please start OpenCode V2 manually");
14148
15494
  }
14149
15495
  if (action === "start") {
14150
15496
  const spinner = ora3("Starting OpenCode V2...").start();
14151
15497
  const { child: proc, password } = await startOpenCode2(port, {
14152
- inheritStdio: ctx.inheritStdio
15498
+ inheritStdio: ctx.inheritStdio,
15499
+ command: ctx.command
14153
15500
  });
14154
15501
  const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
14155
15502
  if (!health.healthy) {
@@ -14175,7 +15522,7 @@ Port ${port} is already in use.`));
14175
15522
  }
14176
15523
 
14177
15524
  // src/lib/runner-credentials.ts
14178
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync5 } from "node:fs";
15525
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
14179
15526
  import { spawn as spawn5 } from "node:child_process";
14180
15527
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
14181
15528
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
@@ -14418,8 +15765,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
14418
15765
  }
14419
15766
  try {
14420
15767
  env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
14421
- writeFileSync5(GIT_CONFIG_GLOBAL, "");
14422
- writeFileSync5(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
15768
+ writeFileSync7(GIT_CONFIG_GLOBAL, "");
15769
+ writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
14423
15770
  chmodSync2(GIT_CREDENTIAL_HELPER, 448);
14424
15771
  const config = [
14425
15772
  ["user.name", env.GIT_USER_NAME ?? "evident-bot"],
@@ -14452,7 +15799,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
14452
15799
  // src/lib/opencode/config-overlay.ts
14453
15800
  import { execFileSync as execFileSync2 } from "node:child_process";
14454
15801
  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";
15802
+ import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
14456
15803
  function parseConfigContent(content) {
14457
15804
  if (!content?.trim()) return {};
14458
15805
  const parsed = JSON.parse(content);
@@ -14482,7 +15829,7 @@ function applyRunnerOpenCodeConfig({
14482
15829
  return;
14483
15830
  }
14484
15831
  const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
14485
- const target = isFile(join12(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
15832
+ const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
14486
15833
  if (!isFile(source)) {
14487
15834
  log3(
14488
15835
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -14490,7 +15837,7 @@ function applyRunnerOpenCodeConfig({
14490
15837
  );
14491
15838
  return;
14492
15839
  }
14493
- copyFileSync(source, join12(cwd, target));
15840
+ copyFileSync(source, join14(cwd, target));
14494
15841
  try {
14495
15842
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
14496
15843
  stdio: "ignore"
@@ -14499,13 +15846,13 @@ function applyRunnerOpenCodeConfig({
14499
15846
  const detail = error2 instanceof Error ? error2.message : String(error2);
14500
15847
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
14501
15848
  }
14502
- log3(`Applied runner OpenCode config ${source} to ${join12(cwd, target)}`);
15849
+ log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
14503
15850
  }
14504
15851
 
14505
15852
  // src/lib/opencode/runtime-provenance.ts
14506
- import { readFileSync as readFileSync8, readdirSync } from "node:fs";
15853
+ import { readFileSync as readFileSync11, readdirSync } from "node:fs";
14507
15854
  import { homedir as homedir7 } from "node:os";
14508
- import { join as join13 } from "node:path";
15855
+ import { join as join15 } from "node:path";
14509
15856
  var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
14510
15857
  var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
14511
15858
  function errorKind(error2) {
@@ -14573,7 +15920,7 @@ function parseJsonc(value) {
14573
15920
  return JSON.parse(output);
14574
15921
  }
14575
15922
  function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14576
- return join13(env.XDG_CACHE_HOME ?? join13(homeDir, ".cache"), "opencode");
15923
+ return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
14577
15924
  }
14578
15925
  function readConfiguredPluginSpecs({
14579
15926
  cwd = process.cwd(),
@@ -14582,10 +15929,10 @@ function readConfiguredPluginSpecs({
14582
15929
  let config;
14583
15930
  try {
14584
15931
  try {
14585
- config = parseJsonc(readFileSync8(join13(cwd, "opencode.jsonc"), "utf8"));
15932
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
14586
15933
  } catch (error2) {
14587
15934
  if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14588
- config = parseJsonc(readFileSync8(join13(cwd, "opencode.json"), "utf8"));
15935
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
14589
15936
  }
14590
15937
  } catch (error2) {
14591
15938
  log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
@@ -14613,7 +15960,7 @@ function resolveInstalledPluginVersions({
14613
15960
  homeDir = homedir7(),
14614
15961
  log: log3
14615
15962
  }) {
14616
- const packagesRoot = join13(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
15963
+ const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14617
15964
  let directories;
14618
15965
  try {
14619
15966
  directories = readdirSync(packagesRoot, { withFileTypes: true });
@@ -14634,8 +15981,8 @@ function resolveInstalledPluginVersions({
14634
15981
  let resolvedVersion = null;
14635
15982
  try {
14636
15983
  const packageJson = JSON.parse(
14637
- readFileSync8(
14638
- join13(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
15984
+ readFileSync11(
15985
+ join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14639
15986
  "utf8"
14640
15987
  )
14641
15988
  );
@@ -14656,7 +16003,7 @@ function resolveInstalledPluginVersions({
14656
16003
  }
14657
16004
 
14658
16005
  // src/lib/credential-sync.ts
14659
- import { renameSync, writeFileSync as writeFileSync6 } from "node:fs";
16006
+ import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
14660
16007
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
14661
16008
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
14662
16009
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -14666,7 +16013,7 @@ var MAX_FLUSH_PASSES = 2;
14666
16013
  function outcomesWith(outcome) {
14667
16014
  return { claude: outcome, opencode: outcome };
14668
16015
  }
14669
- function errorMessage2(error2) {
16016
+ function errorMessage3(error2) {
14670
16017
  return error2 instanceof Error ? error2.message : String(error2);
14671
16018
  }
14672
16019
  function waitForSettlement(promise, timeoutMs) {
@@ -14690,10 +16037,10 @@ function writeMarker(markerPath, outcomes, log3) {
14690
16037
  `;
14691
16038
  const temporaryPath = `${markerPath}.tmp`;
14692
16039
  try {
14693
- writeFileSync6(temporaryPath, body, { mode: 384 });
16040
+ writeFileSync8(temporaryPath, body, { mode: 384 });
14694
16041
  renameSync(temporaryPath, markerPath);
14695
16042
  } catch (error2) {
14696
- log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
16043
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
14697
16044
  }
14698
16045
  }
14699
16046
  function intervalSeconds(env, log3) {
@@ -14725,7 +16072,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
14725
16072
  },
14726
16073
  (error2) => {
14727
16074
  failed = true;
14728
- log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
16075
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
14729
16076
  }
14730
16077
  );
14731
16078
  const abortTimer = setTimeout(() => controller.abort(), remainingMs);
@@ -14785,7 +16132,7 @@ function createCredentialSync({
14785
16132
  outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
14786
16133
  } catch (error2) {
14787
16134
  outcomes[store] = "failed";
14788
- log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
16135
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
14789
16136
  }
14790
16137
  }
14791
16138
  const failed = STORES.some((store) => outcomes[store] === "failed");
@@ -14908,14 +16255,21 @@ function request2(options) {
14908
16255
  function safeFailure(operation, status2) {
14909
16256
  return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
14910
16257
  }
14911
- function logTransportFailure(options, operation, error2, authorization) {
14912
- let detail = error2 instanceof Error ? error2.message : String(error2);
16258
+ function redactAuthorizedText(message, authorization) {
16259
+ let redacted = message;
14913
16260
  const credential = authorization.split(/\s+/, 2)[1];
14914
16261
  for (const secret of [authorization, credential]) {
14915
- if (secret) detail = detail.replaceAll(secret, "[redacted]");
16262
+ if (secret) redacted = redacted.replaceAll(secret, "[redacted]");
14916
16263
  }
16264
+ return redactDiagnosticText(redacted);
16265
+ }
16266
+ function logTransportFailure(options, operation, error2, authorization) {
16267
+ const detail = redactAuthorizedText(
16268
+ error2 instanceof Error ? error2.message : String(error2),
16269
+ authorization
16270
+ );
14917
16271
  options.log(
14918
- `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${redactDiagnosticText(detail)}`,
16272
+ `MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${detail}`,
14919
16273
  "debug"
14920
16274
  );
14921
16275
  }
@@ -14935,6 +16289,18 @@ function readSetup(value) {
14935
16289
  }
14936
16290
  return setup;
14937
16291
  }
16292
+ function readModelDescriptors(value) {
16293
+ if (!Array.isArray(value)) return null;
16294
+ return value.every(
16295
+ (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
16296
+ ) ? value : null;
16297
+ }
16298
+ function logDescriptorRefreshFallback(options, branch, generation) {
16299
+ options.log(
16300
+ `MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
16301
+ "warn"
16302
+ );
16303
+ }
14938
16304
  function clearLocalSetup(options) {
14939
16305
  const env = options.env ?? process.env;
14940
16306
  const store = options.store ?? defaultStore;
@@ -14966,7 +16332,14 @@ async function discoverManagedInferenceSetup(options) {
14966
16332
  return { kind: "none" };
14967
16333
  }
14968
16334
  if (!response.ok) {
14969
- return { kind: "failed", reason: safeFailure("Managed setup discovery", response.status) };
16335
+ const body2 = await responseJson(response);
16336
+ const serverMessage = typeof body2?.error === "string" && body2.error.trim() ? redactAuthorizedText(body2.error.trim(), headers.Authorization).slice(0, 500) : void 0;
16337
+ const failure = serverMessage ? `Managed setup discovery returned HTTP ${response.status}: ${serverMessage}` : safeFailure("Managed setup discovery", response.status);
16338
+ const runnerKeyGuidance = response.status === 401 || response.status === 403 ? "Authenticate as this runner with its runner key (EVIDENT_RUNNER_KEY)." : void 0;
16339
+ return {
16340
+ kind: "failed",
16341
+ reason: runnerKeyGuidance ? `${failure}. ${runnerKeyGuidance}` : failure
16342
+ };
14970
16343
  }
14971
16344
  const body = await responseJson(response);
14972
16345
  const setup = readSetup(body?.setup);
@@ -15066,7 +16439,27 @@ async function reaffirmStored(options, credential) {
15066
16439
  if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
15067
16440
  return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
15068
16441
  }
15069
- const failure = applyCredential(options, credential);
16442
+ const descriptorsValue = body?.model_descriptors;
16443
+ const refreshedDescriptors = readModelDescriptors(descriptorsValue);
16444
+ let credentialToApply = credential;
16445
+ if (descriptorsValue === void 0 || descriptorsValue === null) {
16446
+ logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
16447
+ } else if (refreshedDescriptors === null) {
16448
+ logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
16449
+ } else if (refreshedDescriptors.length === 0) {
16450
+ logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
16451
+ } else {
16452
+ const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
16453
+ inferenceBaseUrl: credential.inferenceBaseUrl,
16454
+ modelDescriptors: refreshedDescriptors
16455
+ });
16456
+ if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
16457
+ logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
16458
+ } else {
16459
+ credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
16460
+ }
16461
+ }
16462
+ const failure = applyCredential(options, credentialToApply);
15070
16463
  return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
15071
16464
  }
15072
16465
  async function prepareManagedInferenceSetup(options, discovery, health) {
@@ -15100,10 +16493,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
15100
16493
  const setup = readSetup(body?.setup);
15101
16494
  const token = body?.inference_token;
15102
16495
  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
- )) {
16496
+ const modelDescriptors = readModelDescriptors(body?.model_descriptors);
16497
+ if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
15107
16498
  return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
15108
16499
  }
15109
16500
  const credential = {
@@ -15172,33 +16563,49 @@ function resolveLogLevel(options) {
15172
16563
  }
15173
16564
  return "info";
15174
16565
  }
15175
- function resolveFileSyncDirectories(raw, homeDir) {
15176
- const directories = [];
16566
+ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
16567
+ const trimmed = entry.trim();
16568
+ if (trimmed === "") {
16569
+ throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
16570
+ }
16571
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
16572
+ if (!isAbsolute4(expanded)) {
16573
+ throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
16574
+ }
16575
+ const normalized = resolvePath2(expanded);
16576
+ if (parse(normalized).root === normalized) {
16577
+ throw new Error(
16578
+ `${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
16579
+ );
16580
+ }
16581
+ return normalized;
16582
+ }
16583
+ function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
16584
+ const paths = [];
15177
16585
  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
- }
16586
+ const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
16587
+ if (!paths.includes(normalized)) paths.push(normalized);
15195
16588
  }
15196
- if (directories.length > MAX_FILE_SYNC_DIRECTORIES) {
16589
+ if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
15197
16590
  throw new Error(
15198
- `--enable-file-sync-to accepts at most ${MAX_FILE_SYNC_DIRECTORIES} directories; got ${directories.length}`
16591
+ `${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
15199
16592
  );
15200
16593
  }
15201
- return directories;
16594
+ return paths;
16595
+ }
16596
+ function resolveFileSyncDirectories(raw, homeDir) {
16597
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
16598
+ noun: "directory",
16599
+ plural: "directories",
16600
+ rootExample: "~/.claude"
16601
+ });
16602
+ }
16603
+ function resolveFileSyncExactFiles(raw, homeDir) {
16604
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
16605
+ noun: "file",
16606
+ plural: "files",
16607
+ rootExample: "~/.local/share/opencode/mcp-auth.json"
16608
+ });
15202
16609
  }
15203
16610
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
15204
16611
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
@@ -15214,13 +16621,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
15214
16621
  raw = env[OPENCODE_VERSION_ENV];
15215
16622
  source = OPENCODE_VERSION_ENV;
15216
16623
  } else {
15217
- return { version: "v1", warnings: [] };
16624
+ return { version: "auto", warnings: [] };
15218
16625
  }
15219
16626
  const normalized = raw.trim().toLowerCase();
15220
- if (normalized !== "v1" && normalized !== "v2") {
16627
+ if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
15221
16628
  return {
15222
- version: "v1",
15223
- warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
16629
+ version: "auto",
16630
+ warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
15224
16631
  };
15225
16632
  }
15226
16633
  return { version: normalized, warnings: [] };
@@ -15730,8 +17137,8 @@ async function driveChannels(state, driver) {
15730
17137
  state.running = false;
15731
17138
  break;
15732
17139
  }
15733
- const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
15734
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
17140
+ const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
17141
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
15735
17142
  if (state.interactive) displayStatus(state);
15736
17143
  if (driver.hasInFlightWatchers()) {
15737
17144
  consecutiveDrainFailures = 0;
@@ -15821,6 +17228,7 @@ async function driveChannels(state, driver) {
15821
17228
  }
15822
17229
  }
15823
17230
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
17231
+ var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
15824
17232
  var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
15825
17233
  var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
15826
17234
  var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
@@ -15835,7 +17243,7 @@ function shouldWarnForReclaimSkip(reason) {
15835
17243
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
15836
17244
  }
15837
17245
  function sessionDbPath() {
15838
- return join14(homedir8(), ".local", "share", "opencode", "opencode.db");
17246
+ return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
15839
17247
  }
15840
17248
  function logSessionCleanupActivity(state, phase, entry) {
15841
17249
  logActivity(state, {
@@ -15844,6 +17252,93 @@ function logSessionCleanupActivity(state, phase, entry) {
15844
17252
  metadata: { ...entry.metadata, phase }
15845
17253
  });
15846
17254
  }
17255
+ function logSessionArchiveActivity(state, phase, entry) {
17256
+ logActivity(state, {
17257
+ ...entry,
17258
+ source: "cli.session-archive",
17259
+ metadata: { ...entry.metadata, phase }
17260
+ });
17261
+ }
17262
+ function logSessionArchiveSummary(state, phase, summary) {
17263
+ const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
17264
+ const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
17265
+ const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
17266
+ const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed || !summary.reconciliationComplete;
17267
+ logSessionArchiveActivity(state, phase, {
17268
+ type: "info",
17269
+ level: warning2 ? "warn" : "info",
17270
+ message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}, conflicts ${summary.conflicts}, reconciliation incomplete ${summary.reconciliationIncomplete}; bindings ledger ${summary.bindingSourceCounts.ledger}, watermark ${summary.bindingSourceCounts.watermark}, current ${summary.bindingSourceCounts.current}, invocation ${summary.bindingSourceCounts.invocation}, history ${summary.bindingSourceCounts.history}, parent ${summary.bindingSourceCounts.parent}` + staleSince + neverWatermarked,
17271
+ metadata: {
17272
+ outcome: "archive-run-complete",
17273
+ sessions_inspected: summary.inspected,
17274
+ sessions_uploaded: summary.uploaded,
17275
+ sessions_unchanged: summary.unchanged,
17276
+ sessions_failed: summary.failed,
17277
+ uploads_attempted: summary.uploadAttempts,
17278
+ uploads_failed: summary.uploadFailures,
17279
+ sessions_skipped_budget: summary.skippedBudget,
17280
+ sessions_unmapped: summary.unmappedIds.length,
17281
+ sessions_unresolved: summary.unresolved,
17282
+ sessions_conflicted: summary.conflicts,
17283
+ sessions_reconciliation_incomplete: summary.reconciliationIncomplete,
17284
+ binding_reconciliation_complete: summary.reconciliationComplete,
17285
+ bindings_from_ledger: summary.bindingSourceCounts.ledger,
17286
+ bindings_from_watermark: summary.bindingSourceCounts.watermark,
17287
+ bindings_from_current: summary.bindingSourceCounts.current,
17288
+ bindings_from_invocation: summary.bindingSourceCounts.invocation,
17289
+ bindings_from_history: summary.bindingSourceCounts.history,
17290
+ bindings_from_parent: summary.bindingSourceCounts.parent,
17291
+ sessions_without_prior_watermark: summary.neverWatermarkedFailures,
17292
+ ...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
17293
+ }
17294
+ });
17295
+ for (const failure of summary.failures) {
17296
+ const permanentSizeFailure = failure.reason === "too-large";
17297
+ const hasPayloadDetails = failure.bytes !== void 0 && failure.byteLimit !== void 0 && failure.messageCount !== void 0 && failure.partCount !== void 0;
17298
+ const payloadDetails = hasPayloadDetails ? `; payload bytes=${failure.bytes} limit=${failure.byteLimit} messages=${failure.messageCount} parts=${failure.partCount}` : "";
17299
+ const safeMessage = !hasPayloadDetails && failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
17300
+ logSessionArchiveActivity(state, phase, {
17301
+ type: "info",
17302
+ level: "warn",
17303
+ message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + payloadDetails + (failure.exitCode !== void 0 ? `; child exit code=${failure.exitCode}` : "") + safeMessage,
17304
+ metadata: {
17305
+ outcome: failure.outcome,
17306
+ session_id: failure.sessionId,
17307
+ archived: failure.archived,
17308
+ ...failure.reason !== void 0 ? { reason: failure.reason } : {},
17309
+ ...failure.exitCode !== void 0 ? { child_exit_code: failure.exitCode } : {},
17310
+ ...hasPayloadDetails ? {
17311
+ payload_bytes: failure.bytes,
17312
+ payload_limit_bytes: failure.byteLimit,
17313
+ message_count: failure.messageCount,
17314
+ part_count: failure.partCount
17315
+ } : {}
17316
+ }
17317
+ });
17318
+ }
17319
+ if (summary.unmappedIds.length > 0) {
17320
+ logSessionArchiveActivity(state, phase, {
17321
+ type: "info",
17322
+ level: "warn",
17323
+ message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
17324
+ metadata: {
17325
+ outcome: "unmapped-sessions",
17326
+ sessions_unmapped: summary.unmappedIds.length
17327
+ }
17328
+ });
17329
+ }
17330
+ if (summary.conflictedIds.length > 0) {
17331
+ logSessionArchiveActivity(state, phase, {
17332
+ type: "info",
17333
+ level: "warn",
17334
+ message: `Session archive: ${summary.conflictedIds.length} session(s) have conflicting conversation identities: ${summary.conflictedIds.join(", ")}`,
17335
+ metadata: {
17336
+ outcome: "conflicted-session-bindings",
17337
+ sessions_conflicted: summary.conflicts
17338
+ }
17339
+ });
17340
+ }
17341
+ }
15847
17342
  async function drainSessionDeletionReports(state) {
15848
17343
  const path = sessionDeletionReportPath(homedir8(), process.env);
15849
17344
  let delivered = 0;
@@ -15941,6 +17436,7 @@ async function runSweep(state, driver, config) {
15941
17436
  let failedOtherStatus = 0;
15942
17437
  let failedThrew = 0;
15943
17438
  let skippedNewlyActive = 0;
17439
+ let skippedArchive = 0;
15944
17440
  let remaining = 0;
15945
17441
  const deletionRecords = [];
15946
17442
  let protectedNow = driver.protectedSessionIds();
@@ -15952,6 +17448,7 @@ async function runSweep(state, driver, config) {
15952
17448
  const lastActivityById = new Map(
15953
17449
  snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
15954
17450
  );
17451
+ const sessionById = new Map(sessions.map((session) => [session.id, session]));
15955
17452
  const toDelete = selectSessionsToDelete(snapshots, {
15956
17453
  maxAgeMs: config.maxAgeMs,
15957
17454
  maxCount: config.maxCount,
@@ -15974,6 +17471,42 @@ async function runSweep(state, driver, config) {
15974
17471
  });
15975
17472
  continue;
15976
17473
  }
17474
+ if (state.sessionArchiveConfig.enabled) {
17475
+ const candidate = sessionById.get(id);
17476
+ const archiveConfig = state.sessionArchiveConfig;
17477
+ const archiveBudgetMs = Math.min(
17478
+ archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
17479
+ MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
17480
+ );
17481
+ const archived = candidate ? await runSessionArchiveTask(
17482
+ state,
17483
+ "sweep",
17484
+ archiveBudgetMs,
17485
+ (signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
17486
+ ) : null;
17487
+ if (!archived?.archivedSessionIds.includes(id)) {
17488
+ skippedArchive++;
17489
+ logSessionArchiveActivity(state, "sweep", {
17490
+ type: "info",
17491
+ level: "warn",
17492
+ message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
17493
+ metadata: {
17494
+ outcome: "deletion-held-for-archive",
17495
+ session_id: id
17496
+ }
17497
+ });
17498
+ continue;
17499
+ }
17500
+ protectedNow = driver.protectedSessionIds();
17501
+ if (protectedNow.has(id)) {
17502
+ skippedNewlyActive++;
17503
+ logActivity(state, {
17504
+ type: "info",
17505
+ message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
17506
+ });
17507
+ continue;
17508
+ }
17509
+ }
15977
17510
  const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
15978
17511
  if (outcome.ok) {
15979
17512
  deleted++;
@@ -16037,10 +17570,11 @@ async function runSweep(state, driver, config) {
16037
17570
  }
16038
17571
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
16039
17572
  const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
17573
+ const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
16040
17574
  const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
16041
17575
  logActivity(state, {
16042
17576
  type: "info",
16043
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
17577
+ message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
16044
17578
  });
16045
17579
  }
16046
17580
  if (deletionRecords.length > 0) {
@@ -16078,6 +17612,7 @@ async function runSweep(state, driver, config) {
16078
17612
  sessions_failed_other: failedOtherStatus,
16079
17613
  sessions_failed_threw: failedThrew,
16080
17614
  sessions_skipped_active: skippedNewlyActive,
17615
+ sessions_archive_held: skippedArchive,
16081
17616
  sessions_remaining: remaining,
16082
17617
  ...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
16083
17618
  };
@@ -16209,6 +17744,213 @@ function scheduleSessionCleanup(state, driver, options) {
16209
17744
  );
16210
17745
  state.sessionCleanupTimers.push(interval, firstSweep);
16211
17746
  }
17747
+ function runSessionArchiveTask(state, phase, budgetMs, work) {
17748
+ if (state.sessionArchiveInFlight) {
17749
+ logSessionArchiveActivity(state, phase, {
17750
+ type: "info",
17751
+ level: "debug",
17752
+ message: "Session archive skipped \u2014 previous archive run still in progress",
17753
+ metadata: { outcome: "skipped-in-flight" }
17754
+ });
17755
+ return Promise.resolve(null);
17756
+ }
17757
+ const controller = new AbortController();
17758
+ state.sessionArchiveAbortController = controller;
17759
+ const timeout = setTimeout(() => {
17760
+ controller.abort();
17761
+ logSessionArchiveActivity(state, phase, {
17762
+ type: "info",
17763
+ level: "warn",
17764
+ message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
17765
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17766
+ });
17767
+ }, budgetMs);
17768
+ const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
17769
+ logSessionArchiveActivity(state, phase, {
17770
+ type: "info",
17771
+ level: "warn",
17772
+ message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
17773
+ metadata: { outcome: "archive-run-failed" }
17774
+ });
17775
+ return null;
17776
+ }).finally(() => {
17777
+ clearTimeout(timeout);
17778
+ if (state.sessionArchiveInFlight === task) {
17779
+ state.sessionArchiveInFlight = null;
17780
+ state.sessionArchiveAbortController = null;
17781
+ }
17782
+ });
17783
+ state.sessionArchiveInFlight = task;
17784
+ return task;
17785
+ }
17786
+ async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
17787
+ const config = state.sessionArchiveConfig;
17788
+ const client = state.opencodeClient;
17789
+ const watermarkPath = state.sessionArchiveWatermarkPath;
17790
+ if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
17791
+ logSessionArchiveActivity(state, phase, {
17792
+ type: "info",
17793
+ level: "warn",
17794
+ message: "Session archive skipped because runner identity or archive configuration is unavailable",
17795
+ metadata: { outcome: "archive-configuration-unavailable" }
17796
+ });
17797
+ return null;
17798
+ }
17799
+ const bindings = readSessionBindings(
17800
+ sessionBindingLedgerPath(homedir8(), process.env),
17801
+ (message) => logSessionArchiveActivity(state, phase, {
17802
+ type: "info",
17803
+ level: "warn",
17804
+ message: redactDiagnosticText(message),
17805
+ metadata: { outcome: "binding-ledger-read-failed" }
17806
+ })
17807
+ );
17808
+ const reconciliation = await fetchSessionBindingProjection({
17809
+ apiUrl: getApiUrlConfig(),
17810
+ runnerId: state.agentId,
17811
+ authHeader: state.authHeader,
17812
+ sessions,
17813
+ signal
17814
+ });
17815
+ if (!reconciliation.complete) {
17816
+ const failure = reconciliation.failures[0];
17817
+ const detail = failure.kind === "http" ? `HTTP ${failure.status} for chunk ${failure.chunk}` : failure.kind === "network" ? `network failure for chunk ${failure.chunk}: ${redactDiagnosticText(failure.message ?? "unknown error").slice(0, 200)}` : `invalid response for chunk ${failure.chunk}`;
17818
+ logSessionArchiveActivity(state, phase, {
17819
+ type: "info",
17820
+ level: "warn",
17821
+ message: `Session archive: binding reconciliation incomplete for runner ${state.agentId}; ${reconciliation.failures.length} chunk(s) failed (${detail}), unresolved sessions will not replace existing archives`,
17822
+ metadata: {
17823
+ outcome: "binding-reconciliation-incomplete",
17824
+ runner_id: state.agentId,
17825
+ failed_chunks: reconciliation.failures.length,
17826
+ first_failed_chunk: failure.chunk,
17827
+ first_failure_kind: failure.kind,
17828
+ ...failure.status !== void 0 ? { http_status: failure.status } : {}
17829
+ }
17830
+ });
17831
+ }
17832
+ const summary = await runSessionArchive({
17833
+ sessions,
17834
+ deps: {
17835
+ client,
17836
+ runnerId: state.agentId,
17837
+ keyPrefix: config.keyPrefix,
17838
+ bindings,
17839
+ projection: reconciliation.projection,
17840
+ reconciliationComplete: reconciliation.complete,
17841
+ watermarks: state.sessionArchiveWatermarks,
17842
+ watermarkPath,
17843
+ signal,
17844
+ upload: uploadSessionArchive,
17845
+ exportMessages: exportSessionMessages,
17846
+ recordWatermark: recordUpload
17847
+ },
17848
+ budgetMs
17849
+ });
17850
+ logSessionArchiveSummary(state, phase, summary);
17851
+ return summary;
17852
+ }
17853
+ async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
17854
+ const startedAt = Date.now();
17855
+ const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
17856
+ if (enumeration.sessions === null) {
17857
+ const interrupted = signal.aborted;
17858
+ logSessionArchiveActivity(state, phase, {
17859
+ type: "info",
17860
+ level: "warn",
17861
+ 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`,
17862
+ metadata: {
17863
+ outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
17864
+ ...!interrupted ? { reason: enumeration.reason } : {}
17865
+ }
17866
+ });
17867
+ return null;
17868
+ }
17869
+ return runSessionArchiveBatch(
17870
+ state,
17871
+ phase,
17872
+ enumeration.sessions,
17873
+ Math.max(0, budgetMs - (Date.now() - startedAt)),
17874
+ signal
17875
+ );
17876
+ }
17877
+ async function runSessionArchiveShutdownFlush(state, budgetMs) {
17878
+ const deadline = Date.now() + budgetMs;
17879
+ const previousRun = state.sessionArchiveInFlight;
17880
+ if (previousRun) {
17881
+ state.sessionArchiveAbortController?.abort(
17882
+ new Error("graceful shutdown archive flush requested")
17883
+ );
17884
+ let timeout;
17885
+ const settled = await Promise.race([
17886
+ previousRun.then(() => true),
17887
+ new Promise((resolve5) => {
17888
+ timeout = setTimeout(() => resolve5(false), budgetMs);
17889
+ })
17890
+ ]);
17891
+ if (timeout) clearTimeout(timeout);
17892
+ if (!settled) {
17893
+ state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
17894
+ logSessionArchiveActivity(state, "shutdown", {
17895
+ type: "info",
17896
+ level: "warn",
17897
+ message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
17898
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17899
+ });
17900
+ return null;
17901
+ }
17902
+ }
17903
+ const remainingMs = Math.floor(deadline - Date.now());
17904
+ if (remainingMs <= 0) {
17905
+ logSessionArchiveActivity(state, "shutdown", {
17906
+ type: "info",
17907
+ level: "warn",
17908
+ message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
17909
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17910
+ });
17911
+ return null;
17912
+ }
17913
+ return runSessionArchiveTask(
17914
+ state,
17915
+ "shutdown",
17916
+ remainingMs,
17917
+ (signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
17918
+ );
17919
+ }
17920
+ function scheduleSessionArchive(state) {
17921
+ const config = state.sessionArchiveConfig;
17922
+ if (!config.enabled) return;
17923
+ state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
17924
+ state.sessionArchiveWatermarks = readWatermarks(
17925
+ state.sessionArchiveWatermarkPath,
17926
+ (message) => logSessionArchiveActivity(state, "startup", {
17927
+ type: "info",
17928
+ level: "warn",
17929
+ message: redactDiagnosticText(message),
17930
+ metadata: { outcome: "watermark-read-failed" }
17931
+ })
17932
+ );
17933
+ logSessionArchiveActivity(state, "startup", {
17934
+ type: "info",
17935
+ message: `Session archive enabled (interval=${config.intervalMs}ms)`,
17936
+ metadata: { outcome: "armed", interval_ms: config.intervalMs }
17937
+ });
17938
+ const cycleBudgetMs = Math.min(
17939
+ config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
17940
+ MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
17941
+ );
17942
+ const run2 = (phase = "scheduled") => {
17943
+ void runSessionArchiveTask(
17944
+ state,
17945
+ phase,
17946
+ cycleBudgetMs,
17947
+ (signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
17948
+ );
17949
+ };
17950
+ const interval = setInterval(run2, config.intervalMs);
17951
+ const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
17952
+ state.sessionArchiveTimers.push(interval, firstRun);
17953
+ }
16212
17954
  function scheduleUsageReporting(state, params) {
16213
17955
  const { mode, warnings } = params.resolved;
16214
17956
  for (const warning2 of warnings) {
@@ -16483,6 +18225,12 @@ async function cleanup(state, opts = {}) {
16483
18225
  clearTimeout(timer);
16484
18226
  }
16485
18227
  state.sessionCleanupTimers = [];
18228
+ for (const timer of state.sessionArchiveTimers) {
18229
+ clearInterval(timer);
18230
+ clearTimeout(timer);
18231
+ }
18232
+ state.sessionArchiveTimers = [];
18233
+ state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
16486
18234
  state.stopOpenCodeLogTail?.();
16487
18235
  state.stopOpenCodeLogTail = null;
16488
18236
  if (state.claudeUsageTimer) {
@@ -16550,6 +18298,15 @@ async function cleanup(state, opts = {}) {
16550
18298
  await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
16551
18299
  state.connection = null;
16552
18300
  }
18301
+ if (opts.graceful && state.sessionArchiveConfig.enabled) {
18302
+ const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
18303
+ await timeShutdownPhase(
18304
+ state,
18305
+ durations,
18306
+ "session_archive_flush",
18307
+ () => runSessionArchiveShutdownFlush(state, budgetMs)
18308
+ );
18309
+ }
16553
18310
  if (state.opencodeProcess) {
16554
18311
  const opencodeProcess = state.opencodeProcess;
16555
18312
  const result = await timeShutdownPhase(
@@ -16621,9 +18378,11 @@ async function run(options) {
16621
18378
  const interactive = isInteractive(options.json);
16622
18379
  let logLevel;
16623
18380
  let fileSyncDirectories;
18381
+ let fileSyncExactFiles;
16624
18382
  try {
16625
18383
  logLevel = resolveLogLevel(options);
16626
18384
  fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
18385
+ fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
16627
18386
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
16628
18387
  throw new Error(
16629
18388
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -16671,6 +18430,18 @@ async function run(options) {
16671
18430
  messageCount: 0,
16672
18431
  lastProxiedActivityAt: null,
16673
18432
  sessionCleanupTimers: [],
18433
+ sessionArchiveConfig: resolveSessionArchiveConfig(
18434
+ {
18435
+ enabled: options.sessionArchive,
18436
+ interval: options.sessionArchiveInterval
18437
+ },
18438
+ process.env
18439
+ ),
18440
+ sessionArchiveTimers: [],
18441
+ sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
18442
+ sessionArchiveWatermarkPath: null,
18443
+ sessionArchiveInFlight: null,
18444
+ sessionArchiveAbortController: null,
16674
18445
  sessionDeleteFailures: /* @__PURE__ */ new Map(),
16675
18446
  claudeUsageTimer: null,
16676
18447
  claudeUsageRearm: null,
@@ -16684,6 +18455,14 @@ async function run(options) {
16684
18455
  };
16685
18456
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
16686
18457
  setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
18458
+ for (const warning2 of state.sessionArchiveConfig.warnings) {
18459
+ logSessionArchiveActivity(state, "startup", {
18460
+ type: "info",
18461
+ level: "warn",
18462
+ message: `Session archive: ${warning2}`,
18463
+ metadata: { outcome: "configuration-warning" }
18464
+ });
18465
+ }
16687
18466
  if (options.credentialSyncMarker) {
16688
18467
  state.credentialSync = createCredentialSync({
16689
18468
  markerPath: options.credentialSyncMarker,
@@ -16699,7 +18478,11 @@ async function run(options) {
16699
18478
  }
16700
18479
  if (fileSyncDirectories.length > 0) {
16701
18480
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
16702
- } else {
18481
+ }
18482
+ if (fileSyncExactFiles.length > 0) {
18483
+ log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
18484
+ }
18485
+ if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
16703
18486
  log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
16704
18487
  }
16705
18488
  if (!options.runner && options.agent) {
@@ -17001,13 +18784,75 @@ async function run(options) {
17001
18784
  for (const warning2 of opencodeStartTimeoutWarnings) {
17002
18785
  logActivity(state, { type: "info", level: "warn", message: warning2 });
17003
18786
  }
17004
- const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
17005
- options,
17006
- process.env
17007
- );
18787
+ const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
17008
18788
  for (const warning2 of opencodeVersionWarnings) {
17009
18789
  logActivity(state, { type: "info", level: "warn", message: warning2 });
17010
18790
  }
18791
+ let opencodeMajor;
18792
+ let opencodeV2Command;
18793
+ let opencodeDetectionSource;
18794
+ let opencodeDetectionLevel = "info";
18795
+ let opencodeDetectionMessage;
18796
+ if (opencodeVersionSelection === "v1") {
18797
+ opencodeMajor = "v1";
18798
+ opencodeDetectionSource = "explicit";
18799
+ opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
18800
+ } else if (opencodeVersionSelection === "v2") {
18801
+ const installed = resolveInstalledOpenCode();
18802
+ opencodeMajor = "v2";
18803
+ opencodeDetectionSource = "explicit";
18804
+ if (installed.kind === "installed" && installed.major === "v2") {
18805
+ opencodeV2Command = installed.command;
18806
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
18807
+ } else if (installed.kind === "unknown") {
18808
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
18809
+ } else if (installed.kind === "installed") {
18810
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
18811
+ } else {
18812
+ opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
18813
+ }
18814
+ } else {
18815
+ const running = await classifyRunningOpenCode(state.port);
18816
+ if (running.kind === "v1") {
18817
+ opencodeMajor = "v1";
18818
+ opencodeDetectionSource = "running-server";
18819
+ opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
18820
+ } else if (running.kind === "v2") {
18821
+ opencodeMajor = "v2";
18822
+ opencodeDetectionSource = "running-server";
18823
+ opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
18824
+ } else if (running.kind === "authenticated-unknown") {
18825
+ 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.`;
18826
+ logActivity(state, {
18827
+ type: "info",
18828
+ level: "warn",
18829
+ message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
18830
+ });
18831
+ throw new Error(message);
18832
+ } else {
18833
+ const installed = resolveInstalledOpenCode();
18834
+ opencodeDetectionLevel = "warn";
18835
+ if (installed.kind === "installed") {
18836
+ opencodeMajor = installed.major;
18837
+ opencodeDetectionSource = "installed-binary";
18838
+ if (installed.major === "v2") opencodeV2Command = installed.command;
18839
+ 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"}.`;
18840
+ } else if (installed.kind === "unknown") {
18841
+ opencodeMajor = "v1";
18842
+ opencodeDetectionSource = "fallback-v1";
18843
+ 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.`;
18844
+ } else {
18845
+ opencodeMajor = "v1";
18846
+ opencodeDetectionSource = "fallback-v1";
18847
+ 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.`;
18848
+ }
18849
+ }
18850
+ }
18851
+ logActivity(state, {
18852
+ type: "info",
18853
+ level: opencodeDetectionLevel,
18854
+ message: opencodeDetectionMessage
18855
+ });
17011
18856
  const managedSetupOptions = {
17012
18857
  agentId: state.agentId,
17013
18858
  apiUrl: getApiUrlConfig(),
@@ -17028,7 +18873,7 @@ async function run(options) {
17028
18873
  managedPreparation = managedDiscovery;
17029
18874
  managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
17030
18875
  } else if (managedDiscovery.kind === "setup") {
17031
- const managedHealth = opencodeVersion === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
18876
+ const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
17032
18877
  managedPreparation = await prepareManagedInferenceSetup(
17033
18878
  managedSetupOptions,
17034
18879
  managedDiscovery,
@@ -17053,13 +18898,14 @@ async function run(options) {
17053
18898
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
17054
18899
  const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
17055
18900
  try {
17056
- const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
18901
+ const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
17057
18902
  port: state.port,
17058
18903
  interactive: state.interactive,
17059
18904
  agentId: state.agentId,
17060
18905
  log: (message) => log2(state, message),
17061
18906
  startTimeoutMs: opencodeStartTimeoutMs,
17062
- inheritStdio: Boolean(options.opencodePidFile)
18907
+ inheritStdio: Boolean(options.opencodePidFile),
18908
+ command: opencodeV2Command
17063
18909
  }) : await ensureOpenCodeRunning({
17064
18910
  port: state.port,
17065
18911
  interactive: state.interactive,
@@ -17071,7 +18917,7 @@ async function run(options) {
17071
18917
  state.port = oc.port;
17072
18918
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
17073
18919
  state.opencodeVersion = oc.version;
17074
- state.opencodeApiVersion = opencodeVersion;
18920
+ state.opencodeApiVersion = opencodeMajor;
17075
18921
  let configuredPlugins = [];
17076
18922
  let resolvedPlugins = [];
17077
18923
  try {
@@ -17093,13 +18939,14 @@ async function run(options) {
17093
18939
  metadata: {
17094
18940
  opencode_version: state.opencodeVersion,
17095
18941
  opencode_api_version: state.opencodeApiVersion,
18942
+ opencode_detection_source: opencodeDetectionSource,
17096
18943
  configured_plugins: JSON.stringify(configuredPlugins),
17097
18944
  resolved_plugins: JSON.stringify(resolvedPlugins)
17098
18945
  },
17099
18946
  source: "cli.runtime-provenance"
17100
18947
  });
17101
18948
  let opencodePassword = null;
17102
- if (opencodeVersion === "v2" && "password" in oc) {
18949
+ if (opencodeMajor === "v2" && "password" in oc) {
17103
18950
  const value = oc.password;
17104
18951
  if (typeof value === "string" || value === null) opencodePassword = value;
17105
18952
  }
@@ -17112,7 +18959,7 @@ async function run(options) {
17112
18959
  state.opencodeClient = openCodeClient;
17113
18960
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
17114
18961
  try {
17115
- writeFileSync7(options.opencodePidFile, `${oc.process.pid}
18962
+ writeFileSync9(options.opencodePidFile, `${oc.process.pid}
17116
18963
  `, { mode: 384 });
17117
18964
  chmodSync3(options.opencodePidFile, 384);
17118
18965
  } catch (error2) {
@@ -17222,7 +19069,7 @@ async function run(options) {
17222
19069
  let existingPid;
17223
19070
  if (existsSync3(options.litestreamPidFile)) {
17224
19071
  try {
17225
- const rawPid = readFileSync9(options.litestreamPidFile, "utf8").trim();
19072
+ const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
17226
19073
  const parsedPid = Number(rawPid);
17227
19074
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
17228
19075
  existingPid = parsedPid;
@@ -17259,7 +19106,7 @@ async function run(options) {
17259
19106
  });
17260
19107
  try {
17261
19108
  if (litestreamProcess.pid !== void 0) {
17262
- writeFileSync7(options.litestreamPidFile, `${litestreamProcess.pid}
19109
+ writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
17263
19110
  `, {
17264
19111
  mode: 384
17265
19112
  });
@@ -17309,6 +19156,15 @@ async function run(options) {
17309
19156
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
17310
19157
  }
17311
19158
  const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
19159
+ const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
19160
+ sessionBindingLedgerPath(homedir8(), process.env),
19161
+ {
19162
+ conversation_id: conversationId,
19163
+ opencode_session_id: sessionId,
19164
+ observed_at: (/* @__PURE__ */ new Date()).toISOString()
19165
+ },
19166
+ (message) => logActivity(state, { type: "info", level: "warn", message })
19167
+ ) : void 0;
17312
19168
  const channelDriver = new ChannelDriver({
17313
19169
  agentId: state.agentId,
17314
19170
  port: state.port,
@@ -17318,9 +19174,11 @@ async function run(options) {
17318
19174
  conversationFilter: state.conversationFilter,
17319
19175
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
17320
19176
  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.
19177
+ recordBinding,
19178
+ // #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
19179
+ // `file_sync_disabled` on the ack, not silently ignored.
17323
19180
  fileSyncDirectories,
19181
+ fileSyncExactFiles,
17324
19182
  homeDir: homedir8(),
17325
19183
  maxActiveSessions,
17326
19184
  log: (entry) => (
@@ -17496,6 +19354,7 @@ async function run(options) {
17496
19354
  }
17497
19355
  if (!state.running) return;
17498
19356
  scheduleSessionCleanup(state, channelDriver, options);
19357
+ scheduleSessionArchive(state);
17499
19358
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
17500
19359
  state.openaiUsageRearm = scheduleUsageReporting(state, {
17501
19360
  label: "OpenAI",
@@ -17591,8 +19450,8 @@ program.command("run").description("Connect to Evident and process messages").op
17591
19450
  "--opencode-start-timeout <seconds>",
17592
19451
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
17593
19452
  ).option(
17594
- "--opencode-version <v1|v2>",
17595
- "Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
19453
+ "--opencode-version <v1|v2|auto>",
19454
+ "OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
17596
19455
  ).option("--json", "Output in JSON format").option(
17597
19456
  "--session-cleanup-max-age <duration>",
17598
19457
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -17605,6 +19464,12 @@ program.command("run").description("Connect to Evident and process messages").op
17605
19464
  ).option(
17606
19465
  "--session-cleanup-interval <duration>",
17607
19466
  "How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
19467
+ ).option(
19468
+ "--session-archive",
19469
+ "Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
19470
+ ).option(
19471
+ "--session-archive-interval <duration>",
19472
+ "How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
17608
19473
  ).option(
17609
19474
  "--claude-usage-reporting <mode>",
17610
19475
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
@@ -17619,6 +19484,11 @@ program.command("run").description("Connect to Evident and process messages").op
17619
19484
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
17620
19485
  (value, previous) => previous.concat([value]),
17621
19486
  []
19487
+ ).option(
19488
+ "--enable-file-sync-to-exact-file <path>",
19489
+ "Allow Evident to write files to exactly this path (repeatable).",
19490
+ (value, previous) => previous.concat([value]),
19491
+ []
17622
19492
  ).option(
17623
19493
  "--tunnel-ready-file <path>",
17624
19494
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
@@ -17671,6 +19541,8 @@ program.command("run").description("Connect to Evident and process messages").op
17671
19541
  sessionCleanupMaxCount: options.sessionCleanupMaxCount,
17672
19542
  maxActiveSessions: options.maxActiveSessions,
17673
19543
  sessionCleanupInterval: options.sessionCleanupInterval,
19544
+ sessionArchive: options.sessionArchive,
19545
+ sessionArchiveInterval: options.sessionArchiveInterval,
17674
19546
  // Raw string — the resolver in run.ts single-sources parsing
17675
19547
  // (resolveClaudeUsageReportingMode).
17676
19548
  claudeUsageReporting: options.claudeUsageReporting,
@@ -17681,6 +19553,9 @@ program.command("run").description("Connect to Evident and process messages").op
17681
19553
  // Raw values — expansion/validation is single-sourced in run.ts's
17682
19554
  // resolveFileSyncDirectories.
17683
19555
  enableFileSyncTo: options.enableFileSyncTo,
19556
+ // Raw values — expansion/validation is single-sourced in run.ts's
19557
+ // resolveFileSyncExactFiles.
19558
+ enableFileSyncToExactFile: options.enableFileSyncToExactFile,
17684
19559
  tunnelReadyFile: options.tunnelReadyFile,
17685
19560
  litestreamConfig: options.litestreamConfig,
17686
19561
  opencodePidFile: options.opencodePidFile,