@evident-ai/cli 3.6.1-dev.1a9c5e7 → 3.6.1-dev.22e9703

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
@@ -723,6 +723,9 @@ async function reportResourceUsage(agentId, authHeader, usage) {
723
723
  opencode_db_bytes: usage.opencodeDbBytes
724
724
  });
725
725
  }
726
+ async function reportSessionDeletions(agentId, authHeader, deletions) {
727
+ return postBestEffort(`/runners/${agentId}/session-deletions`, authHeader, { deletions });
728
+ }
726
729
  async function getRunnerCredentialVault(agentId, authHeader) {
727
730
  const empty = { prefix: null, vaultId: null, source: null };
728
731
  try {
@@ -1176,9 +1179,9 @@ async function claudeUsage() {
1176
1179
  }
1177
1180
 
1178
1181
  // src/commands/run.ts
1179
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
1182
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
1180
1183
  import { homedir as homedir8 } from "node:os";
1181
- import { isAbsolute as isAbsolute4, join as join13, parse, resolve as resolvePath2 } from "node:path";
1184
+ import { isAbsolute as isAbsolute4, join as join16, parse, resolve as resolvePath2 } from "node:path";
1182
1185
  import chalk7 from "chalk";
1183
1186
 
1184
1187
  // ../../packages/types/src/agents/index.ts
@@ -1442,7 +1445,7 @@ import ora4 from "ora";
1442
1445
  import { select as select4 } from "@inquirer/prompts";
1443
1446
 
1444
1447
  // src/lib/telemetry.ts
1445
- var CLI_VERSION = (true ? "3.6.1-dev.1a9c5e7" : void 0) ?? process.env.npm_package_version ?? "unknown";
1448
+ var CLI_VERSION = (true ? "3.6.1-dev.22e9703" : void 0) ?? process.env.npm_package_version ?? "unknown";
1446
1449
  function getCliVersion() {
1447
1450
  return CLI_VERSION;
1448
1451
  }
@@ -2192,6 +2195,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
2192
2195
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
2193
2196
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
2194
2197
  "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2198
+ "cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
2195
2199
  "cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
2196
2200
  };
2197
2201
  var SEVERITY_BY_LEVEL = {
@@ -2638,256 +2642,1351 @@ function isSessionDbRecoveryRecord(value) {
2638
2642
  );
2639
2643
  }
2640
2644
 
2641
- // src/lib/opencode/auth.ts
2642
- function buildOpenCodeBasicAuthHeader(password) {
2643
- return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
2645
+ // src/lib/opencode/session-deletion-report.ts
2646
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
2647
+ import { dirname as dirname4, join as join4 } from "node:path";
2648
+ var SESSION_DELETION_SPOOL_MAX_RECORDS = 1e3;
2649
+ var MAX_DIAGNOSTIC_LENGTH = 200;
2650
+ function sessionDeletionReportPath(homeDir, env) {
2651
+ const override = env.EVIDENT_SESSION_DELETION_REPORT?.trim();
2652
+ return override || join4(homeDir, ".local", "state", "evident", "session-deletions.jsonl");
2653
+ }
2654
+ function appendSessionDeletionRecords(path, records) {
2655
+ if (records.length === 0) return 0;
2656
+ mkdirSync(dirname4(path), { recursive: true });
2657
+ const current = readSessionDeletionRecords(path);
2658
+ if (current.readError !== null) {
2659
+ throw new Error(`cannot enforce the spool limit after reading ${path}: ${current.readError}`);
2660
+ }
2661
+ const combined = [...current.records, ...records];
2662
+ const kept = combined.slice(-SESSION_DELETION_SPOOL_MAX_RECORDS);
2663
+ const dropped = combined.length - kept.length;
2664
+ if (dropped > 0) {
2665
+ writeFileSync(path, `${kept.map((record) => JSON.stringify(record)).join("\n")}
2666
+ `, "utf8");
2667
+ } else {
2668
+ appendFileSync(path, `${records.map((record) => JSON.stringify(record)).join("\n")}
2669
+ `, "utf8");
2670
+ }
2671
+ return dropped;
2644
2672
  }
2645
-
2646
- // src/lib/opencode/health.ts
2647
- async function checkOpenCodeHealth(port) {
2673
+ function readSessionDeletionRecords(path) {
2674
+ let content;
2648
2675
  try {
2649
- const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
2650
- signal: AbortSignal.timeout(2e3)
2651
- // 2 second timeout
2652
- });
2653
- if (response.status === 401) {
2654
- return { healthy: false, authFailed: true, error: "HTTP 401" };
2655
- }
2656
- if (!response.ok) {
2657
- return { healthy: false, error: `HTTP ${response.status}` };
2658
- }
2659
- const data = await response.json().catch(() => ({}));
2660
- return { healthy: true, version: data.version };
2676
+ content = readFileSync3(path, "utf8");
2661
2677
  } catch (error2) {
2662
- const message = error2 instanceof Error ? error2.message : "Unknown error";
2663
- return { healthy: false, error: message };
2678
+ if (isMissingFile(error2)) return { records: [], skippedLines: 0, readError: null };
2679
+ const readError = boundedError(error2);
2680
+ console.error(`[session-deletion-report] could not read ${path}: ${readError}`);
2681
+ return { records: [], skippedLines: 0, readError };
2664
2682
  }
2683
+ let skippedLines = 0;
2684
+ const records = content.split("\n").flatMap((line) => {
2685
+ if (!line.trim()) return [];
2686
+ const record = parseRecord(line, path);
2687
+ if (record) return [record];
2688
+ skippedLines++;
2689
+ return [];
2690
+ });
2691
+ return { records, skippedLines, readError: null };
2665
2692
  }
2666
- function bodyExcerpt(body) {
2667
- const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
2668
- return excerpt ? ` (body="${excerpt}")` : " (empty body)";
2669
- }
2670
- async function checkOpenCode2Health(port, password) {
2693
+ function removeDeliveredSessionDeletionRecords(path, delivered) {
2694
+ let content;
2671
2695
  try {
2672
- const headers = {};
2673
- if (password !== void 0) {
2674
- headers.Authorization = buildOpenCodeBasicAuthHeader(password);
2675
- }
2676
- const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
2677
- headers,
2678
- signal: AbortSignal.timeout(2e3)
2679
- });
2680
- if (response.status === 401) {
2681
- return { healthy: false, authFailed: true, error: "HTTP 401" };
2682
- }
2683
- if (!response.ok) {
2684
- return { healthy: false, error: `HTTP ${response.status}` };
2685
- }
2686
- const body = await response.text();
2687
- const excerpt = bodyExcerpt(body);
2688
- let data;
2689
- try {
2690
- data = JSON.parse(body);
2691
- } catch (error2) {
2692
- return {
2693
- healthy: false,
2694
- error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
2695
- };
2696
- }
2697
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
2698
- return {
2699
- healthy: false,
2700
- error: `Invalid health response${excerpt}: expected healthy=true`
2701
- };
2702
- }
2703
- const health = data;
2704
- if (health.healthy !== true) {
2705
- return {
2706
- healthy: false,
2707
- error: `Invalid health response${excerpt}: expected healthy=true`
2708
- };
2709
- }
2710
- return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
2696
+ content = readFileSync3(path, "utf8");
2711
2697
  } catch (error2) {
2712
- const message = error2 instanceof Error ? error2.message : "Unknown error";
2713
- return { healthy: false, error: message };
2698
+ if (isMissingFile(error2)) return;
2699
+ console.error(
2700
+ `[session-deletion-report] could not read ${path} before removing delivered records: ${boundedError(error2)}`
2701
+ );
2702
+ throw error2;
2714
2703
  }
2715
- }
2716
- async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
2717
- const startTime = Date.now();
2718
- while (Date.now() - startTime < timeoutMs) {
2719
- const health = await checkOpenCodeHealth(port);
2720
- if (health.healthy || health.authFailed) {
2721
- return health;
2722
- }
2723
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
2704
+ const toRemove = /* @__PURE__ */ new Map();
2705
+ for (const record of delivered) {
2706
+ toRemove.set(record.opencode_session_id, (toRemove.get(record.opencode_session_id) ?? 0) + 1);
2724
2707
  }
2725
- return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
2726
- }
2727
- async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
2728
- const startTime = Date.now();
2729
- while (Date.now() - startTime < timeoutMs) {
2730
- const health = await checkOpenCode2Health(port, password);
2731
- if (health.healthy || health.authFailed) {
2732
- return health;
2708
+ const remaining = [];
2709
+ for (const line of content.split("\n")) {
2710
+ if (!line.trim()) continue;
2711
+ const record = parseRecord(line, path);
2712
+ const count = record ? toRemove.get(record.opencode_session_id) ?? 0 : 0;
2713
+ if (record && count > 0) {
2714
+ if (count === 1) toRemove.delete(record.opencode_session_id);
2715
+ else toRemove.set(record.opencode_session_id, count - 1);
2716
+ } else {
2717
+ remaining.push(line);
2733
2718
  }
2734
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
2735
- }
2736
- return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
2737
- }
2738
-
2739
- // src/lib/http-timeout.ts
2740
- var REQUEST_TIMEOUT_MS = 6e4;
2741
- var RequestTimeoutError = class extends Error {
2742
- constructor(operation, timeoutMs, elapsedMs) {
2743
- super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
2744
- this.operation = operation;
2745
- this.timeoutMs = timeoutMs;
2746
- this.elapsedMs = elapsedMs;
2747
- this.name = "TimeoutError";
2748
2719
  }
2749
- };
2750
- function describeRequestOperation(input, init) {
2751
- const method = (init?.method ?? input.method ?? "GET").toUpperCase();
2752
- const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2753
- let path;
2754
2720
  try {
2755
- path = new URL(rawUrl).pathname;
2721
+ if (remaining.length === 0) unlinkSync2(path);
2722
+ else writeFileSync(path, `${remaining.join("\n")}
2723
+ `, "utf8");
2756
2724
  } catch (error2) {
2757
- if (!(error2 instanceof TypeError)) throw error2;
2758
- path = rawUrl.split(/[?#]/, 1)[0];
2725
+ if (isMissingFile(error2)) return;
2726
+ console.error(
2727
+ `[session-deletion-report] could not write ${path} after removing delivered records: ${boundedError(error2)}`
2728
+ );
2729
+ throw error2;
2759
2730
  }
2760
- const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2761
- const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
2762
- const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
2763
- return `${method} ${maskedPath}`;
2764
- }
2765
- function withRequestTimeout(fetchImpl, timeoutMs) {
2766
- return (async (input, init) => {
2767
- const startedAt = Date.now();
2768
- try {
2769
- return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
2770
- } catch (error2) {
2771
- if (error2?.name === "TimeoutError") {
2772
- throw new RequestTimeoutError(
2773
- describeRequestOperation(input, init),
2774
- timeoutMs,
2775
- Date.now() - startedAt
2776
- );
2777
- }
2778
- throw error2;
2779
- }
2780
- });
2781
- }
2782
-
2783
- // src/lib/opencode/client.ts
2784
- function redactPassword(message, password) {
2785
- return message.replaceAll(password, "[redacted]");
2786
- }
2787
- function createOpenCodeClient(options) {
2788
- const password = options.password ?? null;
2789
- const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
2790
- const baseUrl = `http://127.0.0.1:${options.port}`;
2791
- return {
2792
- port: options.port,
2793
- version: options.version,
2794
- password,
2795
- async request(path, init, requestOptions) {
2796
- const requestInit = options.version === "v2" && password !== null ? (() => {
2797
- const headers = new Headers(init?.headers);
2798
- headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
2799
- return { ...init, headers };
2800
- })() : init;
2801
- try {
2802
- const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
2803
- if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
2804
- const body = await response.text();
2805
- throw new Error(
2806
- `OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
2807
- );
2808
- }
2809
- return response;
2810
- } catch (error2) {
2811
- if (options.version === "v2" && password !== null) {
2812
- const message = error2 instanceof Error ? error2.message : String(error2);
2813
- throw new Error(redactPassword(message, password));
2814
- }
2815
- throw error2;
2816
- }
2817
- }
2818
- };
2819
- }
2820
-
2821
- // src/lib/opencode/session.ts
2822
- var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
2823
- function timedFetch(input, init) {
2824
- return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
2825
- }
2826
- function requestWithClient(port, client, path, init, options) {
2827
- return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
2828
- }
2829
- function opencodeBase(port) {
2830
- return `http://127.0.0.1:${port}`;
2831
2731
  }
2832
- async function getOpenCodeDirectory(port, client) {
2732
+ function parseRecord(line, path) {
2833
2733
  try {
2834
- const res = await requestWithClient(port, client, "/path");
2835
- if (!res.ok) return null;
2836
- const body = await res.json();
2837
- const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
2838
- return dir && dir.trim() ? dir.trim() : null;
2734
+ const value = JSON.parse(line);
2735
+ if (isSessionDeletionRecord(value)) return value;
2736
+ console.error(`[session-deletion-report] skipped malformed record in ${path}: invalid shape`);
2737
+ return null;
2839
2738
  } catch (error2) {
2840
2739
  console.error(
2841
- `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2740
+ `[session-deletion-report] skipped malformed record in ${path}: ${boundedError(error2)}`
2842
2741
  );
2843
2742
  return null;
2844
2743
  }
2845
2744
  }
2846
- function roleOf2(m) {
2847
- if (!m || typeof m !== "object") return void 0;
2848
- if (typeof m.role === "string") return m.role;
2849
- const infoRole = m.info?.role;
2850
- return typeof infoRole === "string" ? infoRole : void 0;
2745
+ function isSessionDeletionRecord(value) {
2746
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2747
+ const record = value;
2748
+ const policy = record.policy;
2749
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
2750
+ const fields = policy;
2751
+ return typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.deleted_at === "string" && record.reason === "auto_cleanup" && (fields.max_age_ms === null || typeof fields.max_age_ms === "number") && (fields.max_count === null || typeof fields.max_count === "number") && (fields.last_activity_at === null || typeof fields.last_activity_at === "string");
2851
2752
  }
2852
- function completedOf(m) {
2853
- if (!m || typeof m !== "object") return void 0;
2854
- return m.info?.time?.completed ?? m.time?.completed;
2753
+ function isMissingFile(error2) {
2754
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
2855
2755
  }
2856
- function createdOf(m) {
2857
- if (!m || typeof m !== "object") return void 0;
2858
- return m.info?.time?.created ?? m.time?.created;
2756
+ function boundedError(error2) {
2757
+ const message = error2 instanceof Error ? error2.message : String(error2);
2758
+ return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
2859
2759
  }
2860
- function idOf2(m) {
2861
- if (!m || typeof m !== "object") return void 0;
2862
- if (typeof m.id === "string") return m.id;
2863
- const infoId = m.info?.id;
2864
- return typeof infoId === "string" ? infoId : void 0;
2760
+
2761
+ // src/lib/opencode/session-binding-ledger.ts
2762
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2763
+ import { dirname as dirname5, join as join5 } from "node:path";
2764
+ var SESSION_BINDING_LEDGER_MAX_RECORDS = 1e3;
2765
+ var MAX_DIAGNOSTIC_LENGTH2 = 200;
2766
+ function sessionBindingLedgerPath(homeDir, env) {
2767
+ const override = env.EVIDENT_SESSION_BINDING_LEDGER?.trim();
2768
+ return override || join5(homeDir, ".local", "state", "evident", "session-bindings.jsonl");
2769
+ }
2770
+ function appendSessionBinding(path, record, log3) {
2771
+ mkdirSync2(dirname5(path), { recursive: true });
2772
+ const current = readSessionBindingRecords(path, log3);
2773
+ if (current.readError !== null) {
2774
+ throw new Error(`cannot enforce the ledger limit after reading ${path}: ${current.readError}`);
2775
+ }
2776
+ if (current.records.some(
2777
+ (existing) => existing.conversation_id === record.conversation_id && existing.opencode_session_id === record.opencode_session_id
2778
+ )) {
2779
+ return;
2780
+ }
2781
+ const kept = [...current.records, record].slice(-SESSION_BINDING_LEDGER_MAX_RECORDS);
2782
+ if (kept.length > current.records.length) {
2783
+ appendFileSync2(path, `${JSON.stringify(record)}
2784
+ `, "utf8");
2785
+ } else {
2786
+ writeFileSync2(path, `${kept.map((entry) => JSON.stringify(entry)).join("\n")}
2787
+ `, "utf8");
2788
+ }
2865
2789
  }
2866
- function parentIdOf2(m) {
2867
- if (!m || typeof m !== "object") return void 0;
2868
- if (typeof m.parentID === "string") return m.parentID;
2869
- const infoParent = m.info?.parentID;
2870
- return typeof infoParent === "string" ? infoParent : void 0;
2790
+ function readSessionBindings(path, log3) {
2791
+ const result = readSessionBindingRecords(path, log3);
2792
+ if (result.readError !== null) {
2793
+ log3(`[session-binding-ledger] could not read ${path}: ${result.readError}`);
2794
+ }
2795
+ return new Map(
2796
+ result.records.map((record) => [record.opencode_session_id, record.conversation_id])
2797
+ );
2871
2798
  }
2872
- function finishOf(m) {
2873
- if (!m || typeof m !== "object") return void 0;
2874
- if (typeof m.finish === "string") return m.finish;
2875
- const infoFinish = m.info?.finish;
2876
- return typeof infoFinish === "string" ? infoFinish : void 0;
2799
+ function readSessionBindingRecords(path, log3) {
2800
+ let content;
2801
+ try {
2802
+ content = readFileSync4(path, "utf8");
2803
+ } catch (error2) {
2804
+ if (isMissingFile2(error2)) return { records: [], readError: null };
2805
+ const readError = boundedError2(error2);
2806
+ return { records: [], readError };
2807
+ }
2808
+ const records = content.split("\n").flatMap((line) => {
2809
+ if (!line.trim()) return [];
2810
+ const record = parseRecord2(line, path, log3);
2811
+ return record ? [record] : [];
2812
+ });
2813
+ return { records, readError: null };
2877
2814
  }
2878
- function errorOf(m) {
2879
- if (!m || typeof m !== "object") return void 0;
2880
- return m.info?.error ?? m.error;
2815
+ function parseRecord2(line, path, log3) {
2816
+ try {
2817
+ const value = JSON.parse(line);
2818
+ if (isSessionBindingRecord(value)) return value;
2819
+ log3(`[session-binding-ledger] skipped malformed record in ${path}: invalid shape`);
2820
+ } catch (error2) {
2821
+ log3(`[session-binding-ledger] skipped malformed record in ${path}: ${boundedError2(error2)}`);
2822
+ }
2823
+ return null;
2881
2824
  }
2882
- function isTerminalStopAnswer(m) {
2883
- return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
2825
+ function isSessionBindingRecord(value) {
2826
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2827
+ const record = value;
2828
+ return typeof record.conversation_id === "string" && record.conversation_id.length > 0 && typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.observed_at === "string";
2884
2829
  }
2885
- function isAssistantInFlight(m) {
2886
- if (completedOf(m) == null) return true;
2887
- if (errorOf(m) != null) return false;
2888
- return finishOf(m) === "tool-calls";
2830
+ function isMissingFile2(error2) {
2831
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
2889
2832
  }
2890
- async function getSessionMessages(port, sessionId, client) {
2833
+ function boundedError2(error2) {
2834
+ const message = error2 instanceof Error ? error2.message : String(error2);
2835
+ return message.slice(0, MAX_DIAGNOSTIC_LENGTH2);
2836
+ }
2837
+
2838
+ // src/lib/opencode/session-archive-config.ts
2839
+ import { readFileSync as readFileSync5 } from "node:fs";
2840
+
2841
+ // src/lib/opencode/session-cleanup.ts
2842
+ var DURATION_UNIT_MS = {
2843
+ s: 1e3,
2844
+ m: 60 * 1e3,
2845
+ h: 60 * 60 * 1e3,
2846
+ d: 24 * 60 * 60 * 1e3
2847
+ };
2848
+ function parseDurationMs(input) {
2849
+ const trimmed = input.trim();
2850
+ const match = /^(\d+)([smhd])$/.exec(trimmed);
2851
+ if (!match) {
2852
+ throw new Error(
2853
+ `Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
2854
+ );
2855
+ }
2856
+ const value = Number(match[1]);
2857
+ if (value <= 0) {
2858
+ throw new Error(`Invalid duration "${input}": must be a positive value.`);
2859
+ }
2860
+ return value * DURATION_UNIT_MS[match[2]];
2861
+ }
2862
+ function selectSessionsToDelete(sessions, opts) {
2863
+ const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
2864
+ if (maxAgeMs === void 0 && maxCount === void 0) return [];
2865
+ const ageEligible = (s) => {
2866
+ if (maxAgeMs === void 0) return false;
2867
+ if (s.lastActivityMs === null) return true;
2868
+ return nowMs - s.lastActivityMs > maxAgeMs;
2869
+ };
2870
+ const countEligibleIds = /* @__PURE__ */ new Set();
2871
+ if (maxCount !== void 0) {
2872
+ const byActivityDesc = [...sessions].sort(
2873
+ (a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
2874
+ );
2875
+ for (const s of byActivityDesc.slice(maxCount)) {
2876
+ countEligibleIds.add(s.id);
2877
+ }
2878
+ }
2879
+ const toDelete = [];
2880
+ for (const s of sessions) {
2881
+ if (protectedIds.has(s.id)) continue;
2882
+ if (ageEligible(s) || countEligibleIds.has(s.id)) {
2883
+ toDelete.push(s.id);
2884
+ }
2885
+ }
2886
+ const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
2887
+ return toDelete.sort(
2888
+ (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
2889
+ );
2890
+ }
2891
+ var DEFAULT_INTERVAL = "1h";
2892
+ function resolve2(flag, envValue, fallback) {
2893
+ return flag ?? envValue ?? fallback;
2894
+ }
2895
+ function parseMaxCount(input) {
2896
+ const trimmed = input.trim();
2897
+ if (!/^\d+$/.test(trimmed)) {
2898
+ throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
2899
+ }
2900
+ const value = Number(trimmed);
2901
+ if (value <= 0) {
2902
+ throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
2903
+ }
2904
+ return value;
2905
+ }
2906
+ function resolveSessionCleanupConfig(flags, env = process.env) {
2907
+ const warnings = [];
2908
+ const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
2909
+ const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
2910
+ const intervalRaw = resolve2(
2911
+ flags.interval,
2912
+ env.EVIDENT_SESSION_CLEANUP_INTERVAL,
2913
+ DEFAULT_INTERVAL
2914
+ );
2915
+ let maxAgeMs;
2916
+ if (maxAgeRaw !== void 0) {
2917
+ try {
2918
+ maxAgeMs = parseDurationMs(maxAgeRaw);
2919
+ } catch (err) {
2920
+ warnings.push(
2921
+ `Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
2922
+ );
2923
+ }
2924
+ }
2925
+ let maxCount;
2926
+ if (maxCountRaw !== void 0) {
2927
+ try {
2928
+ maxCount = parseMaxCount(maxCountRaw);
2929
+ } catch (err) {
2930
+ warnings.push(
2931
+ `Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
2932
+ );
2933
+ }
2934
+ }
2935
+ let intervalMs;
2936
+ try {
2937
+ intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
2938
+ } catch (err) {
2939
+ warnings.push(
2940
+ `Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
2941
+ );
2942
+ intervalMs = parseDurationMs(DEFAULT_INTERVAL);
2943
+ }
2944
+ const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
2945
+ return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
2946
+ }
2947
+
2948
+ // src/lib/opencode/session-archive-config.ts
2949
+ var DEFAULT_INTERVAL2 = "1h";
2950
+ var DEFAULT_FLUSH_BUDGET_MS = 2e3;
2951
+ var MAX_SESSION_ARCHIVE_TIMER_DELAY_MS = 2147483647;
2952
+ var STATE_PREFIX_FILE = "/dev/shm/evident-state-prefix";
2953
+ var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
2954
+ var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
2955
+ function nonEmpty(value) {
2956
+ const trimmed = value?.trim();
2957
+ return trimmed ? trimmed : null;
2958
+ }
2959
+ function parseFlushBudgetMs(value) {
2960
+ const trimmed = value.trim();
2961
+ const budgetMs = Number(trimmed);
2962
+ if (!/^\d+$/.test(trimmed) || !Number.isSafeInteger(budgetMs) || budgetMs <= 0 || budgetMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
2963
+ throw new Error(
2964
+ `Invalid flush budget "${value}": expected a positive integer number of milliseconds no greater than ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}.`
2965
+ );
2966
+ }
2967
+ return budgetMs;
2968
+ }
2969
+ function resolveSessionArchiveConfig(flags, env = process.env) {
2970
+ const warnings = [];
2971
+ const archiveRaw = env.EVIDENT_SESSION_ARCHIVE;
2972
+ let enabled = flags.enabled ?? false;
2973
+ if (flags.enabled === void 0 && archiveRaw !== void 0 && archiveRaw.trim() !== "") {
2974
+ const normalized = archiveRaw.trim().toLowerCase();
2975
+ if (ENABLED_VALUES.has(normalized)) {
2976
+ enabled = true;
2977
+ } else if (DISABLED_VALUES.has(normalized)) {
2978
+ enabled = false;
2979
+ } else {
2980
+ warnings.push(
2981
+ `Ignoring invalid EVIDENT_SESSION_ARCHIVE "${archiveRaw}": expected on or off; session archiving remains off.`
2982
+ );
2983
+ }
2984
+ }
2985
+ let intervalMs;
2986
+ const intervalRaw = flags.interval ?? env.EVIDENT_SESSION_ARCHIVE_INTERVAL ?? DEFAULT_INTERVAL2;
2987
+ try {
2988
+ intervalMs = parseDurationMs(intervalRaw);
2989
+ if (intervalMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
2990
+ throw new Error(
2991
+ `Duration "${intervalRaw}" exceeds Node's maximum timer delay of ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}ms.`
2992
+ );
2993
+ }
2994
+ } catch (err) {
2995
+ warnings.push(
2996
+ `Ignoring invalid session archive interval, using default ${DEFAULT_INTERVAL2}: ${err instanceof Error ? err.message : String(err)}`
2997
+ );
2998
+ intervalMs = parseDurationMs(DEFAULT_INTERVAL2);
2999
+ }
3000
+ let flushBudgetMs;
3001
+ const flushBudgetRaw = env.EVIDENT_SESSION_ARCHIVE_FLUSH_MS ?? String(DEFAULT_FLUSH_BUDGET_MS);
3002
+ try {
3003
+ flushBudgetMs = parseFlushBudgetMs(flushBudgetRaw);
3004
+ } catch (err) {
3005
+ warnings.push(
3006
+ `Ignoring invalid EVIDENT_SESSION_ARCHIVE_FLUSH_MS, using default ${DEFAULT_FLUSH_BUDGET_MS}: ${err instanceof Error ? err.message : String(err)}`
3007
+ );
3008
+ flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
3009
+ }
3010
+ const bucket = nonEmpty(env.PERSISTENCE_BUCKET);
3011
+ let keyPrefix = nonEmpty(env.LITESTREAM_PREFIX);
3012
+ if (enabled && bucket === null) {
3013
+ warnings.push("Session archiving is off because PERSISTENCE_BUCKET is unset or empty.");
3014
+ enabled = false;
3015
+ }
3016
+ if (enabled && keyPrefix === null) {
3017
+ let fallbackReadError = null;
3018
+ try {
3019
+ keyPrefix = nonEmpty(readFileSync5(STATE_PREFIX_FILE, "utf8"));
3020
+ } catch (err) {
3021
+ fallbackReadError = err instanceof Error ? err.message : String(err);
3022
+ }
3023
+ if (keyPrefix === null) {
3024
+ const readFailure = fallbackReadError ? ` Could not read ${STATE_PREFIX_FILE}: ${fallbackReadError}.` : "";
3025
+ warnings.push(
3026
+ `Session archiving is off because neither LITESTREAM_PREFIX nor ${STATE_PREFIX_FILE} contains a prefix.${readFailure}`
3027
+ );
3028
+ enabled = false;
3029
+ }
3030
+ }
3031
+ return { enabled, bucket, keyPrefix, intervalMs, flushBudgetMs, warnings };
3032
+ }
3033
+
3034
+ // src/lib/opencode/session-archive-snapshot.ts
3035
+ import { createHash } from "node:crypto";
3036
+ var SessionArchiveExportError = class extends Error {
3037
+ constructor(code, message) {
3038
+ super(message);
3039
+ this.code = code;
3040
+ this.name = "SessionArchiveExportError";
3041
+ }
3042
+ };
3043
+ function canonicalize(value) {
3044
+ if (Array.isArray(value)) return value.map(canonicalize);
3045
+ if (typeof value !== "object" || value === null) return value;
3046
+ return Object.fromEntries(
3047
+ Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, canonicalize(nested)])
3048
+ );
3049
+ }
3050
+ function contentForFingerprint(archive) {
3051
+ return JSON.stringify(
3052
+ canonicalize({
3053
+ opencode_api_version: archive.opencode_api_version,
3054
+ opencode_session_id: archive.opencode_session_id,
3055
+ evident_conversation_id: archive.evident_conversation_id,
3056
+ messages: archive.messages
3057
+ })
3058
+ );
3059
+ }
3060
+ function fingerprintArchive(archive) {
3061
+ return createHash("sha256").update(contentForFingerprint(archive)).digest("hex");
3062
+ }
3063
+ function buildSessionArchive(input) {
3064
+ const archive = {
3065
+ schema_version: 1,
3066
+ opencode_api_version: input.apiVersion,
3067
+ opencode_session_id: input.session.id,
3068
+ evident_conversation_id: input.conversationId,
3069
+ evident_runner_id: input.runnerId,
3070
+ binding_source: input.bindingSource,
3071
+ session: input.session,
3072
+ messages: input.messages,
3073
+ message_count: input.messages.length,
3074
+ archived_at: input.archivedAt
3075
+ };
3076
+ return { ...archive, content_fingerprint: fingerprintArchive(archive) };
3077
+ }
3078
+ function isRecord(value) {
3079
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3080
+ }
3081
+ function errorMessage2(error2) {
3082
+ return error2 instanceof Error ? error2.message : String(error2);
3083
+ }
3084
+ function invalidResponse(version2, sessionId, path, reason) {
3085
+ return new SessionArchiveExportError(
3086
+ "invalid-response",
3087
+ `OpenCode ${version2} session ${sessionId} returned an invalid response from GET ${path}: ${reason}`
3088
+ );
3089
+ }
3090
+ async function requestPage(client, sessionId, path, signal) {
3091
+ let response;
3092
+ try {
3093
+ response = await client.request(path, signal ? { signal } : void 0, {
3094
+ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100)
3095
+ });
3096
+ } catch (error2) {
3097
+ throw new SessionArchiveExportError(
3098
+ "request-failed",
3099
+ `OpenCode ${client.version} session ${sessionId} request GET ${path} failed: ${errorMessage2(error2)}`
3100
+ );
3101
+ }
3102
+ if (response.status === 404) {
3103
+ throw new SessionArchiveExportError(
3104
+ "not-found",
3105
+ `OpenCode ${client.version} session ${sessionId} was not found at GET ${path} (HTTP 404)`
3106
+ );
3107
+ }
3108
+ if (!response.ok) {
3109
+ throw new SessionArchiveExportError(
3110
+ "request-failed",
3111
+ `OpenCode ${client.version} session ${sessionId} request GET ${path} returned HTTP ${response.status}`
3112
+ );
3113
+ }
3114
+ let bodyText;
3115
+ try {
3116
+ bodyText = await response.text();
3117
+ } catch (error2) {
3118
+ throw new SessionArchiveExportError(
3119
+ "request-failed",
3120
+ `OpenCode ${client.version} session ${sessionId} response body from GET ${path} could not be read: ${errorMessage2(error2)}`
3121
+ );
3122
+ }
3123
+ try {
3124
+ return { response, body: JSON.parse(bodyText) };
3125
+ } catch (error2) {
3126
+ if (!(error2 instanceof SyntaxError)) throw error2;
3127
+ const contentType = response.headers.get("content-type") ?? "";
3128
+ const looksLikeHtml = contentType.toLowerCase().includes("text/html") || /^\s*<!doctype\s+html|^\s*<html/i.test(bodyText);
3129
+ throw invalidResponse(
3130
+ client.version,
3131
+ sessionId,
3132
+ path,
3133
+ looksLikeHtml ? "received text/html, expected JSON" : "received non-JSON content, expected JSON"
3134
+ );
3135
+ }
3136
+ }
3137
+ function v1NextCursor(response, sessionId, path) {
3138
+ const headerCursor = response.headers.get("x-next-cursor")?.trim() || null;
3139
+ const linkHeader = response.headers.get("link");
3140
+ const nextLink = linkHeader?.split(",").map((link) => link.trim()).find((link) => /;\s*rel\s*=\s*"?next"?/i.test(link));
3141
+ let linkCursor = null;
3142
+ if (nextLink) {
3143
+ const target = nextLink.match(/<([^>]+)>/)?.[1];
3144
+ if (!target) {
3145
+ throw new SessionArchiveExportError(
3146
+ "pagination-failed",
3147
+ `OpenCode v1 session ${sessionId} returned a next Link without a URL for GET ${path}`
3148
+ );
3149
+ }
3150
+ try {
3151
+ linkCursor = new URL(target, "http://127.0.0.1").searchParams.get("before");
3152
+ } catch (error2) {
3153
+ throw new SessionArchiveExportError(
3154
+ "pagination-failed",
3155
+ `OpenCode v1 session ${sessionId} returned an invalid next Link for GET ${path}: ${errorMessage2(error2)}`
3156
+ );
3157
+ }
3158
+ }
3159
+ if (headerCursor && linkCursor && headerCursor !== linkCursor) {
3160
+ throw new SessionArchiveExportError(
3161
+ "pagination-failed",
3162
+ `OpenCode v1 session ${sessionId} returned conflicting next cursors for GET ${path}`
3163
+ );
3164
+ }
3165
+ const cursor = headerCursor ?? linkCursor;
3166
+ if ((headerCursor || nextLink) && !cursor) {
3167
+ throw new SessionArchiveExportError(
3168
+ "pagination-failed",
3169
+ `OpenCode v1 session ${sessionId} returned a next page without a usable cursor for GET ${path}`
3170
+ );
3171
+ }
3172
+ return cursor;
3173
+ }
3174
+ function v1Messages(body, sessionId, path) {
3175
+ if (!Array.isArray(body)) {
3176
+ throw invalidResponse("v1", sessionId, path, "expected an array of messages");
3177
+ }
3178
+ return body;
3179
+ }
3180
+ async function exportV1SessionMessages(client, sessionId, signal) {
3181
+ const basePath = `/session/${encodeURIComponent(sessionId)}/message`;
3182
+ const firstPage = await requestPage(client, sessionId, basePath, signal);
3183
+ let messages = v1Messages(firstPage.body, sessionId, basePath);
3184
+ let cursor = v1NextCursor(firstPage.response, sessionId, basePath);
3185
+ const seenCursors = /* @__PURE__ */ new Set();
3186
+ while (cursor !== null) {
3187
+ if (seenCursors.has(cursor)) {
3188
+ throw new SessionArchiveExportError(
3189
+ "pagination-failed",
3190
+ `OpenCode v1 session ${sessionId} repeated cursor while reading GET ${basePath}`
3191
+ );
3192
+ }
3193
+ seenCursors.add(cursor);
3194
+ const path = `${basePath}?${new URLSearchParams({ before: cursor }).toString()}`;
3195
+ const page = await requestPage(client, sessionId, path, signal);
3196
+ messages = [...v1Messages(page.body, sessionId, path), ...messages];
3197
+ cursor = v1NextCursor(page.response, sessionId, path);
3198
+ }
3199
+ return messages;
3200
+ }
3201
+ async function exportV2SessionMessages(client, sessionId, signal) {
3202
+ const basePath = `/api/session/${encodeURIComponent(sessionId)}/message`;
3203
+ const messages = [];
3204
+ const seenCursors = /* @__PURE__ */ new Set();
3205
+ let cursor;
3206
+ let hasNextPage = true;
3207
+ while (hasNextPage) {
3208
+ const params = new URLSearchParams({ limit: "100" });
3209
+ if (cursor === void 0) {
3210
+ params.set("order", "asc");
3211
+ } else {
3212
+ params.set("cursor", cursor);
3213
+ }
3214
+ const path = `${basePath}?${params.toString()}`;
3215
+ const { body } = await requestPage(client, sessionId, path, signal);
3216
+ if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) {
3217
+ throw invalidResponse("v2", sessionId, path, "expected a {data: [], cursor: {\u2026}} envelope");
3218
+ }
3219
+ messages.push(...body.data);
3220
+ const next = body.cursor.next;
3221
+ if (next === void 0 || next === null) {
3222
+ hasNextPage = false;
3223
+ continue;
3224
+ }
3225
+ if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
3226
+ throw new SessionArchiveExportError(
3227
+ "pagination-failed",
3228
+ `OpenCode v2 session ${sessionId} returned an invalid or repeated cursor from GET ${path}`
3229
+ );
3230
+ }
3231
+ seenCursors.add(next);
3232
+ cursor = next;
3233
+ }
3234
+ return messages;
3235
+ }
3236
+ async function exportSessionMessages(client, sessionId, signal) {
3237
+ if (client.version === "v1") return exportV1SessionMessages(client, sessionId, signal);
3238
+ if (client.version === "v2") return exportV2SessionMessages(client, sessionId, signal);
3239
+ throw new SessionArchiveExportError(
3240
+ "unsupported-dialect",
3241
+ `OpenCode session ${sessionId} cannot be archived: unsupported API dialect ${String(client.version)}`
3242
+ );
3243
+ }
3244
+
3245
+ // src/lib/opencode/session-archive-watermarks.ts
3246
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
3247
+ import { dirname as dirname6, join as join6 } from "node:path";
3248
+ function sessionArchiveWatermarkPath(homeDir, env) {
3249
+ const override = env.EVIDENT_SESSION_ARCHIVE_WATERMARK?.trim();
3250
+ return override || join6(homeDir, ".local", "state", "evident", "session-archive-watermarks.json");
3251
+ }
3252
+ function readWatermarks(path, log3) {
3253
+ let content;
3254
+ try {
3255
+ content = readFileSync6(path, "utf8");
3256
+ } catch (error2) {
3257
+ if (isMissingFile3(error2)) return /* @__PURE__ */ new Map();
3258
+ log3(`[session-archive-watermarks] could not read ${path}; treating every session as changed`);
3259
+ return /* @__PURE__ */ new Map();
3260
+ }
3261
+ try {
3262
+ const parsed = JSON.parse(content);
3263
+ if (!isRecord2(parsed)) throw new Error("invalid watermark object");
3264
+ const entries = Object.entries(parsed);
3265
+ if (!entries.every(([sessionId, value]) => sessionId.length > 0 && isWatermark(value))) {
3266
+ throw new Error("invalid watermark entry");
3267
+ }
3268
+ return new Map(entries);
3269
+ } catch (error2) {
3270
+ log3(`[session-archive-watermarks] could not parse ${path}; treating every session as changed`);
3271
+ return /* @__PURE__ */ new Map();
3272
+ }
3273
+ }
3274
+ function recordUpload(path, sessionId, entry, watermarks) {
3275
+ const updated = new Map(watermarks);
3276
+ updated.set(sessionId, entry);
3277
+ mkdirSync3(dirname6(path), { recursive: true });
3278
+ writeFileSync3(path, `${JSON.stringify(Object.fromEntries(updated))}
3279
+ `, {
3280
+ encoding: "utf8",
3281
+ mode: 384
3282
+ });
3283
+ watermarks.clear();
3284
+ for (const [id, watermark] of updated) watermarks.set(id, watermark);
3285
+ }
3286
+ function isRecord2(value) {
3287
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3288
+ }
3289
+ function isWatermark(value) {
3290
+ if (!isRecord2(value)) return false;
3291
+ return typeof value.fingerprint === "string" && typeof value.uploaded_at === "string" && typeof value.key === "string";
3292
+ }
3293
+ function isMissingFile3(error2) {
3294
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
3295
+ }
3296
+
3297
+ // src/lib/runner-synchroniser.ts
3298
+ import { spawn } from "node:child_process";
3299
+
3300
+ // ../../runner/synchroniser/src/shell-contract.json
3301
+ var shell_contract_default = {
3302
+ commands: [
3303
+ "env",
3304
+ "litestream-config",
3305
+ "restore",
3306
+ "sync-once",
3307
+ "model-auth-ready",
3308
+ "self-stop",
3309
+ "session-db-classify",
3310
+ "session-db-verify",
3311
+ "archive-put"
3312
+ ],
3313
+ archiveStdinMaxBytes: 16777216
3314
+ };
3315
+
3316
+ // src/lib/runner-synchroniser.ts
3317
+ var MAX_ARCHIVE_STDIN_BYTES = shell_contract_default.archiveStdinMaxBytes;
3318
+ var ERROR_CLOSE_FALLBACK_MS = 1e3;
3319
+ function isPipeClosed(error2) {
3320
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPIPE";
3321
+ }
3322
+ function appendError(stderr, error2) {
3323
+ const message = error2 instanceof Error ? error2.message : String(error2);
3324
+ return stderr === "" ? message : `${stderr}
3325
+ ${message}`;
3326
+ }
3327
+ function runSynchroniser(args, opts) {
3328
+ return new Promise((resolve5) => {
3329
+ let child;
3330
+ let stdout = "";
3331
+ let stderr = "";
3332
+ let settled = false;
3333
+ let shuttingDown = false;
3334
+ let timedOut = false;
3335
+ let stdinFailure;
3336
+ let childFailure;
3337
+ const timer = {};
3338
+ const closeFallbackTimer = {};
3339
+ let abortListener;
3340
+ let spawnListener;
3341
+ const finish = (result) => {
3342
+ if (settled) return;
3343
+ settled = true;
3344
+ if (timer.handle) clearTimeout(timer.handle);
3345
+ if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
3346
+ if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
3347
+ if (spawnListener) child.removeListener("spawn", spawnListener);
3348
+ resolve5(result);
3349
+ };
3350
+ const armErrorCloseFallback = () => {
3351
+ if (closeFallbackTimer.handle) return;
3352
+ closeFallbackTimer.handle = setTimeout(() => {
3353
+ const failure = stdinFailure ?? childFailure;
3354
+ finish({
3355
+ code: null,
3356
+ stdout,
3357
+ stderr: failure === void 0 ? stderr : appendError(stderr, failure),
3358
+ timedOut
3359
+ });
3360
+ }, ERROR_CLOSE_FALLBACK_MS);
3361
+ };
3362
+ const terminateChild = () => {
3363
+ if (!shuttingDown) {
3364
+ shuttingDown = true;
3365
+ child.stdin?.destroy();
3366
+ }
3367
+ if (child.pid !== void 0) {
3368
+ child.kill("SIGKILL");
3369
+ } else if (!spawnListener) {
3370
+ spawnListener = () => {
3371
+ if (child.pid !== void 0) child.kill("SIGKILL");
3372
+ };
3373
+ child.once("spawn", spawnListener);
3374
+ }
3375
+ };
3376
+ try {
3377
+ child = spawn("runner-synchroniser", args, {
3378
+ env: opts.env ?? process.env,
3379
+ stdio: [opts.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
3380
+ });
3381
+ } catch (error2) {
3382
+ finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
3383
+ return;
3384
+ }
3385
+ child.stdout?.setEncoding("utf8");
3386
+ child.stdout?.on("data", (chunk) => {
3387
+ stdout += chunk;
3388
+ });
3389
+ child.stderr?.setEncoding("utf8");
3390
+ child.stderr?.on("data", (chunk) => {
3391
+ stderr += chunk;
3392
+ });
3393
+ child.once("error", (error2) => {
3394
+ childFailure = error2;
3395
+ if (child.pid !== void 0) terminateChild();
3396
+ armErrorCloseFallback();
3397
+ });
3398
+ child.once("close", (code) => {
3399
+ const failure = stdinFailure ?? childFailure;
3400
+ finish({
3401
+ code: failure === void 0 ? code : null,
3402
+ stdout,
3403
+ stderr: failure === void 0 ? stderr : appendError(stderr, failure),
3404
+ timedOut
3405
+ });
3406
+ });
3407
+ const stdin = child.stdin;
3408
+ const failStdin = (error2) => {
3409
+ if (settled || shuttingDown || isPipeClosed(error2)) return;
3410
+ stdinFailure = error2;
3411
+ terminateChild();
3412
+ armErrorCloseFallback();
3413
+ };
3414
+ stdin?.once("error", failStdin);
3415
+ if (opts.stdin !== void 0 && stdin === null) {
3416
+ failStdin(new Error("synchroniser stdin is unavailable"));
3417
+ } else if (opts.stdin !== void 0 && stdin !== null) {
3418
+ const drained = stdin.write(opts.stdin, (error2) => {
3419
+ if (error2) failStdin(error2);
3420
+ });
3421
+ if (!drained) {
3422
+ stdin.once("drain", () => stdin.end());
3423
+ } else {
3424
+ stdin.end();
3425
+ }
3426
+ }
3427
+ if (opts.signal) {
3428
+ const killChild = () => {
3429
+ terminateChild();
3430
+ };
3431
+ abortListener = killChild;
3432
+ if (opts.signal.aborted) {
3433
+ abortListener();
3434
+ } else {
3435
+ opts.signal.addEventListener("abort", abortListener, { once: true });
3436
+ if (opts.signal.aborted) abortListener();
3437
+ }
3438
+ }
3439
+ timer.handle = setTimeout(
3440
+ () => {
3441
+ timedOut = true;
3442
+ terminateChild();
3443
+ },
3444
+ Math.max(0, opts.timeoutMs)
3445
+ );
3446
+ });
3447
+ }
3448
+
3449
+ // src/lib/opencode/session-archive-upload.ts
3450
+ var MAX_SESSION_ARCHIVE_BYTES = MAX_ARCHIVE_STDIN_BYTES;
3451
+ var EXIT_ARCHIVE_CONFIG_MISSING = 50;
3452
+ var EXIT_ARCHIVE_ACCESS_DENIED = 51;
3453
+ function transportFailure(bytes, reason) {
3454
+ const message = reason === "missing-executable" ? "runner-synchroniser is unavailable; session archival requires a hosted runner." : reason === "timeout" ? "runner-synchroniser timed out before the archive upload completed." : "session archive upload did not complete.";
3455
+ return { kind: "transport-failed", bytes, reason, message };
3456
+ }
3457
+ function isJson(body) {
3458
+ try {
3459
+ JSON.parse(body.toString("utf8"));
3460
+ return true;
3461
+ } catch (error2) {
3462
+ void error2;
3463
+ return false;
3464
+ }
3465
+ }
3466
+ async function uploadSessionArchive(key, body, options) {
3467
+ const bytes = Buffer.from(body);
3468
+ if (bytes.byteLength > MAX_SESSION_ARCHIVE_BYTES) {
3469
+ return transportFailure(bytes.byteLength, "too-large");
3470
+ }
3471
+ if (!isJson(bytes)) return transportFailure(bytes.byteLength, "invalid-json");
3472
+ const run2 = options.runSynchroniser ?? runSynchroniser;
3473
+ let result;
3474
+ try {
3475
+ result = await run2(["archive-put", key], {
3476
+ timeoutMs: options.timeoutMs,
3477
+ env: options.env,
3478
+ signal: options.signal,
3479
+ stdin: bytes
3480
+ });
3481
+ } catch (error2) {
3482
+ return transportFailure(
3483
+ bytes.byteLength,
3484
+ error2 instanceof Error && /ENOENT|runner-synchroniser/i.test(error2.message) ? "missing-executable" : "spawn-failed"
3485
+ );
3486
+ }
3487
+ if (result.timedOut) return transportFailure(bytes.byteLength, "timeout");
3488
+ if (result.code === 0) return { kind: "ok", bytes: bytes.byteLength };
3489
+ if (result.code === EXIT_ARCHIVE_CONFIG_MISSING) {
3490
+ return { kind: "config-missing", bytes: bytes.byteLength };
3491
+ }
3492
+ if (result.code === EXIT_ARCHIVE_ACCESS_DENIED) {
3493
+ return { kind: "access-denied", bytes: bytes.byteLength };
3494
+ }
3495
+ if (result.code === null && /ENOENT|runner-synchroniser/i.test(result.stderr)) {
3496
+ return transportFailure(bytes.byteLength, "missing-executable");
3497
+ }
3498
+ return transportFailure(
3499
+ bytes.byteLength,
3500
+ result.code === null ? "spawn-failed" : "process-failed"
3501
+ );
3502
+ }
3503
+
3504
+ // src/lib/opencode/session-archive-run.ts
3505
+ var SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS = 6e4;
3506
+ function updatedAt(session) {
3507
+ const updated = session.time?.updated;
3508
+ return typeof updated === "number" && Number.isFinite(updated) ? updated : -Infinity;
3509
+ }
3510
+ function sessionArchiveKey(prefix, sessionId) {
3511
+ return `${prefix.replace(/\/+$/, "")}/conversation-archives/${encodeURIComponent(sessionId)}.json`;
3512
+ }
3513
+ function failureMessage(error2) {
3514
+ return error2 instanceof Error ? error2.message : String(error2);
3515
+ }
3516
+ function isBudgetExpired(deadline, now, signal) {
3517
+ return signal?.aborted === true || now() >= deadline;
3518
+ }
3519
+ function createBudgetSignal(parent, timeoutMs) {
3520
+ const controller = new AbortController();
3521
+ const forwardAbort = () => controller.abort(parent?.reason);
3522
+ if (parent?.aborted) forwardAbort();
3523
+ else parent?.addEventListener("abort", forwardAbort, { once: true });
3524
+ const timer = setTimeout(
3525
+ () => controller.abort(new Error("session archive budget exceeded")),
3526
+ timeoutMs
3527
+ );
3528
+ return {
3529
+ signal: controller.signal,
3530
+ dispose: () => {
3531
+ clearTimeout(timer);
3532
+ parent?.removeEventListener("abort", forwardAbort);
3533
+ }
3534
+ };
3535
+ }
3536
+ async function runSessionArchive({
3537
+ sessions,
3538
+ deps,
3539
+ budgetMs,
3540
+ now = Date.now
3541
+ }) {
3542
+ const startedAt = now();
3543
+ const deadline = startedAt + Math.max(0, budgetMs);
3544
+ const summary = {
3545
+ inspected: sessions.length,
3546
+ uploaded: 0,
3547
+ unchanged: 0,
3548
+ failed: 0,
3549
+ uploadAttempts: 0,
3550
+ uploadFailures: 0,
3551
+ skippedBudget: 0,
3552
+ skippedBudgetIds: [],
3553
+ neverWatermarkedFailures: 0,
3554
+ unmappedIds: [],
3555
+ archivedSessionIds: [],
3556
+ failures: [],
3557
+ staleSince: null
3558
+ };
3559
+ const ordered = [...sessions].sort((left, right) => updatedAt(right) - updatedAt(left));
3560
+ const upload = deps.upload ?? uploadSessionArchive;
3561
+ const exportMessages = deps.exportMessages ?? exportSessionMessages;
3562
+ const writeWatermark = deps.recordWatermark ?? recordUpload;
3563
+ const failedWatermarks = [];
3564
+ const recordFailedWatermark = (watermark) => {
3565
+ if (watermark) failedWatermarks.push(watermark.uploaded_at);
3566
+ else summary.neverWatermarkedFailures++;
3567
+ };
3568
+ for (let index = 0; index < ordered.length; index++) {
3569
+ const session = ordered[index];
3570
+ const remainingMs = Math.floor(deadline - now());
3571
+ if (remainingMs <= 0 || deps.signal?.aborted) {
3572
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3573
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3574
+ break;
3575
+ }
3576
+ const conversationId = deps.bindings.get(session.id) ?? null;
3577
+ if (conversationId === null) summary.unmappedIds.push(session.id);
3578
+ const exportBudget = createBudgetSignal(deps.signal, remainingMs);
3579
+ let messages;
3580
+ try {
3581
+ messages = await exportMessages(deps.client, session.id, exportBudget.signal);
3582
+ } catch (error2) {
3583
+ if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
3584
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3585
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3586
+ break;
3587
+ }
3588
+ summary.failed++;
3589
+ const reason = error2 instanceof SessionArchiveExportError ? error2.code : "export-failed";
3590
+ summary.failures.push({
3591
+ sessionId: session.id,
3592
+ outcome: "export-failed",
3593
+ reason,
3594
+ message: failureMessage(error2),
3595
+ archived: false
3596
+ });
3597
+ recordFailedWatermark(deps.watermarks.get(session.id));
3598
+ continue;
3599
+ } finally {
3600
+ exportBudget.dispose();
3601
+ }
3602
+ if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
3603
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3604
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3605
+ break;
3606
+ }
3607
+ try {
3608
+ const archivedAt = new Date(now()).toISOString();
3609
+ const snapshot = buildSessionArchive({
3610
+ session,
3611
+ messages,
3612
+ apiVersion: deps.client.version,
3613
+ conversationId,
3614
+ bindingSource: conversationId === null ? "unmapped" : "ledger",
3615
+ runnerId: deps.runnerId,
3616
+ archivedAt
3617
+ });
3618
+ const key = sessionArchiveKey(deps.keyPrefix, session.id);
3619
+ const existing = deps.watermarks.get(session.id);
3620
+ if (existing?.fingerprint === snapshot.content_fingerprint) {
3621
+ summary.unchanged++;
3622
+ summary.archivedSessionIds.push(session.id);
3623
+ continue;
3624
+ }
3625
+ const uploadRemainingMs = Math.floor(deadline - now());
3626
+ if (uploadRemainingMs <= 0 || deps.signal?.aborted) {
3627
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3628
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3629
+ break;
3630
+ }
3631
+ summary.uploadAttempts++;
3632
+ const uploadBudget = createBudgetSignal(deps.signal, uploadRemainingMs);
3633
+ let outcome;
3634
+ try {
3635
+ outcome = await upload(key, JSON.stringify(snapshot), {
3636
+ timeoutMs: Math.min(uploadRemainingMs, SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS),
3637
+ signal: uploadBudget.signal
3638
+ });
3639
+ } finally {
3640
+ uploadBudget.dispose();
3641
+ }
3642
+ if (outcome.kind !== "ok") {
3643
+ summary.failed++;
3644
+ summary.uploadFailures++;
3645
+ const reason = "reason" in outcome ? outcome.reason : void 0;
3646
+ summary.failures.push({
3647
+ sessionId: session.id,
3648
+ outcome: outcome.kind,
3649
+ ...reason !== void 0 ? { reason } : {},
3650
+ ..."message" in outcome ? { message: outcome.message } : {},
3651
+ archived: false
3652
+ });
3653
+ recordFailedWatermark(existing);
3654
+ if (uploadBudget.signal.aborted) {
3655
+ summary.skippedBudgetIds.push(...ordered.slice(index + 1).map(({ id }) => id));
3656
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3657
+ break;
3658
+ }
3659
+ continue;
3660
+ }
3661
+ summary.uploaded++;
3662
+ summary.archivedSessionIds.push(session.id);
3663
+ const watermark = {
3664
+ fingerprint: snapshot.content_fingerprint,
3665
+ uploaded_at: new Date(now()).toISOString(),
3666
+ key
3667
+ };
3668
+ try {
3669
+ writeWatermark(deps.watermarkPath, session.id, watermark, deps.watermarks);
3670
+ } catch (error2) {
3671
+ summary.failures.push({
3672
+ sessionId: session.id,
3673
+ outcome: "watermark-write-failed",
3674
+ message: failureMessage(error2),
3675
+ archived: true
3676
+ });
3677
+ }
3678
+ } catch (error2) {
3679
+ if (isBudgetExpired(deadline, now, deps.signal)) {
3680
+ summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
3681
+ summary.skippedBudget = summary.skippedBudgetIds.length;
3682
+ break;
3683
+ }
3684
+ summary.failed++;
3685
+ summary.failures.push({
3686
+ sessionId: session.id,
3687
+ outcome: "archive-failed",
3688
+ message: failureMessage(error2),
3689
+ archived: false
3690
+ });
3691
+ recordFailedWatermark(deps.watermarks.get(session.id));
3692
+ }
3693
+ }
3694
+ summary.staleSince = failedWatermarks.sort()[0] ?? null;
3695
+ return summary;
3696
+ }
3697
+
3698
+ // src/lib/opencode/auth.ts
3699
+ function buildOpenCodeBasicAuthHeader(password) {
3700
+ return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
3701
+ }
3702
+
3703
+ // src/lib/opencode/health.ts
3704
+ async function checkOpenCodeHealth(port) {
3705
+ try {
3706
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
3707
+ signal: AbortSignal.timeout(2e3)
3708
+ // 2 second timeout
3709
+ });
3710
+ if (response.status === 401) {
3711
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
3712
+ }
3713
+ if (!response.ok) {
3714
+ return { healthy: false, error: `HTTP ${response.status}` };
3715
+ }
3716
+ const body = await response.text();
3717
+ let data;
3718
+ try {
3719
+ data = JSON.parse(body);
3720
+ } catch (error2) {
3721
+ return {
3722
+ healthy: false,
3723
+ okWithNonJsonBody: true,
3724
+ error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
3725
+ };
3726
+ }
3727
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
3728
+ return {
3729
+ healthy: false,
3730
+ okWithNonJsonBody: true,
3731
+ error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
3732
+ };
3733
+ }
3734
+ const version2 = data.version;
3735
+ return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
3736
+ } catch (error2) {
3737
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
3738
+ return { healthy: false, error: message };
3739
+ }
3740
+ }
3741
+ function bodyExcerpt(body) {
3742
+ const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
3743
+ return excerpt ? ` (body="${excerpt}")` : " (empty body)";
3744
+ }
3745
+ async function checkOpenCode2Health(port, password) {
3746
+ try {
3747
+ const headers = {};
3748
+ if (password !== void 0) {
3749
+ headers.Authorization = buildOpenCodeBasicAuthHeader(password);
3750
+ }
3751
+ let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
3752
+ headers,
3753
+ signal: AbortSignal.timeout(2e3)
3754
+ });
3755
+ const usesHealthFallback = response.status === 404;
3756
+ if (usesHealthFallback) {
3757
+ response = await fetch(`http://127.0.0.1:${port}/api/health`, {
3758
+ headers,
3759
+ signal: AbortSignal.timeout(2e3)
3760
+ });
3761
+ }
3762
+ if (response.status === 401) {
3763
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
3764
+ }
3765
+ if (!response.ok) {
3766
+ return { healthy: false, error: `HTTP ${response.status}` };
3767
+ }
3768
+ const body = await response.text();
3769
+ const excerpt = bodyExcerpt(body);
3770
+ let data;
3771
+ try {
3772
+ data = JSON.parse(body);
3773
+ } catch (error2) {
3774
+ return {
3775
+ healthy: false,
3776
+ error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
3777
+ };
3778
+ }
3779
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
3780
+ return {
3781
+ healthy: false,
3782
+ error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
3783
+ };
3784
+ }
3785
+ const bodyObject = data;
3786
+ if (usesHealthFallback && bodyObject.healthy !== true) {
3787
+ return {
3788
+ healthy: false,
3789
+ error: `Invalid health response${excerpt}: expected healthy=true`
3790
+ };
3791
+ }
3792
+ if (!usesHealthFallback) {
3793
+ return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
3794
+ healthy: false,
3795
+ error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
3796
+ };
3797
+ }
3798
+ return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
3799
+ } catch (error2) {
3800
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
3801
+ return { healthy: false, error: message };
3802
+ }
3803
+ }
3804
+ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
3805
+ const startTime = Date.now();
3806
+ while (Date.now() - startTime < timeoutMs) {
3807
+ const health = await checkOpenCodeHealth(port);
3808
+ if (health.healthy || health.authFailed) {
3809
+ return health;
3810
+ }
3811
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
3812
+ }
3813
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
3814
+ }
3815
+ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
3816
+ const startTime = Date.now();
3817
+ while (Date.now() - startTime < timeoutMs) {
3818
+ const health = await checkOpenCode2Health(port, password);
3819
+ if (health.healthy || health.authFailed) {
3820
+ return health;
3821
+ }
3822
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
3823
+ }
3824
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
3825
+ }
3826
+
3827
+ // src/lib/http-timeout.ts
3828
+ var REQUEST_TIMEOUT_MS = 6e4;
3829
+ var RequestTimeoutError = class extends Error {
3830
+ constructor(operation, timeoutMs, elapsedMs) {
3831
+ super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
3832
+ this.operation = operation;
3833
+ this.timeoutMs = timeoutMs;
3834
+ this.elapsedMs = elapsedMs;
3835
+ this.name = "TimeoutError";
3836
+ }
3837
+ };
3838
+ function describeRequestOperation(input, init) {
3839
+ const method = (init?.method ?? input.method ?? "GET").toUpperCase();
3840
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
3841
+ let path;
3842
+ try {
3843
+ path = new URL(rawUrl).pathname;
3844
+ } catch (error2) {
3845
+ if (!(error2 instanceof TypeError)) throw error2;
3846
+ path = rawUrl.split(/[?#]/, 1)[0];
3847
+ }
3848
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3849
+ const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
3850
+ const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
3851
+ return `${method} ${maskedPath}`;
3852
+ }
3853
+ function withRequestTimeout(fetchImpl, timeoutMs) {
3854
+ return (async (input, init) => {
3855
+ const startedAt = Date.now();
3856
+ const controller = new AbortController();
3857
+ const abortFromCaller = () => controller.abort(init?.signal?.reason);
3858
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
3859
+ const abortFromTimeout = () => controller.abort(timeoutSignal.reason);
3860
+ if (init?.signal?.aborted) abortFromCaller();
3861
+ else init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
3862
+ if (timeoutSignal.aborted) abortFromTimeout();
3863
+ else timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
3864
+ try {
3865
+ return await fetchImpl(input, { ...init, signal: controller.signal });
3866
+ } catch (error2) {
3867
+ if (timeoutSignal.aborted) {
3868
+ throw new RequestTimeoutError(
3869
+ describeRequestOperation(input, init),
3870
+ timeoutMs,
3871
+ Date.now() - startedAt
3872
+ );
3873
+ }
3874
+ throw error2;
3875
+ } finally {
3876
+ init?.signal?.removeEventListener("abort", abortFromCaller);
3877
+ timeoutSignal.removeEventListener("abort", abortFromTimeout);
3878
+ }
3879
+ });
3880
+ }
3881
+
3882
+ // src/lib/opencode/client.ts
3883
+ function redactPassword(message, password) {
3884
+ return message.replaceAll(password, "[redacted]");
3885
+ }
3886
+ function createOpenCodeClient(options) {
3887
+ const password = options.password ?? null;
3888
+ const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
3889
+ const baseUrl = `http://127.0.0.1:${options.port}`;
3890
+ return {
3891
+ port: options.port,
3892
+ version: options.version,
3893
+ password,
3894
+ async request(path, init, requestOptions) {
3895
+ const requestInit = options.version === "v2" && password !== null ? (() => {
3896
+ const headers = new Headers(init?.headers);
3897
+ headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
3898
+ return { ...init, headers };
3899
+ })() : init;
3900
+ try {
3901
+ const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
3902
+ if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
3903
+ const body = await response.text();
3904
+ throw new Error(
3905
+ `OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
3906
+ );
3907
+ }
3908
+ return response;
3909
+ } catch (error2) {
3910
+ if (options.version === "v2" && password !== null) {
3911
+ const message = error2 instanceof Error ? error2.message : String(error2);
3912
+ throw new Error(redactPassword(message, password));
3913
+ }
3914
+ throw error2;
3915
+ }
3916
+ }
3917
+ };
3918
+ }
3919
+
3920
+ // src/lib/opencode/session.ts
3921
+ var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
3922
+ function timedFetch(input, init) {
3923
+ return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
3924
+ }
3925
+ function requestWithClient(port, client, path, init, options) {
3926
+ return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
3927
+ }
3928
+ function opencodeBase(port) {
3929
+ return `http://127.0.0.1:${port}`;
3930
+ }
3931
+ async function getOpenCodeDirectory(port, client) {
3932
+ try {
3933
+ const res = await requestWithClient(port, client, "/path");
3934
+ if (!res.ok) return null;
3935
+ const body = await res.json();
3936
+ const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
3937
+ return dir && dir.trim() ? dir.trim() : null;
3938
+ } catch (error2) {
3939
+ console.error(
3940
+ `[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3941
+ );
3942
+ return null;
3943
+ }
3944
+ }
3945
+ function roleOf2(m) {
3946
+ if (!m || typeof m !== "object") return void 0;
3947
+ if (typeof m.role === "string") return m.role;
3948
+ const infoRole = m.info?.role;
3949
+ return typeof infoRole === "string" ? infoRole : void 0;
3950
+ }
3951
+ function completedOf(m) {
3952
+ if (!m || typeof m !== "object") return void 0;
3953
+ return m.info?.time?.completed ?? m.time?.completed;
3954
+ }
3955
+ function createdOf(m) {
3956
+ if (!m || typeof m !== "object") return void 0;
3957
+ return m.info?.time?.created ?? m.time?.created;
3958
+ }
3959
+ function idOf2(m) {
3960
+ if (!m || typeof m !== "object") return void 0;
3961
+ if (typeof m.id === "string") return m.id;
3962
+ const infoId = m.info?.id;
3963
+ return typeof infoId === "string" ? infoId : void 0;
3964
+ }
3965
+ function parentIdOf2(m) {
3966
+ if (!m || typeof m !== "object") return void 0;
3967
+ if (typeof m.parentID === "string") return m.parentID;
3968
+ const infoParent = m.info?.parentID;
3969
+ return typeof infoParent === "string" ? infoParent : void 0;
3970
+ }
3971
+ function finishOf(m) {
3972
+ if (!m || typeof m !== "object") return void 0;
3973
+ if (typeof m.finish === "string") return m.finish;
3974
+ const infoFinish = m.info?.finish;
3975
+ return typeof infoFinish === "string" ? infoFinish : void 0;
3976
+ }
3977
+ function errorOf(m) {
3978
+ if (!m || typeof m !== "object") return void 0;
3979
+ return m.info?.error ?? m.error;
3980
+ }
3981
+ function isTerminalStopAnswer(m) {
3982
+ return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
3983
+ }
3984
+ function isAssistantInFlight(m) {
3985
+ if (completedOf(m) == null) return true;
3986
+ if (errorOf(m) != null) return false;
3987
+ return finishOf(m) === "tool-calls";
3988
+ }
3989
+ async function getSessionMessages(port, sessionId, client) {
2891
3990
  try {
2892
3991
  const path = `/session/${sessionId}/message`;
2893
3992
  const res = await requestWithClient(port, client, path);
@@ -2959,28 +4058,38 @@ function sessionListPath(options) {
2959
4058
  async function listSessionPage(port, client, options) {
2960
4059
  const path = sessionListPath(options);
2961
4060
  try {
2962
- const res = await requestWithClient(port, client, path);
2963
- if (!res.ok) return null;
4061
+ const res = await requestWithClient(
4062
+ port,
4063
+ client,
4064
+ path,
4065
+ options?.signal ? { signal: options.signal } : void 0,
4066
+ { allowStatuses: ALL_HTTP_STATUSES }
4067
+ );
4068
+ if (!res.ok) {
4069
+ console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
4070
+ return null;
4071
+ }
2964
4072
  const body = await res.json();
2965
4073
  return Array.isArray(body) ? body : null;
2966
- } catch (error2) {
2967
- console.error(
2968
- `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2969
- );
4074
+ } catch (error2) {
4075
+ if (options?.signal?.aborted) return null;
4076
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4077
+ console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
2970
4078
  return null;
2971
4079
  }
2972
4080
  }
2973
4081
  async function listSessions(port, client, options) {
2974
- if (client?.version === "v2") return listV2Sessions(client);
4082
+ if (client?.version === "v2") return listV2Sessions(client, options?.signal);
2975
4083
  return listSessionPage(port, client, options);
2976
4084
  }
2977
- async function listAllSessions(port, client) {
4085
+ async function listAllSessions(port, client, signal) {
2978
4086
  if (client?.version === "v2") {
2979
- const sessions2 = await listV2Sessions(client);
4087
+ const sessions2 = await listV2Sessions(client, signal);
2980
4088
  return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
2981
4089
  }
2982
4090
  const sessions = await listSessionPage(port, client, {
2983
- limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
4091
+ limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
4092
+ signal
2984
4093
  });
2985
4094
  if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
2986
4095
  if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
@@ -3669,9 +4778,17 @@ function isPreamblePinnedRunning(messages, userMessageId) {
3669
4778
  const reply = findLastAssistantReplyFor(messages, userMessageId);
3670
4779
  return completedOf(reply) != null && finishOf(reply) === "tool-calls";
3671
4780
  }
4781
+ function isMidFlightPinnedRunning(messages, userMessageId) {
4782
+ if (messageRunState(messages, userMessageId) !== "running") return false;
4783
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
4784
+ return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
4785
+ }
3672
4786
  function isB2AbandonmentConfirmed(params) {
3673
4787
  return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
3674
4788
  }
4789
+ function isMidFlightOrphanConfirmed(params) {
4790
+ return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
4791
+ }
3675
4792
  function isAmbiguousTerminalFinish(m) {
3676
4793
  if (completedOf(m) == null) return false;
3677
4794
  if (errorOf(m) != null) return false;
@@ -3773,8 +4890,8 @@ function classifyReplyAuthError(reply) {
3773
4890
  };
3774
4891
  }
3775
4892
  }
3776
- const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3777
- if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
4893
+ const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
4894
+ if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
3778
4895
  return {
3779
4896
  kind: "model_auth",
3780
4897
  providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
@@ -4045,14 +5162,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
4045
5162
  }
4046
5163
 
4047
5164
  // src/lib/opencode/session-v2.ts
4048
- function isRecord(value) {
5165
+ var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
5166
+ function isRecord3(value) {
4049
5167
  return typeof value === "object" && value !== null && !Array.isArray(value);
4050
5168
  }
4051
5169
  function finiteNumber2(value) {
4052
5170
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4053
5171
  }
4054
5172
  function adaptTime(value) {
4055
- if (!isRecord(value)) return void 0;
5173
+ if (!isRecord3(value)) return void 0;
4056
5174
  const created = finiteNumber2(value.created);
4057
5175
  const completed = finiteNumber2(value.completed);
4058
5176
  if (created === void 0 && completed === void 0) return void 0;
@@ -4062,11 +5180,11 @@ function adaptTime(value) {
4062
5180
  };
4063
5181
  }
4064
5182
  function adaptTokens(value) {
4065
- if (!isRecord(value)) return void 0;
5183
+ if (!isRecord3(value)) return void 0;
4066
5184
  const input = finiteNumber2(value.input);
4067
5185
  const output = finiteNumber2(value.output);
4068
5186
  const reasoning = finiteNumber2(value.reasoning);
4069
- const cache = isRecord(value.cache) ? {
5187
+ const cache = isRecord3(value.cache) ? {
4070
5188
  ...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
4071
5189
  ...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
4072
5190
  } : void 0;
@@ -4090,7 +5208,7 @@ function adaptMessageInfo(value, role) {
4090
5208
  if (typeof value.finish === "string") info.finish = value.finish;
4091
5209
  if ("error" in value) info.error = value.error;
4092
5210
  if (typeof value.agent === "string") info.agent = value.agent;
4093
- if (isRecord(value.model)) {
5211
+ if (isRecord3(value.model)) {
4094
5212
  if (typeof value.model.id === "string") info.modelID = value.model.id;
4095
5213
  if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
4096
5214
  }
@@ -4100,7 +5218,7 @@ function adaptMessageInfo(value, role) {
4100
5218
  return info;
4101
5219
  }
4102
5220
  function adaptV2Message(value) {
4103
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") {
5221
+ if (!isRecord3(value) || typeof value.id !== "string" || typeof value.type !== "string") {
4104
5222
  return null;
4105
5223
  }
4106
5224
  if (value.type === "user") {
@@ -4113,7 +5231,7 @@ function adaptV2Message(value) {
4113
5231
  if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
4114
5232
  const parts = [];
4115
5233
  for (const content of value.content) {
4116
- if (!isRecord(content) || typeof content.type !== "string") return null;
5234
+ if (!isRecord3(content) || typeof content.type !== "string") return null;
4117
5235
  if (content.type === "text") {
4118
5236
  if (typeof content.text !== "string") return null;
4119
5237
  parts.push({ type: "text", text: content.text });
@@ -4127,23 +5245,23 @@ function adaptV2Message(value) {
4127
5245
  };
4128
5246
  }
4129
5247
  function adaptFormTool(value) {
4130
- if (!isRecord(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
5248
+ if (!isRecord3(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
4131
5249
  return void 0;
4132
5250
  }
4133
5251
  return { messageID: value.messageID, callID: value.id };
4134
5252
  }
4135
5253
  function adaptV2FormWire(value) {
4136
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5254
+ if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
4137
5255
  return null;
4138
5256
  }
4139
5257
  return value;
4140
5258
  }
4141
5259
  function adaptV2FormField(value, header) {
4142
- if (!isRecord(value)) return null;
5260
+ if (!isRecord3(value)) return null;
4143
5261
  const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
4144
5262
  if (!question) return null;
4145
5263
  const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
4146
- if (!isRecord(option)) return [];
5264
+ if (!isRecord3(option)) return [];
4147
5265
  const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
4148
5266
  if (!label) return [];
4149
5267
  return [
@@ -4161,7 +5279,7 @@ function adaptV2Form(value) {
4161
5279
  const header = typeof form.title === "string" ? form.title : "";
4162
5280
  const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
4163
5281
  if (questions.length === 0) return null;
4164
- const tool = isRecord(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
5282
+ const tool = isRecord3(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
4165
5283
  return {
4166
5284
  id: form.id,
4167
5285
  sessionID: form.sessionID,
@@ -4171,7 +5289,7 @@ function adaptV2Form(value) {
4171
5289
  };
4172
5290
  }
4173
5291
  function adaptV2FormList(value) {
4174
- if (!isRecord(value) || !Array.isArray(value.data)) return null;
5292
+ if (!isRecord3(value) || !Array.isArray(value.data)) return null;
4175
5293
  return value.data.map(adaptV2Form).filter((form) => form !== null);
4176
5294
  }
4177
5295
  function adaptPattern(value) {
@@ -4182,7 +5300,7 @@ function adaptPattern(value) {
4182
5300
  return void 0;
4183
5301
  }
4184
5302
  function adaptV2PermissionWire(value) {
4185
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
5303
+ if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
4186
5304
  return null;
4187
5305
  }
4188
5306
  if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
@@ -4194,12 +5312,12 @@ function adaptV2Permission(value) {
4194
5312
  const type = permission.permission ?? permission.action;
4195
5313
  if (!type) return null;
4196
5314
  const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
4197
- const time = isRecord(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
5315
+ const time = isRecord3(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
4198
5316
  return {
4199
5317
  id: permission.id,
4200
5318
  type,
4201
5319
  sessionID: permission.sessionID,
4202
- metadata: isRecord(permission.metadata) ? permission.metadata : {},
5320
+ metadata: isRecord3(permission.metadata) ? permission.metadata : {},
4203
5321
  raw: permission,
4204
5322
  ...pattern !== void 0 ? { pattern } : {},
4205
5323
  ...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
@@ -4209,12 +5327,12 @@ function adaptV2Permission(value) {
4209
5327
  };
4210
5328
  }
4211
5329
  function adaptV2PermissionList(value) {
4212
- if (!isRecord(value) || !Array.isArray(value.data)) return null;
5330
+ if (!isRecord3(value) || !Array.isArray(value.data)) return null;
4213
5331
  return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
4214
5332
  }
4215
5333
  function adaptV2Session(value) {
4216
- if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
4217
- const time = isRecord(value.time) ? {
5334
+ if (!isRecord3(value) || typeof value.id !== "string" || value.id.length === 0) return null;
5335
+ const time = isRecord3(value.time) ? {
4218
5336
  ...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
4219
5337
  ...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
4220
5338
  } : void 0;
@@ -4226,7 +5344,7 @@ function adaptV2Session(value) {
4226
5344
  };
4227
5345
  }
4228
5346
  function adaptV2SessionList(value) {
4229
- if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.cursor)) return null;
5347
+ if (!isRecord3(value) || !Array.isArray(value.data) || !isRecord3(value.cursor)) return null;
4230
5348
  return {
4231
5349
  data: value.data.map(adaptV2Session).filter((session) => session !== null),
4232
5350
  cursor: value.cursor
@@ -4235,18 +5353,18 @@ function adaptV2SessionList(value) {
4235
5353
  function adaptV2Location(value) {
4236
5354
  const candidates = [
4237
5355
  value,
4238
- isRecord(value) ? value.data : void 0,
4239
- isRecord(value) ? value.location : void 0
5356
+ isRecord3(value) ? value.data : void 0,
5357
+ isRecord3(value) ? value.location : void 0
4240
5358
  ];
4241
5359
  for (const candidate of candidates) {
4242
- if (!isRecord(candidate) || typeof candidate.directory !== "string") continue;
5360
+ if (!isRecord3(candidate) || typeof candidate.directory !== "string") continue;
4243
5361
  const directory = candidate.directory.trim();
4244
5362
  if (directory) return directory;
4245
5363
  }
4246
5364
  return null;
4247
5365
  }
4248
5366
  function adaptV2Messages(value) {
4249
- if (!isRecord(value) || !Array.isArray(value.data)) return [];
5367
+ if (!isRecord3(value) || !Array.isArray(value.data)) return [];
4250
5368
  return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
4251
5369
  }
4252
5370
  async function readJson(response) {
@@ -4261,7 +5379,7 @@ async function readJson(response) {
4261
5379
  async function readData(client, path, init) {
4262
5380
  const response = await client.request(path, init);
4263
5381
  const body = await readJson(response);
4264
- if (!isRecord(body) || !("data" in body)) {
5382
+ if (!isRecord3(body) || !("data" in body)) {
4265
5383
  throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
4266
5384
  }
4267
5385
  return body.data;
@@ -4288,7 +5406,7 @@ async function createV2Session(client, directory) {
4288
5406
  headers: { "Content-Type": "application/json" },
4289
5407
  body: JSON.stringify({ location: { directory } })
4290
5408
  });
4291
- if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
5409
+ if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
4292
5410
  throw new Error("OpenCode V2 create session response was missing data.id");
4293
5411
  }
4294
5412
  return data.id;
@@ -4299,25 +5417,35 @@ async function getV2Session(client, sessionId) {
4299
5417
  if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
4300
5418
  return session;
4301
5419
  }
4302
- async function listV2SessionPage(client, cursor) {
5420
+ async function listV2SessionPage(client, cursor, signal) {
5421
+ const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
4303
5422
  const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
4304
5423
  try {
4305
- return adaptV2SessionList(await readJson(await client.request(path)));
5424
+ const response = await client.request(path, signal ? { signal } : void 0, {
5425
+ allowStatuses: ALL_HTTP_STATUSES2
5426
+ });
5427
+ if (!response.ok) {
5428
+ console.error(
5429
+ `[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
5430
+ );
5431
+ return null;
5432
+ }
5433
+ return adaptV2SessionList(await readJson(response));
4306
5434
  } catch (error2) {
4307
- console.error(
4308
- `[listV2SessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
4309
- );
5435
+ if (signal?.aborted) return null;
5436
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
5437
+ console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
4310
5438
  return null;
4311
5439
  }
4312
5440
  }
4313
- async function listV2Sessions(client) {
5441
+ async function listV2Sessions(client, signal) {
4314
5442
  const sessions = [];
4315
5443
  const seenCursors = /* @__PURE__ */ new Set();
4316
5444
  let cursor;
4317
5445
  let hasNextPage = true;
4318
5446
  try {
4319
5447
  while (hasNextPage) {
4320
- const page = await listV2SessionPage(client, cursor);
5448
+ const page = await listV2SessionPage(client, cursor, signal);
4321
5449
  if (!page) return null;
4322
5450
  sessions.push(...page.data);
4323
5451
  const next = page.cursor.next;
@@ -4333,8 +5461,9 @@ async function listV2Sessions(client) {
4333
5461
  }
4334
5462
  return sessions;
4335
5463
  } catch (error2) {
5464
+ const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
4336
5465
  console.error(
4337
- `[listV2Sessions] session pagination failed: ${error2 instanceof Error ? error2.message : String(error2)}`
5466
+ `[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
4338
5467
  );
4339
5468
  return null;
4340
5469
  }
@@ -4344,7 +5473,7 @@ async function deleteV2Session(client, sessionId) {
4344
5473
  const response = await client.request(
4345
5474
  `/api/session/${encodeURIComponent(sessionId)}`,
4346
5475
  { method: "DELETE" },
4347
- { allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
5476
+ { allowStatuses: ALL_HTTP_STATUSES2 }
4348
5477
  );
4349
5478
  if (response.status >= 200 && response.status < 300) {
4350
5479
  return { ok: true, status: response.status };
@@ -4395,8 +5524,8 @@ async function sendV2Prompt(client, sessionId, text) {
4395
5524
  `OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
4396
5525
  );
4397
5526
  }
4398
- const data = isRecord(body) && "data" in body ? body.data : void 0;
4399
- if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
5527
+ const data = isRecord3(body) && "data" in body ? body.data : void 0;
5528
+ if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
4400
5529
  throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
4401
5530
  }
4402
5531
  return data.id;
@@ -4405,7 +5534,7 @@ async function getV2SessionMessages(client, sessionId) {
4405
5534
  const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
4406
5535
  try {
4407
5536
  const body = await readJson(await client.request(path));
4408
- if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) return null;
5537
+ if (!isRecord3(body) || !Array.isArray(body.data) || !isRecord3(body.cursor)) return null;
4409
5538
  return adaptV2Messages(body);
4410
5539
  } catch (error2) {
4411
5540
  console.error(
@@ -4444,7 +5573,7 @@ async function interruptV2Session(client, sessionId) {
4444
5573
  async function getV2ActiveSessions(client) {
4445
5574
  try {
4446
5575
  const body = await readJson(await client.request("/api/session/active"));
4447
- if (!isRecord(body) || !isRecord(body.data)) return null;
5576
+ if (!isRecord3(body) || !isRecord3(body.data)) return null;
4448
5577
  return body.data;
4449
5578
  } catch (error2) {
4450
5579
  console.error(
@@ -4460,8 +5589,8 @@ async function isV2SessionOngoing(client, sessionId) {
4460
5589
  }
4461
5590
  function sessionErrorReason2(value) {
4462
5591
  if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
4463
- if (isRecord(value)) {
4464
- const data = isRecord(value.data) ? value.data : void 0;
5592
+ if (isRecord3(value)) {
5593
+ const data = isRecord3(value.data) ? value.data : void 0;
4465
5594
  const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
4466
5595
  if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
4467
5596
  }
@@ -4477,17 +5606,17 @@ function adaptV2SessionErrorEvent(value) {
4477
5606
  return null;
4478
5607
  }
4479
5608
  }
4480
- if (!isRecord(parsed)) return null;
5609
+ if (!isRecord3(parsed)) return null;
4481
5610
  try {
4482
5611
  const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
4483
5612
  if (establishedShape) return establishedShape;
4484
5613
  } catch (error2) {
4485
5614
  void error2;
4486
5615
  }
4487
- const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord);
5616
+ const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord3);
4488
5617
  for (const event of candidates) {
4489
5618
  if (event.type !== "session.error") continue;
4490
- const properties = [event.properties, event.data, event].find(isRecord);
5619
+ const properties = [event.properties, event.data, event].find(isRecord3);
4491
5620
  if (!properties) continue;
4492
5621
  const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
4493
5622
  if (!sessionId) continue;
@@ -4537,13 +5666,13 @@ async function readV2SessionErrorStream(client, options) {
4537
5666
  }
4538
5667
 
4539
5668
  // src/lib/opencode/credential-import-v2.ts
4540
- import { readFileSync as readFileSync4 } from "node:fs";
4541
- import { join as join5 } from "node:path";
5669
+ import { readFileSync as readFileSync8 } from "node:fs";
5670
+ import { join as join8 } from "node:path";
4542
5671
 
4543
5672
  // src/lib/openai-usage.ts
4544
- import { readFileSync as readFileSync3 } from "node:fs";
5673
+ import { readFileSync as readFileSync7 } from "node:fs";
4545
5674
  import { homedir as homedir3 } from "node:os";
4546
- import { join as join4 } from "node:path";
5675
+ import { join as join7 } from "node:path";
4547
5676
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4548
5677
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4549
5678
  var OpenAiUsageError = class extends Error {
@@ -4557,7 +5686,7 @@ function isLocalCredentialProblem2(err) {
4557
5686
  }
4558
5687
  function readOpenCodeChatGptCredentials() {
4559
5688
  try {
4560
- const raw = readFileSync3(join4(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
5689
+ const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4561
5690
  let parsed;
4562
5691
  try {
4563
5692
  parsed = JSON.parse(raw);
@@ -4623,7 +5752,7 @@ function parseCodexUsageHeaders(headers) {
4623
5752
  function normalizeProbeModel(model) {
4624
5753
  return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
4625
5754
  }
4626
- function isRecord2(value) {
5755
+ function isRecord4(value) {
4627
5756
  return typeof value === "object" && value !== null && !Array.isArray(value);
4628
5757
  }
4629
5758
  function unsupportedProbeModels(reason, port) {
@@ -4634,14 +5763,14 @@ async function resolveV1ProbeModels(client, port) {
4634
5763
  try {
4635
5764
  const response = await client.request("/config/providers");
4636
5765
  const body = await response.json();
4637
- if (!isRecord2(body) || !Array.isArray(body.providers)) {
5766
+ if (!isRecord4(body) || !Array.isArray(body.providers)) {
4638
5767
  return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
4639
5768
  }
4640
5769
  const provider = body.providers.find(
4641
- (candidate) => isRecord2(candidate) && candidate.id === "openai"
5770
+ (candidate) => isRecord4(candidate) && candidate.id === "openai"
4642
5771
  );
4643
- if (!provider || !isRecord2(provider.models)) return { status: "supported", models: [] };
4644
- const defaults2 = isRecord2(body.default) ? body.default : void 0;
5772
+ if (!provider || !isRecord4(provider.models)) return { status: "supported", models: [] };
5773
+ const defaults2 = isRecord4(body.default) ? body.default : void 0;
4645
5774
  const candidates = [
4646
5775
  ...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
4647
5776
  ...Object.keys(provider.models)
@@ -4663,14 +5792,14 @@ async function resolveV2ProbeModels(client, port) {
4663
5792
  try {
4664
5793
  const response = await client.request(path);
4665
5794
  const body = await response.json();
4666
- if (!isRecord2(body) || !Array.isArray(body.data)) {
5795
+ if (!isRecord4(body) || !Array.isArray(body.data)) {
4667
5796
  return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
4668
5797
  }
4669
5798
  const provider = body.data.find(
4670
- (candidate) => isRecord2(candidate) && candidate.id === "openai"
5799
+ (candidate) => isRecord4(candidate) && candidate.id === "openai"
4671
5800
  );
4672
5801
  if (!provider) return { status: "supported", models: [] };
4673
- if (!isRecord2(provider.models)) {
5802
+ if (!isRecord4(provider.models)) {
4674
5803
  return unsupportedProbeModels(
4675
5804
  "V2 provider response has no safe OpenAI model catalogue",
4676
5805
  port
@@ -4780,7 +5909,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
4780
5909
  var CONNECT_KEY_RETRY_DELAY_MS = 250;
4781
5910
  var INTEGRATION_LIST_MAX_READS = 10;
4782
5911
  var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
4783
- function isRecord3(value) {
5912
+ function isRecord5(value) {
4784
5913
  return typeof value === "object" && value !== null && !Array.isArray(value);
4785
5914
  }
4786
5915
  var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
@@ -4789,14 +5918,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
4789
5918
  }
4790
5919
  function readOpenCodeCredentials(homeDir, log3) {
4791
5920
  try {
4792
- const raw = readFileSync4(join5(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
5921
+ const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4793
5922
  let parsed;
4794
5923
  try {
4795
5924
  parsed = JSON.parse(raw);
4796
5925
  } catch (error2) {
4797
5926
  return null;
4798
5927
  }
4799
- return isRecord3(parsed) ? parsed : null;
5928
+ return isRecord5(parsed) ? parsed : null;
4800
5929
  } catch (error2) {
4801
5930
  const code = error2.code;
4802
5931
  if (code !== "ENOENT" && code !== "ENOTDIR") {
@@ -4809,7 +5938,7 @@ function classifyOpenCodeCredentials(credentials2) {
4809
5938
  const importable = [];
4810
5939
  const unsupported = [];
4811
5940
  for (const [integrationId, value] of Object.entries(credentials2)) {
4812
- if (!isRecord3(value) || typeof value.type !== "string") {
5941
+ if (!isRecord5(value) || typeof value.type !== "string") {
4813
5942
  unsupported.push({ integrationId, reason: "unrecognised credential entry" });
4814
5943
  continue;
4815
5944
  }
@@ -4824,7 +5953,7 @@ function classifyOpenCodeCredentials(credentials2) {
4824
5953
  unsupported.push({ integrationId, reason: "unrecognised credential entry" });
4825
5954
  continue;
4826
5955
  }
4827
- if (value.metadata === void 0 || isRecord3(value.metadata) && Object.keys(value.metadata).length === 0) {
5956
+ if (value.metadata === void 0 || isRecord5(value.metadata) && Object.keys(value.metadata).length === 0) {
4828
5957
  importable.push({ integrationId, key: value.key });
4829
5958
  } else {
4830
5959
  unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
@@ -4837,7 +5966,7 @@ function statusFromRequestError(error2) {
4837
5966
  const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
4838
5967
  return match ? Number(match[1]) : void 0;
4839
5968
  }
4840
- function failureMessage(status2) {
5969
+ function failureMessage2(status2) {
4841
5970
  return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
4842
5971
  }
4843
5972
  function isRetryableStatus(status2) {
@@ -4864,18 +5993,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
4864
5993
  const failure = {
4865
5994
  integrationId: credential.integrationId,
4866
5995
  status: response.status,
4867
- message: failureMessage(response.status)
5996
+ message: failureMessage2(response.status)
4868
5997
  };
4869
- log3(`${failureMessage(failure.status)} for ${failure.integrationId}`, "warn");
5998
+ log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
4870
5999
  return { ok: false, failure };
4871
6000
  } catch (error2) {
4872
6001
  const status2 = statusFromRequestError(error2);
4873
6002
  const failure = {
4874
6003
  integrationId: credential.integrationId,
4875
6004
  ...status2 === void 0 ? {} : { status: status2 },
4876
- message: failureMessage(status2)
6005
+ message: failureMessage2(status2)
4877
6006
  };
4878
- log3(`${failureMessage(status2)} for ${failure.integrationId}`, "warn");
6007
+ log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
4879
6008
  return { ok: false, failure };
4880
6009
  }
4881
6010
  }
@@ -4901,10 +6030,10 @@ async function importOpenCodeCredentials({
4901
6030
  return result;
4902
6031
  }
4903
6032
  function parseIntegrationList(value) {
4904
- if (!isRecord3(value) || !Array.isArray(value.data)) return null;
6033
+ if (!isRecord5(value) || !Array.isArray(value.data)) return null;
4905
6034
  const integrationIds = /* @__PURE__ */ new Set();
4906
6035
  for (const integration of value.data) {
4907
- if (!isRecord3(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
6036
+ if (!isRecord5(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
4908
6037
  return null;
4909
6038
  }
4910
6039
  if (integration.connections.length > 0) integrationIds.add(integration.id);
@@ -4989,87 +6118,9 @@ async function importOpenCodeV2Credentials({
4989
6118
 
4990
6119
  // src/lib/opencode/session-db-boot.ts
4991
6120
  import { spawn as spawn2 } from "node:child_process";
4992
- import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
6121
+ import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
4993
6122
  import { homedir as homedir4 } from "node:os";
4994
- import { dirname as dirname4, resolve as resolvePath } from "node:path";
4995
-
4996
- // src/lib/runner-synchroniser.ts
4997
- import { spawn } from "node:child_process";
4998
- function appendError(stderr, error2) {
4999
- const message = error2 instanceof Error ? error2.message : String(error2);
5000
- return stderr === "" ? message : `${stderr}
5001
- ${message}`;
5002
- }
5003
- function runSynchroniser(args, opts) {
5004
- return new Promise((resolve5) => {
5005
- let child;
5006
- let stdout = "";
5007
- let stderr = "";
5008
- let settled = false;
5009
- const timer = {};
5010
- let abortListener;
5011
- let spawnListener;
5012
- const finish = (result) => {
5013
- if (settled) return;
5014
- settled = true;
5015
- if (timer.handle) clearTimeout(timer.handle);
5016
- if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
5017
- if (spawnListener) child.removeListener("spawn", spawnListener);
5018
- resolve5(result);
5019
- };
5020
- try {
5021
- child = spawn("runner-synchroniser", args, {
5022
- env: opts.env ?? process.env,
5023
- stdio: ["ignore", "pipe", "pipe"]
5024
- });
5025
- } catch (error2) {
5026
- finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
5027
- return;
5028
- }
5029
- child.stdout?.setEncoding("utf8");
5030
- child.stdout?.on("data", (chunk) => {
5031
- stdout += chunk;
5032
- });
5033
- child.stderr?.setEncoding("utf8");
5034
- child.stderr?.on("data", (chunk) => {
5035
- stderr += chunk;
5036
- });
5037
- child.once("error", (error2) => {
5038
- finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
5039
- });
5040
- child.once("close", (code) => {
5041
- finish({ code, stdout, stderr, timedOut: false });
5042
- });
5043
- if (opts.signal) {
5044
- const killChild = () => {
5045
- if (child.pid === void 0) {
5046
- if (!spawnListener) {
5047
- spawnListener = killChild;
5048
- child.once("spawn", spawnListener);
5049
- }
5050
- return;
5051
- }
5052
- child.kill("SIGKILL");
5053
- };
5054
- abortListener = killChild;
5055
- if (opts.signal.aborted) {
5056
- abortListener();
5057
- } else {
5058
- opts.signal.addEventListener("abort", abortListener, { once: true });
5059
- if (opts.signal.aborted) abortListener();
5060
- }
5061
- }
5062
- timer.handle = setTimeout(
5063
- () => {
5064
- child.kill("SIGKILL");
5065
- finish({ code: null, stdout, stderr, timedOut: true });
5066
- },
5067
- Math.max(0, opts.timeoutMs)
5068
- );
5069
- });
5070
- }
5071
-
5072
- // src/lib/opencode/session-db-boot.ts
6123
+ import { dirname as dirname7, resolve as resolvePath } from "node:path";
5073
6124
  var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
5074
6125
  var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
5075
6126
  var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
@@ -5107,7 +6158,7 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
5107
6158
  function clearMarker(options) {
5108
6159
  if (!options.noReplicateMarker) return;
5109
6160
  try {
5110
- unlinkSync2(options.noReplicateMarker);
6161
+ unlinkSync3(options.noReplicateMarker);
5111
6162
  } catch (error2) {
5112
6163
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
5113
6164
  options.log(
@@ -5119,8 +6170,8 @@ function clearMarker(options) {
5119
6170
  function markNoReplicate(options, message) {
5120
6171
  if (options.noReplicateMarker) {
5121
6172
  try {
5122
- mkdirSync(dirname4(options.noReplicateMarker), { recursive: true });
5123
- writeFileSync(options.noReplicateMarker, "");
6173
+ mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
6174
+ writeFileSync4(options.noReplicateMarker, "");
5124
6175
  } catch (error2) {
5125
6176
  options.log(
5126
6177
  `Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5133,7 +6184,7 @@ function markNoReplicate(options, message) {
5133
6184
  function discardSessionDbDebris(options) {
5134
6185
  for (const path of [options.dbPath, `${options.dbPath}-wal`, `${options.dbPath}-shm`]) {
5135
6186
  try {
5136
- unlinkSync2(path);
6187
+ unlinkSync3(path);
5137
6188
  } catch (error2) {
5138
6189
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") continue;
5139
6190
  options.log(
@@ -5274,8 +6325,8 @@ async function ensureLitestreamConfig(options, env) {
5274
6325
  return null;
5275
6326
  }
5276
6327
  try {
5277
- mkdirSync(dirname4(configPath), { recursive: true });
5278
- writeFileSync(configPath, rendered.stdout);
6328
+ mkdirSync4(dirname7(configPath), { recursive: true });
6329
+ writeFileSync4(configPath, rendered.stdout);
5279
6330
  } catch (error2) {
5280
6331
  options.log(
5281
6332
  `Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5540,8 +6591,8 @@ async function restoreAndVerifySessionDb(options) {
5540
6591
 
5541
6592
  // src/lib/opencode/session-db-provenance.ts
5542
6593
  import { createRequire } from "node:module";
5543
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
5544
- import { dirname as dirname5, join as join6 } from "node:path";
6594
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
6595
+ import { dirname as dirname8, join as join9 } from "node:path";
5545
6596
  var require2 = createRequire(import.meta.url);
5546
6597
  function readSessionDbMigrationIds(dbPath) {
5547
6598
  let db;
@@ -5578,12 +6629,12 @@ function readSessionDbMigrationIds(dbPath) {
5578
6629
  }
5579
6630
  function sessionDbProvenanceStatePath(homeDir, env) {
5580
6631
  const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
5581
- return override || join6(homeDir, ".local", "state", "evident", "session-db-provenance.json");
6632
+ return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5582
6633
  }
5583
6634
  function loadSessionDbProvenanceState(path) {
5584
6635
  let value;
5585
6636
  try {
5586
- value = JSON.parse(readFileSync5(path, "utf8"));
6637
+ value = JSON.parse(readFileSync9(path, "utf8"));
5587
6638
  } catch (error2) {
5588
6639
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
5589
6640
  console.error(
@@ -5607,8 +6658,8 @@ function loadSessionDbProvenanceState(path) {
5607
6658
  }
5608
6659
  function saveSessionDbProvenanceState(path, state) {
5609
6660
  try {
5610
- mkdirSync2(dirname5(path), { recursive: true });
5611
- writeFileSync2(path, `${JSON.stringify(state, null, 2)}
6661
+ mkdirSync5(dirname8(path), { recursive: true });
6662
+ writeFileSync5(path, `${JSON.stringify(state, null, 2)}
5612
6663
  `, "utf8");
5613
6664
  } catch (error2) {
5614
6665
  console.error(
@@ -5663,6 +6714,26 @@ function isSessionDbProvenanceRecord(value) {
5663
6714
  return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
5664
6715
  }
5665
6716
 
6717
+ // src/lib/opencode/detect.ts
6718
+ function describeProbe(route, result) {
6719
+ if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
6720
+ if (result.error) return `${route}: no HTTP response (${result.error})`;
6721
+ return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
6722
+ }
6723
+ async function classifyRunningOpenCode(port) {
6724
+ const v1 = await checkOpenCodeHealth(port);
6725
+ if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
6726
+ const v2 = await checkOpenCode2Health(port);
6727
+ if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
6728
+ const detail = [
6729
+ describeProbe("V1 /global/health", v1),
6730
+ describeProbe("V2 /api/info (or /api/health fallback)", v2)
6731
+ ].join("; ");
6732
+ if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
6733
+ if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
6734
+ return { kind: "no-server-identified", detail };
6735
+ }
6736
+
5666
6737
  // src/lib/opencode/opencode2-package.ts
5667
6738
  var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
5668
6739
 
@@ -5971,26 +7042,28 @@ async function startOpenCode(port, options = {}) {
5971
7042
  }
5972
7043
  async function startOpenCode2(port, options = {}) {
5973
7044
  const password = randomBytes(24).toString("hex");
5974
- let command = "opencode2";
7045
+ let command = options.command ?? "opencode2";
5975
7046
  const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
5976
7047
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
5977
- try {
5978
- execSync("which opencode2", { stdio: "ignore" });
5979
- } catch {
5980
- command = "npx";
5981
- args = [
5982
- "-y",
5983
- "-p",
5984
- OPENCODE2_PACKAGE_SPEC,
5985
- "--",
5986
- "opencode2",
5987
- "serve",
5988
- "--port",
5989
- port.toString(),
5990
- "--hostname",
5991
- "127.0.0.1",
5992
- ...logLevel
5993
- ];
7048
+ if (options.command === void 0) {
7049
+ try {
7050
+ execSync("which opencode2", { stdio: "ignore" });
7051
+ } catch {
7052
+ command = "npx";
7053
+ args = [
7054
+ "-y",
7055
+ "-p",
7056
+ OPENCODE2_PACKAGE_SPEC,
7057
+ "--",
7058
+ "opencode2",
7059
+ "serve",
7060
+ "--port",
7061
+ port.toString(),
7062
+ "--hostname",
7063
+ "127.0.0.1",
7064
+ ...logLevel
7065
+ ];
7066
+ }
5994
7067
  }
5995
7068
  const child = spawn3(command, args, {
5996
7069
  env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
@@ -6047,6 +7120,56 @@ function isOpenCode2Installed() {
6047
7120
  return false;
6048
7121
  }
6049
7122
  }
7123
+ function readVersionOutput(command) {
7124
+ try {
7125
+ return execSync2(`${command} --version`, {
7126
+ encoding: "utf-8",
7127
+ stdio: ["ignore", "pipe", "pipe"]
7128
+ }).trim();
7129
+ } catch (error2) {
7130
+ const output = typeof error2 === "object" && error2 !== null ? [
7131
+ "stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
7132
+ "stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
7133
+ ].filter(Boolean).join("\n") : "";
7134
+ return output || (error2 instanceof Error ? error2.message : String(error2));
7135
+ }
7136
+ }
7137
+ function parseOpenCodeVersion(rawVersion) {
7138
+ const output = rawVersion.trim();
7139
+ const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
7140
+ const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
7141
+ if (prefix === "opencode2") return { major: "v2", version: version2 };
7142
+ const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
7143
+ if (!major) return null;
7144
+ if (Number(major) === 1) return { major: "v1", version: version2 };
7145
+ if (Number(major) >= 2) return { major: "v2", version: version2 };
7146
+ return null;
7147
+ }
7148
+ function resolveInstalledOpenCode() {
7149
+ let opencode2Available = false;
7150
+ if (isOpenCodeInstalled()) {
7151
+ const rawVersion = readVersionOutput("opencode");
7152
+ const parsed = parseOpenCodeVersion(rawVersion);
7153
+ if (parsed?.major === "v1") {
7154
+ return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
7155
+ }
7156
+ if (parsed?.major === "v2") {
7157
+ return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
7158
+ }
7159
+ opencode2Available = isOpenCode2Installed();
7160
+ if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
7161
+ }
7162
+ if (opencode2Available || isOpenCode2Installed()) {
7163
+ const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
7164
+ return {
7165
+ kind: "installed",
7166
+ major: "v2",
7167
+ command: "opencode2",
7168
+ version: parsed?.version
7169
+ };
7170
+ }
7171
+ return { kind: "none" };
7172
+ }
6050
7173
  async function promptOpenCodeInstall(interactive) {
6051
7174
  if (!interactive) {
6052
7175
  console.log(
@@ -6105,166 +7228,59 @@ async function promptOpenCodeInstall(interactive) {
6105
7228
  console.log(chalk4.dim(`For more options, visit: ${chalk4.cyan(OPENCODE_INSTALL_URL)}`));
6106
7229
  blank();
6107
7230
  const afterInstall = await select({
6108
- message: "After installing, what would you like to do?",
6109
- choices: [
6110
- {
6111
- name: "I installed it - continue",
6112
- value: "continue",
6113
- description: "Proceed with the run command"
6114
- },
6115
- {
6116
- name: "Exit",
6117
- value: "exit",
6118
- description: "Exit now and run the command again later"
6119
- }
6120
- ]
6121
- });
6122
- if (afterInstall === "continue") {
6123
- if (isOpenCodeInstalled()) {
6124
- console.log(chalk4.green("\n\u2713 OpenCode detected!"));
6125
- return "installed";
6126
- } else {
6127
- console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
6128
- console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
6129
- const proceed = await select({
6130
- message: "Continue anyway?",
6131
- choices: [
6132
- { name: "Yes, continue", value: "continue" },
6133
- { name: "No, exit", value: "exit" }
6134
- ]
6135
- });
6136
- return proceed === "continue" ? "continue" : "exit";
6137
- }
6138
- }
6139
- return "exit";
6140
- }
6141
- return action;
6142
- }
6143
-
6144
- // src/lib/opencode/provider-check.ts
6145
- function buildNoProviderWarning(hasProvider) {
6146
- if (hasProvider !== false) return null;
6147
- 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).";
6148
- }
6149
-
6150
- // src/lib/opencode/session-cleanup.ts
6151
- var DURATION_UNIT_MS = {
6152
- s: 1e3,
6153
- m: 60 * 1e3,
6154
- h: 60 * 60 * 1e3,
6155
- d: 24 * 60 * 60 * 1e3
6156
- };
6157
- function parseDurationMs(input) {
6158
- const trimmed = input.trim();
6159
- const match = /^(\d+)([smhd])$/.exec(trimmed);
6160
- if (!match) {
6161
- throw new Error(
6162
- `Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
6163
- );
6164
- }
6165
- const value = Number(match[1]);
6166
- if (value <= 0) {
6167
- throw new Error(`Invalid duration "${input}": must be a positive value.`);
6168
- }
6169
- return value * DURATION_UNIT_MS[match[2]];
6170
- }
6171
- function selectSessionsToDelete(sessions, opts) {
6172
- const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
6173
- if (maxAgeMs === void 0 && maxCount === void 0) return [];
6174
- const ageEligible = (s) => {
6175
- if (maxAgeMs === void 0) return false;
6176
- if (s.lastActivityMs === null) return true;
6177
- return nowMs - s.lastActivityMs > maxAgeMs;
6178
- };
6179
- const countEligibleIds = /* @__PURE__ */ new Set();
6180
- if (maxCount !== void 0) {
6181
- const byActivityDesc = [...sessions].sort(
6182
- (a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
6183
- );
6184
- for (const s of byActivityDesc.slice(maxCount)) {
6185
- countEligibleIds.add(s.id);
6186
- }
6187
- }
6188
- const toDelete = [];
6189
- for (const s of sessions) {
6190
- if (protectedIds.has(s.id)) continue;
6191
- if (ageEligible(s) || countEligibleIds.has(s.id)) {
6192
- toDelete.push(s.id);
6193
- }
6194
- }
6195
- const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
6196
- return toDelete.sort(
6197
- (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
6198
- );
6199
- }
6200
- var DEFAULT_INTERVAL = "1h";
6201
- function resolve2(flag, envValue, fallback) {
6202
- return flag ?? envValue ?? fallback;
6203
- }
6204
- function parseMaxCount(input) {
6205
- const trimmed = input.trim();
6206
- if (!/^\d+$/.test(trimmed)) {
6207
- throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
6208
- }
6209
- const value = Number(trimmed);
6210
- if (value <= 0) {
6211
- throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
6212
- }
6213
- return value;
6214
- }
6215
- function resolveSessionCleanupConfig(flags, env = process.env) {
6216
- const warnings = [];
6217
- const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
6218
- const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
6219
- const intervalRaw = resolve2(
6220
- flags.interval,
6221
- env.EVIDENT_SESSION_CLEANUP_INTERVAL,
6222
- DEFAULT_INTERVAL
6223
- );
6224
- let maxAgeMs;
6225
- if (maxAgeRaw !== void 0) {
6226
- try {
6227
- maxAgeMs = parseDurationMs(maxAgeRaw);
6228
- } catch (err) {
6229
- warnings.push(
6230
- `Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
6231
- );
6232
- }
6233
- }
6234
- let maxCount;
6235
- if (maxCountRaw !== void 0) {
6236
- try {
6237
- maxCount = parseMaxCount(maxCountRaw);
6238
- } catch (err) {
6239
- warnings.push(
6240
- `Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
6241
- );
7231
+ message: "After installing, what would you like to do?",
7232
+ choices: [
7233
+ {
7234
+ name: "I installed it - continue",
7235
+ value: "continue",
7236
+ description: "Proceed with the run command"
7237
+ },
7238
+ {
7239
+ name: "Exit",
7240
+ value: "exit",
7241
+ description: "Exit now and run the command again later"
7242
+ }
7243
+ ]
7244
+ });
7245
+ if (afterInstall === "continue") {
7246
+ if (isOpenCodeInstalled()) {
7247
+ console.log(chalk4.green("\n\u2713 OpenCode detected!"));
7248
+ return "installed";
7249
+ } else {
7250
+ console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
7251
+ console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
7252
+ const proceed = await select({
7253
+ message: "Continue anyway?",
7254
+ choices: [
7255
+ { name: "Yes, continue", value: "continue" },
7256
+ { name: "No, exit", value: "exit" }
7257
+ ]
7258
+ });
7259
+ return proceed === "continue" ? "continue" : "exit";
7260
+ }
6242
7261
  }
7262
+ return "exit";
6243
7263
  }
6244
- let intervalMs;
6245
- try {
6246
- intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
6247
- } catch (err) {
6248
- warnings.push(
6249
- `Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
6250
- );
6251
- intervalMs = parseDurationMs(DEFAULT_INTERVAL);
6252
- }
6253
- const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
6254
- return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
7264
+ return action;
7265
+ }
7266
+
7267
+ // src/lib/opencode/provider-check.ts
7268
+ function buildNoProviderWarning(hasProvider) {
7269
+ if (hasProvider !== false) return null;
7270
+ 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).";
6255
7271
  }
6256
7272
 
6257
7273
  // src/lib/opencode/session-db-size.ts
6258
7274
  import { statSync as statSync4 } from "node:fs";
6259
- import { join as join7 } from "node:path";
7275
+ import { join as join10 } from "node:path";
6260
7276
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
6261
7277
  function statSessionDbBytes(homeDir) {
6262
- const dbPath = join7(homeDir, ".local", "share", "opencode", "opencode.db");
7278
+ const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
6263
7279
  try {
6264
7280
  return statSync4(dbPath).size;
6265
7281
  } catch (err) {
6266
- const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
6267
- if (!isMissingFile) {
7282
+ const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
7283
+ if (!isMissingFile4) {
6268
7284
  console.error(
6269
7285
  `[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
6270
7286
  );
@@ -6289,12 +7305,12 @@ function buildSessionStoreSizeWarning(input) {
6289
7305
  // src/lib/opencode/log-tail.ts
6290
7306
  import { statSync as statSync5 } from "node:fs";
6291
7307
  import { homedir as homedir5 } from "node:os";
6292
- import { join as join8 } from "node:path";
7308
+ import { join as join11 } from "node:path";
6293
7309
  import { open as open2, stat } from "node:fs/promises";
6294
7310
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
6295
7311
  function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
6296
- const dataDir = env.XDG_DATA_HOME || join8(homeDir, ".local", "share");
6297
- return join8(dataDir, "opencode", "log", "opencode.log");
7312
+ const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
7313
+ return join11(dataDir, "opencode", "log", "opencode.log");
6298
7314
  }
6299
7315
  function isEnoent(error2) {
6300
7316
  return error2?.code === "ENOENT";
@@ -6779,8 +7795,8 @@ function connectTunnel(options) {
6779
7795
  }
6780
7796
  forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
6781
7797
  } catch (error2) {
6782
- const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
6783
- onError?.(`Failed to handle binary message: ${errorMessage3}`);
7798
+ const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
7799
+ onError?.(`Failed to handle binary message: ${errorMessage4}`);
6784
7800
  }
6785
7801
  return;
6786
7802
  }
@@ -6788,8 +7804,8 @@ function connectTunnel(options) {
6788
7804
  try {
6789
7805
  message = JSON.parse(data.toString());
6790
7806
  } catch (error2) {
6791
- const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
6792
- onError?.(`Failed to handle message: ${errorMessage3}`);
7807
+ const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
7808
+ onError?.(`Failed to handle message: ${errorMessage4}`);
6793
7809
  return;
6794
7810
  }
6795
7811
  if (isStreamFrame(message)) {
@@ -6961,10 +7977,10 @@ var RunnerConnection = class _RunnerConnection {
6961
7977
  };
6962
7978
 
6963
7979
  // src/lib/tunnel/ready-marker.ts
6964
- import { writeFileSync as writeFileSync3 } from "node:fs";
7980
+ import { writeFileSync as writeFileSync6 } from "node:fs";
6965
7981
  function writeTunnelReadyMarker(path, agentId) {
6966
7982
  try {
6967
- writeFileSync3(path, `${agentId}
7983
+ writeFileSync6(path, `${agentId}
6968
7984
  `);
6969
7985
  return { ok: true };
6970
7986
  } catch (error2) {
@@ -6989,7 +8005,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
6989
8005
  }
6990
8006
 
6991
8007
  // src/lib/process-liveness.ts
6992
- import { readFileSync as readFileSync6 } from "node:fs";
8008
+ import { readFileSync as readFileSync10 } from "node:fs";
6993
8009
  function isProcessAlive(pid) {
6994
8010
  try {
6995
8011
  process.kill(pid, 0);
@@ -7004,7 +8020,7 @@ function isProcessAlive(pid) {
7004
8020
  }
7005
8021
  if (process.platform !== "linux") return true;
7006
8022
  try {
7007
- const status2 = readFileSync6(`/proc/${pid}/status`, "utf8");
8023
+ const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
7008
8024
  return !/^State:\s+Z(?:\s|$)/m.test(status2);
7009
8025
  } catch (error2) {
7010
8026
  console.error(
@@ -7071,8 +8087,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
7071
8087
  }
7072
8088
 
7073
8089
  // src/lib/resource-usage-reporting.ts
7074
- var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
7075
- var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
8090
+ var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
8091
+ var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
7076
8092
  function resolveResourceUsageReportingEnabled(flagValue, env) {
7077
8093
  if (flagValue === false) {
7078
8094
  return { enabled: false, warnings: [] };
@@ -7082,10 +8098,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
7082
8098
  return { enabled: true, warnings: [] };
7083
8099
  }
7084
8100
  const normalized = raw.trim().toLowerCase();
7085
- if (DISABLED_VALUES.has(normalized)) {
8101
+ if (DISABLED_VALUES2.has(normalized)) {
7086
8102
  return { enabled: false, warnings: [] };
7087
8103
  }
7088
- if (ENABLED_VALUES.has(normalized)) {
8104
+ if (ENABLED_VALUES2.has(normalized)) {
7089
8105
  return { enabled: true, warnings: [] };
7090
8106
  }
7091
8107
  return {
@@ -7363,18 +8379,18 @@ function createSessionStoreHealth(options = {}) {
7363
8379
  import { homedir as homedir6 } from "node:os";
7364
8380
 
7365
8381
  // src/lib/runner-file-sync.ts
7366
- import { join as join10 } from "node:path";
8382
+ import { join as join13 } from "node:path";
7367
8383
 
7368
8384
  // src/lib/file-push.ts
7369
8385
  import { randomUUID } from "node:crypto";
7370
8386
  import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
7371
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
8387
+ 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";
7372
8388
  var FILE_MODE = 384;
7373
8389
  var DIRECTORY_MODE = 448;
7374
8390
  async function writePushedFile(request3) {
7375
- const { requestedPath, content, allowedDirectories, homeDir } = request3;
8391
+ const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
7376
8392
  const bytes = content.byteLength;
7377
- if (allowedDirectories.length === 0) {
8393
+ if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
7378
8394
  return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
7379
8395
  path: requestedPath,
7380
8396
  bytes
@@ -7399,10 +8415,10 @@ async function writePushedFile(request3) {
7399
8415
  }
7400
8416
  try {
7401
8417
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
7402
- dirname6(candidate)
8418
+ dirname9(candidate)
7403
8419
  );
7404
- const realTarget = join9(existingAncestor, ...missingSegments, basename2(candidate));
7405
- const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
8420
+ const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
8421
+ const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
7406
8422
  if (allowedDirectory === null) {
7407
8423
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7408
8424
  path: realTarget,
@@ -7411,8 +8427,8 @@ async function writePushedFile(request3) {
7411
8427
  }
7412
8428
  if (missingSegments.length > 0) {
7413
8429
  await createMissingDirectories(existingAncestor, missingSegments);
7414
- const realParent = await realpath(dirname6(realTarget));
7415
- if (realParent !== dirname6(realTarget) || !contains(allowedDirectory, realTarget)) {
8430
+ const realParent = await realpath(dirname9(realTarget));
8431
+ if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
7416
8432
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7417
8433
  path: realTarget,
7418
8434
  bytes,
@@ -7437,7 +8453,7 @@ function expandAndValidate(requestedPath, homeDir) {
7437
8453
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
7438
8454
  return null;
7439
8455
  }
7440
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
8456
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
7441
8457
  if (expanded.split(/[/\\]/).includes("..")) {
7442
8458
  return null;
7443
8459
  }
@@ -7455,7 +8471,7 @@ async function resolveNearestExistingAncestor(directory) {
7455
8471
  try {
7456
8472
  return { existingAncestor: await realpath(current), missingSegments };
7457
8473
  } catch (err) {
7458
- const parent = dirname6(current);
8474
+ const parent = dirname9(current);
7459
8475
  if (err.code !== "ENOENT" || parent === current) {
7460
8476
  throw err;
7461
8477
  }
@@ -7477,6 +8493,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
7477
8493
  }
7478
8494
  return null;
7479
8495
  }
8496
+ async function findMatchingAllowedFile(allowedFiles, realTarget) {
8497
+ for (const entry of allowedFiles) {
8498
+ if (!isAbsolute2(entry)) {
8499
+ log("warn", "file_push_allowed_directory_skipped", {
8500
+ directory: entry,
8501
+ reason: "not_absolute_file"
8502
+ });
8503
+ continue;
8504
+ }
8505
+ const realParent = await realpathCreatingIfMissing(dirname9(entry));
8506
+ if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
8507
+ return realParent;
8508
+ }
8509
+ }
8510
+ return null;
8511
+ }
7480
8512
  async function realpathCreatingIfMissing(directory) {
7481
8513
  try {
7482
8514
  return await realpath(directory);
@@ -7510,13 +8542,13 @@ function contains(realDirectory, realTarget) {
7510
8542
  async function createMissingDirectories(existingAncestor, missingSegments) {
7511
8543
  let current = existingAncestor;
7512
8544
  for (const segment of missingSegments) {
7513
- current = join9(current, segment);
8545
+ current = join12(current, segment);
7514
8546
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
7515
8547
  await chmod(current, DIRECTORY_MODE);
7516
8548
  }
7517
8549
  }
7518
8550
  async function writeAtomically(realTarget, content) {
7519
- const temporaryPath = join9(dirname6(realTarget), `.evident-push-${randomUUID()}.tmp`);
8551
+ const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
7520
8552
  let handle;
7521
8553
  try {
7522
8554
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -7646,16 +8678,16 @@ var NOT_APPLIED = {
7646
8678
  opencodeAuthApplied: false
7647
8679
  };
7648
8680
  function isClaudeCredentialPath(requestedPath, homeDir) {
7649
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7650
- return expanded === join10(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
8681
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
8682
+ return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7651
8683
  }
7652
8684
  function isOpenCodeAuthPath(requestedPath, homeDir) {
7653
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7654
- return expanded === join10(homeDir, ...OPENCODE_AUTH_SEGMENTS);
8685
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
8686
+ return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7655
8687
  }
7656
8688
  async function applyOne(options, file) {
7657
8689
  const label = `${file.id.slice(0, 8)} (${file.path})`;
7658
- if (options.allowedDirectories.length === 0) {
8690
+ if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
7659
8691
  options.log({
7660
8692
  level: "warn",
7661
8693
  message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
@@ -7682,6 +8714,7 @@ async function applyOne(options, file) {
7682
8714
  requestedPath: file.path,
7683
8715
  content: download.content,
7684
8716
  allowedDirectories: options.allowedDirectories,
8717
+ allowedFiles: options.allowedFiles,
7685
8718
  homeDir: options.homeDir
7686
8719
  });
7687
8720
  } catch (err) {
@@ -7788,6 +8821,212 @@ function describe(err) {
7788
8821
  return err instanceof Error ? err.message : String(err);
7789
8822
  }
7790
8823
 
8824
+ // src/lib/vault-file-sync.ts
8825
+ import { createHash as createHash2 } from "node:crypto";
8826
+ async function syncVaultFiles(options) {
8827
+ const files = await listVaultFiles(options);
8828
+ if (files === null) return { applied: 0 };
8829
+ const listedIds = new Set(files.map((file) => file.id));
8830
+ for (const fileId of options.outcomes.keys()) {
8831
+ if (!listedIds.has(fileId)) options.outcomes.delete(fileId);
8832
+ }
8833
+ let applied = 0;
8834
+ for (const file of files) {
8835
+ try {
8836
+ if (await reconcileVaultFile(options, file)) applied += 1;
8837
+ } catch (err) {
8838
+ options.log({
8839
+ level: "error",
8840
+ message: `Vault file ${file.id} reconciliation failed unexpectedly (message processing is unaffected): ${describe2(err)}`
8841
+ });
8842
+ }
8843
+ }
8844
+ return { applied };
8845
+ }
8846
+ async function listVaultFiles(options) {
8847
+ let response;
8848
+ try {
8849
+ response = await options.fetchImpl(`${options.apiUrl}/runners/${options.agentId}/vault-files`, {
8850
+ headers: { Authorization: options.getAuthHeader() }
8851
+ });
8852
+ } catch (err) {
8853
+ options.log({
8854
+ level: "warn",
8855
+ message: `Could not list vault files \u2014 retrying on the next drain: ${describe2(err)}`
8856
+ });
8857
+ return null;
8858
+ }
8859
+ if (!response.ok) {
8860
+ options.log({
8861
+ level: response.status === 404 ? "debug" : "warn",
8862
+ message: `Listing vault files returned HTTP ${response.status} \u2014 retrying on the next drain`
8863
+ });
8864
+ return null;
8865
+ }
8866
+ let body;
8867
+ try {
8868
+ body = await response.json();
8869
+ } catch (err) {
8870
+ options.log({
8871
+ level: "warn",
8872
+ message: `Vault file list was not readable JSON \u2014 retrying on the next drain: ${describe2(err)}`
8873
+ });
8874
+ return null;
8875
+ }
8876
+ if (body === null || typeof body !== "object" || !Array.isArray(body.files)) {
8877
+ options.log({
8878
+ level: "warn",
8879
+ message: "Vault file list was not an object with a files array \u2014 ignoring it for this drain"
8880
+ });
8881
+ return null;
8882
+ }
8883
+ const files = [];
8884
+ for (const entry of body.files) {
8885
+ const file = asVaultFile(entry);
8886
+ if (file === null) {
8887
+ options.log({
8888
+ level: "warn",
8889
+ message: "Ignoring a malformed vault file entry"
8890
+ });
8891
+ continue;
8892
+ }
8893
+ files.push(file);
8894
+ }
8895
+ return files;
8896
+ }
8897
+ function asVaultFile(entry) {
8898
+ if (entry === null || typeof entry !== "object") return null;
8899
+ const value = entry;
8900
+ if (typeof value.id !== "string" || value.id === "" || typeof value.destination_path !== "string" || value.destination_path === "" || typeof value.size_bytes !== "number" || !Number.isFinite(value.size_bytes) || value.size_bytes < 0 || typeof value.content_sha256 !== "string" || value.content_sha256 === "" || typeof value.created_at !== "string" || typeof value.updated_at !== "string") {
8901
+ return null;
8902
+ }
8903
+ return {
8904
+ id: value.id,
8905
+ destination_path: value.destination_path,
8906
+ size_bytes: value.size_bytes,
8907
+ content_sha256: value.content_sha256,
8908
+ created_at: value.created_at,
8909
+ updated_at: value.updated_at
8910
+ };
8911
+ }
8912
+ async function reconcileVaultFile(options, file) {
8913
+ const existing = options.outcomes.get(file.id);
8914
+ if (existing?.sha256 === file.content_sha256) {
8915
+ if (existing.reported || existing.reportAttempts >= MAX_ACK_ATTEMPTS) return false;
8916
+ await reportOutcome(options, file, existing);
8917
+ return false;
8918
+ }
8919
+ const content = await downloadVaultFile(options, file);
8920
+ if (content === null) return false;
8921
+ const actualSha256 = createHash2("sha256").update(content).digest("hex");
8922
+ if (actualSha256 !== file.content_sha256) {
8923
+ options.log({
8924
+ level: "warn",
8925
+ message: `Vault file ${file.id} content hash mismatch: expected ${file.content_sha256}, received ${actualSha256} \u2014 retrying on the next drain`
8926
+ });
8927
+ return false;
8928
+ }
8929
+ const outcome = await applyVaultFile(options, file, content);
8930
+ const entry = {
8931
+ sha256: file.content_sha256,
8932
+ outcome: outcome.ok ? "applied" : "refused",
8933
+ reason: outcome.ok ? null : outcome.code,
8934
+ reported: false,
8935
+ reportAttempts: 0
8936
+ };
8937
+ options.outcomes.set(file.id, entry);
8938
+ await reportOutcome(options, file, entry);
8939
+ return outcome.ok;
8940
+ }
8941
+ async function downloadVaultFile(options, file) {
8942
+ try {
8943
+ const response = await options.fetchImpl(
8944
+ `${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/content`,
8945
+ { headers: { Authorization: options.getAuthHeader() } }
8946
+ );
8947
+ if (!response.ok) {
8948
+ options.log({
8949
+ level: "warn",
8950
+ message: `Downloading vault file ${file.id} returned HTTP ${response.status} \u2014 retrying on the next drain`
8951
+ });
8952
+ return null;
8953
+ }
8954
+ return Buffer.from(await response.arrayBuffer());
8955
+ } catch (err) {
8956
+ options.log({
8957
+ level: "warn",
8958
+ message: `Downloading vault file ${file.id} failed \u2014 retrying on the next drain: ${describe2(err)}`
8959
+ });
8960
+ return null;
8961
+ }
8962
+ }
8963
+ async function applyVaultFile(options, file, content) {
8964
+ try {
8965
+ const outcome = await writePushedFile({
8966
+ requestedPath: file.destination_path,
8967
+ content,
8968
+ allowedDirectories: options.allowedDirectories,
8969
+ allowedFiles: options.allowedFiles,
8970
+ homeDir: options.homeDir
8971
+ });
8972
+ if (!outcome.ok) {
8973
+ options.log({
8974
+ level: "warn",
8975
+ message: `Vault file ${file.id} refused (${outcome.code}): ${outcome.message}`
8976
+ });
8977
+ } else {
8978
+ options.log({
8979
+ level: "info",
8980
+ message: `Vault file ${file.id} applied (${content.byteLength} bytes)`
8981
+ });
8982
+ }
8983
+ return outcome;
8984
+ } catch (err) {
8985
+ options.log({
8986
+ level: "error",
8987
+ message: `Vault file ${file.id} could not be written: ${describe2(err)}`
8988
+ });
8989
+ return { ok: false, code: "write_failed", message: "The runner could not write the file." };
8990
+ }
8991
+ }
8992
+ async function reportOutcome(options, file, entry) {
8993
+ const body = {
8994
+ status: entry.outcome === "applied" ? "applied" : "rejected",
8995
+ reported_sha256: entry.sha256
8996
+ };
8997
+ if (entry.outcome === "refused" && entry.reason !== null) body.reason = entry.reason;
8998
+ try {
8999
+ const response = await options.fetchImpl(
9000
+ `${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/status`,
9001
+ {
9002
+ method: "POST",
9003
+ headers: {
9004
+ Authorization: options.getAuthHeader(),
9005
+ "Content-Type": "application/json"
9006
+ },
9007
+ body: JSON.stringify(body)
9008
+ }
9009
+ );
9010
+ if (response.ok) {
9011
+ entry.reported = true;
9012
+ return;
9013
+ }
9014
+ recordReportFailure(options, file, entry, `HTTP ${response.status}`);
9015
+ } catch (err) {
9016
+ recordReportFailure(options, file, entry, describe2(err));
9017
+ }
9018
+ }
9019
+ function recordReportFailure(options, file, entry, reason) {
9020
+ entry.reportAttempts += 1;
9021
+ options.log({
9022
+ level: "error",
9023
+ message: entry.reportAttempts >= MAX_ACK_ATTEMPTS ? `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 giving up after ${entry.reportAttempts} attempts` : `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 retrying on the next drain (attempt ${entry.reportAttempts} of ${MAX_ACK_ATTEMPTS})`
9024
+ });
9025
+ }
9026
+ function describe2(err) {
9027
+ return err instanceof Error ? err.message : String(err);
9028
+ }
9029
+
7791
9030
  // src/lib/channels/driver.ts
7792
9031
  function messageIdOf(m) {
7793
9032
  if (!m || typeof m !== "object") return void 0;
@@ -7796,7 +9035,7 @@ function messageIdOf(m) {
7796
9035
  return typeof infoId === "string" ? infoId : void 0;
7797
9036
  }
7798
9037
  function composeAbandonmentNote(finish, replyShape, outcome) {
7799
- const base = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
9038
+ const base = outcome === "midflight-without-answer" || outcome === "midflight-with-answer" ? "OpenCode stopped this turn without ever finishing, and confirmed the session is no longer running. " + (outcome === "midflight-without-answer" ? "Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "Evident delivered what was written and is reporting the run as finished.") : `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
7800
9039
  if (replyShape === null) return base;
7801
9040
  return `${base} ${replyShape.lastToolName === null ? "No tool call was recorded" : `The last tool call was "${replyShape.lastToolName}" with status "${replyShape.lastToolStatus ?? "(unknown)"}"`}; the reply's final part types were [${replyShape.partTypes.join(", ")}]${replyShape.droppedPartCount > 0 ? ` (${replyShape.droppedPartCount} earlier part types omitted)` : ""}.`;
7802
9041
  }
@@ -7833,6 +9072,9 @@ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
7833
9072
  var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
7834
9073
  var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
7835
9074
  var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
9075
+ var MIDFLIGHT_ORPHAN_MIN_PINNED_MS = 3 * 6e4;
9076
+ var MIDFLIGHT_ORPHAN_RECHECK_MS = 6e4;
9077
+ var MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK = 2;
7836
9078
  var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
7837
9079
  var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
7838
9080
  var WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;
@@ -7877,6 +9119,7 @@ var ChannelDriver = class _ChannelDriver {
7877
9119
  conversationFilter;
7878
9120
  retry;
7879
9121
  log;
9122
+ recordBinding;
7880
9123
  fetchImpl;
7881
9124
  sleep;
7882
9125
  pausedPollIntervalMs;
@@ -7884,6 +9127,7 @@ var ChannelDriver = class _ChannelDriver {
7884
9127
  stuckQueuedMs;
7885
9128
  now;
7886
9129
  fileSyncDirectories;
9130
+ fileSyncExactFiles;
7887
9131
  homeDir;
7888
9132
  maxActiveSessions;
7889
9133
  watcherStallMs;
@@ -8196,6 +9440,7 @@ var ChannelDriver = class _ChannelDriver {
8196
9440
  * re-downloaded and re-written every ~2s until the server expires it.
8197
9441
  */
8198
9442
  fileAckFailures = /* @__PURE__ */ new Map();
9443
+ vaultFileOutcomes = /* @__PURE__ */ new Map();
8199
9444
  /**
8200
9445
  * Monotonic count of files this runner has pulled and written (#559). Only
8201
9446
  * ever increases, so `run.ts` detects work by comparing it against the value
@@ -8237,6 +9482,7 @@ var ChannelDriver = class _ChannelDriver {
8237
9482
  this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
8238
9483
  this.log = config.log ?? (() => {
8239
9484
  });
9485
+ this.recordBinding = config.recordBinding;
8240
9486
  this.fetchImpl = withRequestTimeout(
8241
9487
  config.fetchImpl ?? fetch,
8242
9488
  config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
@@ -8252,6 +9498,7 @@ var ChannelDriver = class _ChannelDriver {
8252
9498
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
8253
9499
  this.now = config.now ?? (() => Date.now());
8254
9500
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
9501
+ this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
8255
9502
  this.homeDir = config.homeDir ?? homedir6();
8256
9503
  this.maxActiveSessions = config.maxActiveSessions;
8257
9504
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
@@ -8349,14 +9596,27 @@ var ChannelDriver = class _ChannelDriver {
8349
9596
  getAuthHeader: this.getAuthHeader,
8350
9597
  fetchImpl: this.fetchImpl,
8351
9598
  allowedDirectories: this.fileSyncDirectories,
9599
+ allowedFiles: this.fileSyncExactFiles,
8352
9600
  homeDir: this.homeDir,
8353
9601
  ackFailures: this.fileAckFailures,
8354
9602
  log: this.log
8355
9603
  });
8356
- this.appliedFileCount += result.applied;
9604
+ const vaultResult = await syncVaultFiles({
9605
+ agentId: this.agentId,
9606
+ apiUrl: this.apiUrl,
9607
+ getAuthHeader: this.getAuthHeader,
9608
+ fetchImpl: this.fetchImpl,
9609
+ allowedDirectories: this.fileSyncDirectories,
9610
+ allowedFiles: this.fileSyncExactFiles,
9611
+ homeDir: this.homeDir,
9612
+ outcomes: this.vaultFileOutcomes,
9613
+ log: this.log
9614
+ });
9615
+ const applied = result.applied + vaultResult.applied;
9616
+ this.appliedFileCount += applied;
8357
9617
  if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
8358
9618
  if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
8359
- return result.applied;
9619
+ return applied;
8360
9620
  } catch (err) {
8361
9621
  this.log({
8362
9622
  level: "error",
@@ -8410,6 +9670,9 @@ var ChannelDriver = class _ChannelDriver {
8410
9670
  let cappedSkips = 0;
8411
9671
  for (const conv of conversations) {
8412
9672
  if (this.stopped) break;
9673
+ if (conv.opencode_session_id) {
9674
+ this.recordSessionBinding(conv.id, conv.opencode_session_id);
9675
+ }
8413
9676
  if (this.maxActiveSessions !== void 0) {
8414
9677
  const activeSessionIds = this.activeSessionIdsForCap();
8415
9678
  const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
@@ -8492,12 +9755,12 @@ var ChannelDriver = class _ChannelDriver {
8492
9755
  this.sessions.delete(conversationId);
8493
9756
  this.supersede(conversationId, sessionId);
8494
9757
  const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
8495
- 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.`;
8496
- this.untrackableAck.set(messageId, errorMessage3);
9758
+ 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.`;
9759
+ this.untrackableAck.set(messageId, errorMessage4);
8497
9760
  this.untrackableAckSessionIds.set(messageId, null);
8498
9761
  this.log({
8499
9762
  level: "error",
8500
- message: errorMessage3,
9763
+ message: errorMessage4,
8501
9764
  conversation_id: conversationId,
8502
9765
  message_id: messageId
8503
9766
  });
@@ -8505,7 +9768,7 @@ var ChannelDriver = class _ChannelDriver {
8505
9768
  conversationId,
8506
9769
  messageId,
8507
9770
  null,
8508
- errorMessage3
9771
+ errorMessage4
8509
9772
  );
8510
9773
  if (reportError !== null) {
8511
9774
  this.log({
@@ -8686,12 +9949,12 @@ var ChannelDriver = class _ChannelDriver {
8686
9949
  conversation_id: conv.id,
8687
9950
  message_id: message.id
8688
9951
  });
8689
- const errorMessage3 = this.untrackableAck.get(message.id);
9952
+ const errorMessage4 = this.untrackableAck.get(message.id);
8690
9953
  const reportError = await this.reportUntrackableAck(
8691
9954
  conv.id,
8692
9955
  message.id,
8693
9956
  null,
8694
- errorMessage3
9957
+ errorMessage4
8695
9958
  );
8696
9959
  if (reportError === null) {
8697
9960
  continue;
@@ -8779,7 +10042,7 @@ var ChannelDriver = class _ChannelDriver {
8779
10042
  const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
8780
10043
  const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
8781
10044
  const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
8782
- const errorMessage3 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
10045
+ const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
8783
10046
  this.log({
8784
10047
  level: "error",
8785
10048
  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`,
@@ -8787,7 +10050,7 @@ var ChannelDriver = class _ChannelDriver {
8787
10050
  message_id: message.id
8788
10051
  });
8789
10052
  try {
8790
- await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
10053
+ await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
8791
10054
  classification: {
8792
10055
  kind: "model_auth",
8793
10056
  providerId,
@@ -8859,12 +10122,12 @@ var ChannelDriver = class _ChannelDriver {
8859
10122
  } catch (err) {
8860
10123
  if (err instanceof ChannelAuthError) throw err;
8861
10124
  if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
8862
- const errorMessage4 = err instanceof Error ? err.message : String(err);
8863
- this.untrackableAck.set(message.id, errorMessage4);
10125
+ const errorMessage5 = err instanceof Error ? err.message : String(err);
10126
+ this.untrackableAck.set(message.id, errorMessage5);
8864
10127
  this.untrackableAckSessionIds.set(message.id, null);
8865
10128
  this.log({
8866
10129
  level: "error",
8867
- message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
10130
+ message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
8868
10131
  conversation_id: conv.id,
8869
10132
  message_id: message.id
8870
10133
  });
@@ -8872,7 +10135,7 @@ var ChannelDriver = class _ChannelDriver {
8872
10135
  conv.id,
8873
10136
  message.id,
8874
10137
  null,
8875
- errorMessage4
10138
+ errorMessage5
8876
10139
  );
8877
10140
  if (markError !== null) {
8878
10141
  this.log({
@@ -8907,7 +10170,7 @@ var ChannelDriver = class _ChannelDriver {
8907
10170
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
8908
10171
  break;
8909
10172
  }
8910
- const errorMessage3 = err instanceof Error ? err.message : String(err);
10173
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
8911
10174
  this.sessions.delete(conv.id);
8912
10175
  this.supersede(conv.id, sessionId);
8913
10176
  this.log({
@@ -8916,7 +10179,7 @@ var ChannelDriver = class _ChannelDriver {
8916
10179
  conversation_id: conv.id,
8917
10180
  message_id: message.id
8918
10181
  });
8919
- await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
10182
+ await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
8920
10183
  this.log({
8921
10184
  level: "warn",
8922
10185
  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)}`,
@@ -8927,7 +10190,7 @@ var ChannelDriver = class _ChannelDriver {
8927
10190
  });
8928
10191
  this.log({
8929
10192
  level: "error",
8930
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage3}`,
10193
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
8931
10194
  conversation_id: conv.id,
8932
10195
  message_id: message.id
8933
10196
  });
@@ -9714,6 +10977,7 @@ var ChannelDriver = class _ChannelDriver {
9714
10977
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
9715
10978
  }
9716
10979
  this.sessions.set(conv.id, bound);
10980
+ this.recordSessionBinding(conv.id, bound);
9717
10981
  return { sessionId: bound, created: false };
9718
10982
  }
9719
10983
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
@@ -9727,6 +10991,7 @@ var ChannelDriver = class _ChannelDriver {
9727
10991
  const directory = await this.resolveOpenCodeDirectory();
9728
10992
  const sessionId = await this.createOpenCodeSession(directory);
9729
10993
  this.sessions.set(conversationId, sessionId);
10994
+ this.recordSessionBinding(conversationId, sessionId);
9730
10995
  await this.persistSession(conversationId, sessionId).catch((err) => {
9731
10996
  this.log({
9732
10997
  level: "warn",
@@ -9736,6 +11001,18 @@ var ChannelDriver = class _ChannelDriver {
9736
11001
  });
9737
11002
  return sessionId;
9738
11003
  }
11004
+ recordSessionBinding(conversationId, sessionId) {
11005
+ if (!this.recordBinding) return;
11006
+ try {
11007
+ this.recordBinding(conversationId, sessionId);
11008
+ } catch (error2) {
11009
+ this.log({
11010
+ level: "warn",
11011
+ message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
11012
+ conversation_id: conversationId
11013
+ });
11014
+ }
11015
+ }
9739
11016
  /**
9740
11017
  * Lazily resolve (and cache) opencode's root directory via the selected client's
9741
11018
  * location lookup.
@@ -9933,6 +11210,12 @@ var ChannelDriver = class _ChannelDriver {
9933
11210
  b2AbandonedWithoutAnswer: false,
9934
11211
  ambiguousPinnedSinceMs: 0,
9935
11212
  ambiguousResolved: false,
11213
+ midFlightPinnedSinceMs: 0,
11214
+ midFlightNotOngoingStreak: 0,
11215
+ midFlightLastStatusCheckMs: 0,
11216
+ midFlightOrphanSignalled: false,
11217
+ midFlightOrphanWithoutAnswer: false,
11218
+ midFlightHeldLogged: false,
9936
11219
  sessionStoreFailurePending: false,
9937
11220
  neverStartedResolved: false
9938
11221
  });
@@ -10072,6 +11355,12 @@ var ChannelDriver = class _ChannelDriver {
10072
11355
  b2AbandonedWithoutAnswer: false,
10073
11356
  ambiguousPinnedSinceMs: 0,
10074
11357
  ambiguousResolved: false,
11358
+ midFlightPinnedSinceMs: 0,
11359
+ midFlightNotOngoingStreak: 0,
11360
+ midFlightLastStatusCheckMs: 0,
11361
+ midFlightOrphanSignalled: false,
11362
+ midFlightOrphanWithoutAnswer: false,
11363
+ midFlightHeldLogged: false,
10075
11364
  sessionStoreFailurePending: false,
10076
11365
  neverStartedResolved: false
10077
11366
  });
@@ -10843,7 +12132,11 @@ var ChannelDriver = class _ChannelDriver {
10843
12132
  watcher,
10844
12133
  inFlight,
10845
12134
  messages,
10846
- finish
12135
+ composeAbandonmentNote(
12136
+ finish,
12137
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
12138
+ "without-answer"
12139
+ )
10847
12140
  );
10848
12141
  } else {
10849
12142
  await this.settleMessageDone(
@@ -10902,7 +12195,11 @@ var ChannelDriver = class _ChannelDriver {
10902
12195
  watcher,
10903
12196
  inFlight,
10904
12197
  messages,
10905
- finish
12198
+ composeAbandonmentNote(
12199
+ finish,
12200
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
12201
+ "without-answer"
12202
+ )
10906
12203
  );
10907
12204
  return;
10908
12205
  }
@@ -10979,21 +12276,121 @@ var ChannelDriver = class _ChannelDriver {
10979
12276
  run_state: "failed",
10980
12277
  opencode_message_id: inFlight.opencodeMessageId
10981
12278
  });
10982
- await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
12279
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
12280
+ return;
12281
+ }
12282
+ inFlight.ambiguousResolved = true;
12283
+ this.log({
12284
+ level: "warn",
12285
+ message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1e3)}s \u2014 resolved (${ongoing === false ? "session confirmed not-ongoing" : "pin exceeded the no-hang cap"}) \u2014 settling done`,
12286
+ conversation_id: conv.id,
12287
+ message_id: id
12288
+ });
12289
+ void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
12290
+ watched_for_ms: pinnedForMs
12291
+ });
12292
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages);
12293
+ return;
12294
+ }
12295
+ }
12296
+ const midFlightPinnedNow = activelyRunning && isMidFlightPinnedRunning(messages, inFlight.opencodeMessageId);
12297
+ if (!midFlightPinnedNow) {
12298
+ if (snapshotReadable) {
12299
+ inFlight.midFlightPinnedSinceMs = 0;
12300
+ inFlight.midFlightNotOngoingStreak = 0;
12301
+ inFlight.midFlightLastStatusCheckMs = 0;
12302
+ inFlight.midFlightOrphanSignalled = false;
12303
+ inFlight.midFlightOrphanWithoutAnswer = false;
12304
+ inFlight.midFlightHeldLogged = false;
12305
+ }
12306
+ } else {
12307
+ if (inFlight.midFlightOrphanSignalled) {
12308
+ const note = composeAbandonmentNote(
12309
+ void 0,
12310
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
12311
+ inFlight.midFlightOrphanWithoutAnswer ? "midflight-without-answer" : "midflight-with-answer"
12312
+ );
12313
+ if (inFlight.midFlightOrphanWithoutAnswer) {
12314
+ await this.settleMessageAbandonedWithoutAnswer(
12315
+ sessionId,
12316
+ watcher,
12317
+ inFlight,
12318
+ messages,
12319
+ note
12320
+ );
12321
+ } else {
12322
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
12323
+ }
12324
+ return;
12325
+ }
12326
+ if (inFlight.midFlightPinnedSinceMs === 0) {
12327
+ inFlight.midFlightPinnedSinceMs = this.now();
12328
+ }
12329
+ const pinnedForMs = this.now() - inFlight.midFlightPinnedSinceMs;
12330
+ if (pinnedForMs >= MIDFLIGHT_ORPHAN_MIN_PINNED_MS && this.now() - inFlight.midFlightLastStatusCheckMs >= MIDFLIGHT_ORPHAN_RECHECK_MS) {
12331
+ inFlight.midFlightLastStatusCheckMs = this.now();
12332
+ const rootOngoing = await this.isSessionOngoing(sessionId);
12333
+ inFlight.midFlightNotOngoingStreak = rootOngoing === false ? inFlight.midFlightNotOngoingStreak + 1 : 0;
12334
+ if (isMidFlightOrphanConfirmed({
12335
+ pinnedForMs,
12336
+ minPinnedMs: MIDFLIGHT_ORPHAN_MIN_PINNED_MS,
12337
+ notOngoingStreak: inFlight.midFlightNotOngoingStreak,
12338
+ minNotOngoingStreak: MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK
12339
+ })) {
12340
+ if (this.sessionStoreHealth?.isUnwritable()) {
12341
+ inFlight.sessionStoreFailurePending = true;
12342
+ void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
12343
+ watched_for_ms: pinnedForMs,
12344
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
12345
+ opencode_message_id: inFlight.opencodeMessageId,
12346
+ run_state: "failed"
12347
+ });
12348
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
12349
+ return;
12350
+ }
12351
+ inFlight.midFlightOrphanSignalled = true;
12352
+ const noteOutcome = extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null ? "midflight-without-answer" : "midflight-with-answer";
12353
+ inFlight.midFlightOrphanWithoutAnswer = noteOutcome === "midflight-without-answer";
12354
+ void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
12355
+ watched_for_ms: pinnedForMs,
12356
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
12357
+ opencode_message_id: inFlight.opencodeMessageId,
12358
+ ...inFlight.midFlightOrphanWithoutAnswer ? { run_state: "failed" } : {}
12359
+ });
12360
+ const note = composeAbandonmentNote(
12361
+ void 0,
12362
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
12363
+ noteOutcome
12364
+ );
12365
+ this.log({
12366
+ level: "warn",
12367
+ message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=${rootOngoing} \u2014 resolving the orphaned turn`,
12368
+ conversation_id: conv.id,
12369
+ message_id: id
12370
+ });
12371
+ if (inFlight.midFlightOrphanWithoutAnswer) {
12372
+ await this.settleMessageAbandonedWithoutAnswer(
12373
+ sessionId,
12374
+ watcher,
12375
+ inFlight,
12376
+ messages,
12377
+ note
12378
+ );
12379
+ } else {
12380
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
12381
+ }
10983
12382
  return;
10984
12383
  }
10985
- inFlight.ambiguousResolved = true;
10986
- this.log({
10987
- level: "warn",
10988
- message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1e3)}s \u2014 resolved (${ongoing === false ? "session confirmed not-ongoing" : "pin exceeded the no-hang cap"}) \u2014 settling done`,
10989
- conversation_id: conv.id,
10990
- message_id: id
10991
- });
10992
- void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
10993
- watched_for_ms: pinnedForMs
10994
- });
10995
- await this.settleMessageDone(sessionId, watcher, inFlight, messages);
10996
- return;
12384
+ if (rootOngoing === true && !inFlight.midFlightHeldLogged) {
12385
+ inFlight.midFlightHeldLogged = true;
12386
+ const replyShape = describeAbandonedReplyShape(messages, inFlight.opencodeMessageId);
12387
+ this.log({
12388
+ level: "warn",
12389
+ message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=true \u2014 holding; last tool ${replyShape?.lastToolName ?? "(unknown)"} status ${replyShape?.lastToolStatus ?? "(unknown)"}`,
12390
+ conversation_id: conv.id,
12391
+ message_id: id
12392
+ });
12393
+ }
10997
12394
  }
10998
12395
  }
10999
12396
  if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
@@ -11195,13 +12592,8 @@ var ChannelDriver = class _ChannelDriver {
11195
12592
  this.removeInFlight(watcher, inFlight.evidentMessageId);
11196
12593
  }
11197
12594
  /** Report a confirmed b2 abandonment that produced no answer text. */
11198
- async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
12595
+ async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, error2) {
11199
12596
  const conv = watcher.conv;
11200
- const error2 = composeAbandonmentNote(
11201
- finish,
11202
- describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11203
- "without-answer"
11204
- );
11205
12597
  this.anchorDeliveryDeadline(inFlight);
11206
12598
  this.log({
11207
12599
  level: "warn",
@@ -11721,12 +13113,12 @@ var ChannelDriver = class _ChannelDriver {
11721
13113
  conversation_id: row.conversation_id,
11722
13114
  message_id: row.id
11723
13115
  });
11724
- const errorMessage3 = this.untrackableAck.get(row.id);
13116
+ const errorMessage4 = this.untrackableAck.get(row.id);
11725
13117
  const reportError = await this.reportUntrackableAck(
11726
13118
  row.conversation_id,
11727
13119
  row.id,
11728
13120
  this.untrackableAckSessionIds.get(row.id) ?? null,
11729
- errorMessage3
13121
+ errorMessage4
11730
13122
  );
11731
13123
  if (reportError !== null) {
11732
13124
  this.log({
@@ -12039,14 +13431,14 @@ var ChannelDriver = class _ChannelDriver {
12039
13431
  this.awaitingReadopt.delete(row.id);
12040
13432
  if (err instanceof ChannelAuthError) throw err;
12041
13433
  if (this.isV2) {
12042
- const errorMessage3 = err instanceof Error ? err.message : String(err);
13434
+ const errorMessage4 = err instanceof Error ? err.message : String(err);
12043
13435
  const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
12044
13436
  if (!invalidPromptAck) {
12045
13437
  const exists = await this.sessionExists(sessionId);
12046
13438
  if (exists === false) {
12047
13439
  this.log({
12048
13440
  level: "warn",
12049
- 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}`,
13441
+ 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}`,
12050
13442
  conversation_id: row.conversation_id,
12051
13443
  message_id: row.id
12052
13444
  });
@@ -12056,7 +13448,7 @@ var ChannelDriver = class _ChannelDriver {
12056
13448
  if (exists === null) {
12057
13449
  this.log({
12058
13450
  level: "warn",
12059
- 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}`,
13451
+ 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}`,
12060
13452
  conversation_id: row.conversation_id,
12061
13453
  message_id: row.id
12062
13454
  });
@@ -12064,12 +13456,12 @@ var ChannelDriver = class _ChannelDriver {
12064
13456
  return;
12065
13457
  }
12066
13458
  } else {
12067
- this.untrackableAck.set(row.id, errorMessage3);
13459
+ this.untrackableAck.set(row.id, errorMessage4);
12068
13460
  this.untrackableAckSessionIds.set(row.id, sessionId);
12069
13461
  }
12070
13462
  this.log({
12071
13463
  level: "error",
12072
- message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage3}`,
13464
+ message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
12073
13465
  conversation_id: row.conversation_id,
12074
13466
  message_id: row.id
12075
13467
  });
@@ -12078,7 +13470,7 @@ var ChannelDriver = class _ChannelDriver {
12078
13470
  row.conversation_id,
12079
13471
  row.id,
12080
13472
  sessionId,
12081
- errorMessage3
13473
+ errorMessage4
12082
13474
  );
12083
13475
  if (markError !== null) {
12084
13476
  this.log({
@@ -12089,7 +13481,7 @@ var ChannelDriver = class _ChannelDriver {
12089
13481
  });
12090
13482
  }
12091
13483
  } else {
12092
- await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage3).catch(
13484
+ await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
12093
13485
  (markErr) => {
12094
13486
  this.log({
12095
13487
  level: "warn",
@@ -13073,9 +14465,9 @@ var ChannelDriver = class _ChannelDriver {
13073
14465
  * trackable. Callers use the returned error to keep their path-specific logging
13074
14466
  * while the same report payload can be retried on a later drain.
13075
14467
  */
13076
- async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage3) {
14468
+ async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
13077
14469
  try {
13078
- await this.markFailed(conversationId, messageId, sessionId, errorMessage3);
14470
+ await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
13079
14471
  return null;
13080
14472
  } catch (err) {
13081
14473
  this.signalUntrackableAck(conversationId, messageId);
@@ -13587,7 +14979,7 @@ import ora3 from "ora";
13587
14979
  import { select as select3 } from "@inquirer/prompts";
13588
14980
  function unknownPasswordError2(port) {
13589
14981
  return new Error(
13590
- `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
14982
+ `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.`
13591
14983
  );
13592
14984
  }
13593
14985
  var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
@@ -13605,9 +14997,9 @@ async function ensureOpenCode2Running(ctx) {
13605
14997
  password: null
13606
14998
  };
13607
14999
  }
13608
- if (!isOpenCode2Installed()) {
15000
+ if (!ctx.command && !isOpenCode2Installed()) {
13609
15001
  throw new Error(
13610
- `OpenCode V2 (opencode2) is not installed. Install it with: npm install -g ${OPENCODE2_PACKAGE_SPEC}`
15002
+ "OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
13611
15003
  );
13612
15004
  }
13613
15005
  let port = ctx.port;
@@ -13635,7 +15027,8 @@ Port ${port} is already in use.`));
13635
15027
  if (!ctx.interactive) {
13636
15028
  ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
13637
15029
  const { child: proc, password } = await startOpenCode2(port, {
13638
- inheritStdio: ctx.inheritStdio
15030
+ inheritStdio: ctx.inheritStdio,
15031
+ command: ctx.command
13639
15032
  });
13640
15033
  const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
13641
15034
  if (!health.healthy) {
@@ -13656,13 +15049,14 @@ Port ${port} is already in use.`));
13656
15049
  password
13657
15050
  };
13658
15051
  }
15052
+ const command = ctx.command ?? "opencode2";
13659
15053
  const action = await select3({
13660
15054
  message: "OpenCode V2 is not running. What would you like to do?",
13661
15055
  choices: [
13662
15056
  {
13663
15057
  name: "Start OpenCode V2 for me",
13664
15058
  value: "start",
13665
- description: `Run 'opencode2 serve --port ${port}'`
15059
+ description: `Run '${command} serve --port ${port}'`
13666
15060
  },
13667
15061
  {
13668
15062
  name: "Show me the command",
@@ -13680,14 +15074,15 @@ Port ${port} is already in use.`));
13680
15074
  blank();
13681
15075
  console.log(chalk6.bold("Run this command in another terminal:"));
13682
15076
  blank();
13683
- console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
15077
+ console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
13684
15078
  blank();
13685
15079
  throw new Error("Please start OpenCode V2 manually");
13686
15080
  }
13687
15081
  if (action === "start") {
13688
15082
  const spinner = ora3("Starting OpenCode V2...").start();
13689
15083
  const { child: proc, password } = await startOpenCode2(port, {
13690
- inheritStdio: ctx.inheritStdio
15084
+ inheritStdio: ctx.inheritStdio,
15085
+ command: ctx.command
13691
15086
  });
13692
15087
  const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
13693
15088
  if (!health.healthy) {
@@ -13713,7 +15108,7 @@ Port ${port} is already in use.`));
13713
15108
  }
13714
15109
 
13715
15110
  // src/lib/runner-credentials.ts
13716
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
15111
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
13717
15112
  import { spawn as spawn5 } from "node:child_process";
13718
15113
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
13719
15114
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
@@ -13956,8 +15351,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
13956
15351
  }
13957
15352
  try {
13958
15353
  env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
13959
- writeFileSync4(GIT_CONFIG_GLOBAL, "");
13960
- writeFileSync4(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
15354
+ writeFileSync7(GIT_CONFIG_GLOBAL, "");
15355
+ writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
13961
15356
  chmodSync2(GIT_CREDENTIAL_HELPER, 448);
13962
15357
  const config = [
13963
15358
  ["user.name", env.GIT_USER_NAME ?? "evident-bot"],
@@ -13990,7 +15385,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
13990
15385
  // src/lib/opencode/config-overlay.ts
13991
15386
  import { execFileSync as execFileSync2 } from "node:child_process";
13992
15387
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
13993
- import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
15388
+ import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
13994
15389
  function parseConfigContent(content) {
13995
15390
  if (!content?.trim()) return {};
13996
15391
  const parsed = JSON.parse(content);
@@ -14020,7 +15415,7 @@ function applyRunnerOpenCodeConfig({
14020
15415
  return;
14021
15416
  }
14022
15417
  const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
14023
- const target = isFile(join11(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
15418
+ const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
14024
15419
  if (!isFile(source)) {
14025
15420
  log3(
14026
15421
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -14028,7 +15423,7 @@ function applyRunnerOpenCodeConfig({
14028
15423
  );
14029
15424
  return;
14030
15425
  }
14031
- copyFileSync(source, join11(cwd, target));
15426
+ copyFileSync(source, join14(cwd, target));
14032
15427
  try {
14033
15428
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
14034
15429
  stdio: "ignore"
@@ -14037,13 +15432,13 @@ function applyRunnerOpenCodeConfig({
14037
15432
  const detail = error2 instanceof Error ? error2.message : String(error2);
14038
15433
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
14039
15434
  }
14040
- log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
15435
+ log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
14041
15436
  }
14042
15437
 
14043
15438
  // src/lib/opencode/runtime-provenance.ts
14044
- import { readFileSync as readFileSync7, readdirSync } from "node:fs";
15439
+ import { readFileSync as readFileSync11, readdirSync } from "node:fs";
14045
15440
  import { homedir as homedir7 } from "node:os";
14046
- import { join as join12 } from "node:path";
15441
+ import { join as join15 } from "node:path";
14047
15442
  var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
14048
15443
  var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
14049
15444
  function errorKind(error2) {
@@ -14111,7 +15506,7 @@ function parseJsonc(value) {
14111
15506
  return JSON.parse(output);
14112
15507
  }
14113
15508
  function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14114
- return join12(env.XDG_CACHE_HOME ?? join12(homeDir, ".cache"), "opencode");
15509
+ return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
14115
15510
  }
14116
15511
  function readConfiguredPluginSpecs({
14117
15512
  cwd = process.cwd(),
@@ -14120,10 +15515,10 @@ function readConfiguredPluginSpecs({
14120
15515
  let config;
14121
15516
  try {
14122
15517
  try {
14123
- config = parseJsonc(readFileSync7(join12(cwd, "opencode.jsonc"), "utf8"));
15518
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
14124
15519
  } catch (error2) {
14125
15520
  if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14126
- config = parseJsonc(readFileSync7(join12(cwd, "opencode.json"), "utf8"));
15521
+ config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
14127
15522
  }
14128
15523
  } catch (error2) {
14129
15524
  log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
@@ -14151,7 +15546,7 @@ function resolveInstalledPluginVersions({
14151
15546
  homeDir = homedir7(),
14152
15547
  log: log3
14153
15548
  }) {
14154
- const packagesRoot = join12(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
15549
+ const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14155
15550
  let directories;
14156
15551
  try {
14157
15552
  directories = readdirSync(packagesRoot, { withFileTypes: true });
@@ -14172,8 +15567,8 @@ function resolveInstalledPluginVersions({
14172
15567
  let resolvedVersion = null;
14173
15568
  try {
14174
15569
  const packageJson = JSON.parse(
14175
- readFileSync7(
14176
- join12(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
15570
+ readFileSync11(
15571
+ join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14177
15572
  "utf8"
14178
15573
  )
14179
15574
  );
@@ -14194,7 +15589,7 @@ function resolveInstalledPluginVersions({
14194
15589
  }
14195
15590
 
14196
15591
  // src/lib/credential-sync.ts
14197
- import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
15592
+ import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
14198
15593
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
14199
15594
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
14200
15595
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -14204,7 +15599,7 @@ var MAX_FLUSH_PASSES = 2;
14204
15599
  function outcomesWith(outcome) {
14205
15600
  return { claude: outcome, opencode: outcome };
14206
15601
  }
14207
- function errorMessage2(error2) {
15602
+ function errorMessage3(error2) {
14208
15603
  return error2 instanceof Error ? error2.message : String(error2);
14209
15604
  }
14210
15605
  function waitForSettlement(promise, timeoutMs) {
@@ -14228,10 +15623,10 @@ function writeMarker(markerPath, outcomes, log3) {
14228
15623
  `;
14229
15624
  const temporaryPath = `${markerPath}.tmp`;
14230
15625
  try {
14231
- writeFileSync5(temporaryPath, body, { mode: 384 });
15626
+ writeFileSync8(temporaryPath, body, { mode: 384 });
14232
15627
  renameSync(temporaryPath, markerPath);
14233
15628
  } catch (error2) {
14234
- log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
15629
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
14235
15630
  }
14236
15631
  }
14237
15632
  function intervalSeconds(env, log3) {
@@ -14263,7 +15658,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
14263
15658
  },
14264
15659
  (error2) => {
14265
15660
  failed = true;
14266
- log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage2(error2)}`, "warn");
15661
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
14267
15662
  }
14268
15663
  );
14269
15664
  const abortTimer = setTimeout(() => controller.abort(), remainingMs);
@@ -14323,7 +15718,7 @@ function createCredentialSync({
14323
15718
  outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
14324
15719
  } catch (error2) {
14325
15720
  outcomes[store] = "failed";
14326
- log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage2(error2)}`, "debug");
15721
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
14327
15722
  }
14328
15723
  }
14329
15724
  const failed = STORES.some((store) => outcomes[store] === "failed");
@@ -14473,6 +15868,18 @@ function readSetup(value) {
14473
15868
  }
14474
15869
  return setup;
14475
15870
  }
15871
+ function readModelDescriptors(value) {
15872
+ if (!Array.isArray(value)) return null;
15873
+ return value.every(
15874
+ (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
15875
+ ) ? value : null;
15876
+ }
15877
+ function logDescriptorRefreshFallback(options, branch, generation) {
15878
+ options.log(
15879
+ `MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
15880
+ "warn"
15881
+ );
15882
+ }
14476
15883
  function clearLocalSetup(options) {
14477
15884
  const env = options.env ?? process.env;
14478
15885
  const store = options.store ?? defaultStore;
@@ -14604,7 +16011,27 @@ async function reaffirmStored(options, credential) {
14604
16011
  if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
14605
16012
  return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
14606
16013
  }
14607
- const failure = applyCredential(options, credential);
16014
+ const descriptorsValue = body?.model_descriptors;
16015
+ const refreshedDescriptors = readModelDescriptors(descriptorsValue);
16016
+ let credentialToApply = credential;
16017
+ if (descriptorsValue === void 0 || descriptorsValue === null) {
16018
+ logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
16019
+ } else if (refreshedDescriptors === null) {
16020
+ logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
16021
+ } else if (refreshedDescriptors.length === 0) {
16022
+ logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
16023
+ } else {
16024
+ const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
16025
+ inferenceBaseUrl: credential.inferenceBaseUrl,
16026
+ modelDescriptors: refreshedDescriptors
16027
+ });
16028
+ if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
16029
+ logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
16030
+ } else {
16031
+ credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
16032
+ }
16033
+ }
16034
+ const failure = applyCredential(options, credentialToApply);
14608
16035
  return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
14609
16036
  }
14610
16037
  async function prepareManagedInferenceSetup(options, discovery, health) {
@@ -14638,10 +16065,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
14638
16065
  const setup = readSetup(body?.setup);
14639
16066
  const token = body?.inference_token;
14640
16067
  const inferenceBaseUrl = body?.inference_base_url;
14641
- const modelDescriptors = body?.model_descriptors;
14642
- if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || !Array.isArray(modelDescriptors) || !modelDescriptors.every(
14643
- (descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
14644
- )) {
16068
+ const modelDescriptors = readModelDescriptors(body?.model_descriptors);
16069
+ if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
14645
16070
  return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
14646
16071
  }
14647
16072
  const credential = {
@@ -14710,33 +16135,49 @@ function resolveLogLevel(options) {
14710
16135
  }
14711
16136
  return "info";
14712
16137
  }
14713
- function resolveFileSyncDirectories(raw, homeDir) {
14714
- const directories = [];
16138
+ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
16139
+ const trimmed = entry.trim();
16140
+ if (trimmed === "") {
16141
+ throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
16142
+ }
16143
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
16144
+ if (!isAbsolute4(expanded)) {
16145
+ throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
16146
+ }
16147
+ const normalized = resolvePath2(expanded);
16148
+ if (parse(normalized).root === normalized) {
16149
+ throw new Error(
16150
+ `${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
16151
+ );
16152
+ }
16153
+ return normalized;
16154
+ }
16155
+ function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
16156
+ const paths = [];
14715
16157
  for (const entry of raw ?? []) {
14716
- const trimmed = entry.trim();
14717
- if (trimmed === "") {
14718
- throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
14719
- }
14720
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
14721
- if (!isAbsolute4(expanded)) {
14722
- throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
14723
- }
14724
- const normalized = resolvePath2(expanded);
14725
- if (parse(normalized).root === normalized) {
14726
- throw new Error(
14727
- `--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
14728
- );
14729
- }
14730
- if (!directories.includes(normalized)) {
14731
- directories.push(normalized);
14732
- }
16158
+ const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
16159
+ if (!paths.includes(normalized)) paths.push(normalized);
14733
16160
  }
14734
- if (directories.length > MAX_FILE_SYNC_DIRECTORIES) {
16161
+ if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
14735
16162
  throw new Error(
14736
- `--enable-file-sync-to accepts at most ${MAX_FILE_SYNC_DIRECTORIES} directories; got ${directories.length}`
16163
+ `${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
14737
16164
  );
14738
16165
  }
14739
- return directories;
16166
+ return paths;
16167
+ }
16168
+ function resolveFileSyncDirectories(raw, homeDir) {
16169
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
16170
+ noun: "directory",
16171
+ plural: "directories",
16172
+ rootExample: "~/.claude"
16173
+ });
16174
+ }
16175
+ function resolveFileSyncExactFiles(raw, homeDir) {
16176
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
16177
+ noun: "file",
16178
+ plural: "files",
16179
+ rootExample: "~/.local/share/opencode/mcp-auth.json"
16180
+ });
14740
16181
  }
14741
16182
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
14742
16183
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
@@ -14752,13 +16193,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
14752
16193
  raw = env[OPENCODE_VERSION_ENV];
14753
16194
  source = OPENCODE_VERSION_ENV;
14754
16195
  } else {
14755
- return { version: "v1", warnings: [] };
16196
+ return { version: "auto", warnings: [] };
14756
16197
  }
14757
16198
  const normalized = raw.trim().toLowerCase();
14758
- if (normalized !== "v1" && normalized !== "v2") {
16199
+ if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
14759
16200
  return {
14760
- version: "v1",
14761
- warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
16201
+ version: "auto",
16202
+ warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
14762
16203
  };
14763
16204
  }
14764
16205
  return { version: normalized, warnings: [] };
@@ -15268,8 +16709,8 @@ async function driveChannels(state, driver) {
15268
16709
  state.running = false;
15269
16710
  break;
15270
16711
  }
15271
- const errorMessage3 = error2 instanceof Error ? error2.message : String(error2);
15272
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage3}` });
16712
+ const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
16713
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
15273
16714
  if (state.interactive) displayStatus(state);
15274
16715
  if (driver.hasInFlightWatchers()) {
15275
16716
  consecutiveDrainFailures = 0;
@@ -15359,6 +16800,7 @@ async function driveChannels(state, driver) {
15359
16800
  }
15360
16801
  }
15361
16802
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
16803
+ var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
15362
16804
  var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
15363
16805
  var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
15364
16806
  var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
@@ -15373,7 +16815,7 @@ function shouldWarnForReclaimSkip(reason) {
15373
16815
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
15374
16816
  }
15375
16817
  function sessionDbPath() {
15376
- return join13(homedir8(), ".local", "share", "opencode", "opencode.db");
16818
+ return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
15377
16819
  }
15378
16820
  function logSessionCleanupActivity(state, phase, entry) {
15379
16821
  logActivity(state, {
@@ -15382,6 +16824,104 @@ function logSessionCleanupActivity(state, phase, entry) {
15382
16824
  metadata: { ...entry.metadata, phase }
15383
16825
  });
15384
16826
  }
16827
+ function logSessionArchiveActivity(state, phase, entry) {
16828
+ logActivity(state, {
16829
+ ...entry,
16830
+ source: "cli.session-archive",
16831
+ metadata: { ...entry.metadata, phase }
16832
+ });
16833
+ }
16834
+ function logSessionArchiveSummary(state, phase, summary) {
16835
+ const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
16836
+ const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
16837
+ const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
16838
+ const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed;
16839
+ logSessionArchiveActivity(state, phase, {
16840
+ type: "info",
16841
+ level: warning2 ? "warn" : "info",
16842
+ message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}` + staleSince + neverWatermarked,
16843
+ metadata: {
16844
+ outcome: "archive-run-complete",
16845
+ sessions_inspected: summary.inspected,
16846
+ sessions_uploaded: summary.uploaded,
16847
+ sessions_unchanged: summary.unchanged,
16848
+ sessions_failed: summary.failed,
16849
+ uploads_attempted: summary.uploadAttempts,
16850
+ uploads_failed: summary.uploadFailures,
16851
+ sessions_skipped_budget: summary.skippedBudget,
16852
+ sessions_unmapped: summary.unmappedIds.length,
16853
+ sessions_without_prior_watermark: summary.neverWatermarkedFailures,
16854
+ ...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
16855
+ }
16856
+ });
16857
+ for (const failure of summary.failures) {
16858
+ const permanentSizeFailure = failure.reason === "too-large";
16859
+ const safeMessage = failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
16860
+ logSessionArchiveActivity(state, phase, {
16861
+ type: "info",
16862
+ level: "warn",
16863
+ message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + safeMessage,
16864
+ metadata: {
16865
+ outcome: failure.outcome,
16866
+ session_id: failure.sessionId,
16867
+ archived: failure.archived,
16868
+ ...failure.reason !== void 0 ? { reason: failure.reason } : {}
16869
+ }
16870
+ });
16871
+ }
16872
+ if (summary.unmappedIds.length > 0) {
16873
+ logSessionArchiveActivity(state, phase, {
16874
+ type: "info",
16875
+ level: "warn",
16876
+ message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
16877
+ metadata: {
16878
+ outcome: "unmapped-sessions",
16879
+ sessions_unmapped: summary.unmappedIds.length
16880
+ }
16881
+ });
16882
+ }
16883
+ }
16884
+ async function drainSessionDeletionReports(state) {
16885
+ const path = sessionDeletionReportPath(homedir8(), process.env);
16886
+ let delivered = 0;
16887
+ let failure = null;
16888
+ try {
16889
+ const spool = readSessionDeletionRecords(path);
16890
+ if (spool.readError !== null) {
16891
+ failure = `could not read spool: ${spool.readError}`;
16892
+ } else if (spool.records.length === 0) {
16893
+ return;
16894
+ } else if (!state.agentId || !state.authHeader) {
16895
+ failure = "runner authentication is not available";
16896
+ } else {
16897
+ for (let offset = 0; offset < spool.records.length; offset += 200) {
16898
+ const batch = spool.records.slice(offset, offset + 200);
16899
+ const result = await reportSessionDeletions(state.agentId, state.authHeader, batch);
16900
+ if (!result.ok) {
16901
+ failure = result.error ?? "unknown API error";
16902
+ break;
16903
+ }
16904
+ removeDeliveredSessionDeletionRecords(path, batch);
16905
+ delivered += batch.length;
16906
+ }
16907
+ }
16908
+ } catch (error2) {
16909
+ failure = error2 instanceof Error ? error2.message : String(error2);
16910
+ }
16911
+ const pending = readSessionDeletionRecords(path);
16912
+ const pendingCount = pending.readError === null ? pending.records.length : null;
16913
+ const message = `Session deletion reports: delivered ${delivered}, ${pendingCount === null ? "pending count unavailable" : `${pendingCount} pending`}` + (failure === null ? "." : `; delivery failed: ${failure}.`);
16914
+ logActivity(state, {
16915
+ type: "info",
16916
+ level: failure === null ? "info" : "warn",
16917
+ message,
16918
+ metadata: {
16919
+ outcome: failure === null ? "session-deletion-reports-drained" : "session-deletion-reports-pending",
16920
+ delivered,
16921
+ pending: pendingCount
16922
+ }
16923
+ });
16924
+ }
15385
16925
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
15386
16926
  const record = {
15387
16927
  v: 1,
@@ -15438,18 +16978,25 @@ async function runSweep(state, driver, config) {
15438
16978
  let failedOtherStatus = 0;
15439
16979
  let failedThrew = 0;
15440
16980
  let skippedNewlyActive = 0;
16981
+ let skippedArchive = 0;
15441
16982
  let remaining = 0;
16983
+ const deletionRecords = [];
15442
16984
  let protectedNow = driver.protectedSessionIds();
15443
16985
  if (sessions !== null) {
15444
- const toDelete = selectSessionsToDelete(
15445
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
15446
- {
15447
- maxAgeMs: config.maxAgeMs,
15448
- maxCount: config.maxCount,
15449
- nowMs: Date.now(),
15450
- protectedIds: protectedNow
15451
- }
16986
+ const snapshots = sessions.map((session) => ({
16987
+ id: session.id,
16988
+ lastActivityMs: sessionLastActivityMs(session)
16989
+ }));
16990
+ const lastActivityById = new Map(
16991
+ snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
15452
16992
  );
16993
+ const sessionById = new Map(sessions.map((session) => [session.id, session]));
16994
+ const toDelete = selectSessionsToDelete(snapshots, {
16995
+ maxAgeMs: config.maxAgeMs,
16996
+ maxCount: config.maxCount,
16997
+ nowMs: Date.now(),
16998
+ protectedIds: protectedNow
16999
+ });
15453
17000
  const eligibleToDelete = toDelete.filter(
15454
17001
  (id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
15455
17002
  );
@@ -15466,9 +17013,56 @@ async function runSweep(state, driver, config) {
15466
17013
  });
15467
17014
  continue;
15468
17015
  }
17016
+ if (state.sessionArchiveConfig.enabled) {
17017
+ const candidate = sessionById.get(id);
17018
+ const archiveConfig = state.sessionArchiveConfig;
17019
+ const archiveBudgetMs = Math.min(
17020
+ archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
17021
+ MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
17022
+ );
17023
+ const archived = candidate ? await runSessionArchiveTask(
17024
+ state,
17025
+ "sweep",
17026
+ archiveBudgetMs,
17027
+ (signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
17028
+ ) : null;
17029
+ if (!archived?.archivedSessionIds.includes(id)) {
17030
+ skippedArchive++;
17031
+ logSessionArchiveActivity(state, "sweep", {
17032
+ type: "info",
17033
+ level: "warn",
17034
+ message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
17035
+ metadata: {
17036
+ outcome: "deletion-held-for-archive",
17037
+ session_id: id
17038
+ }
17039
+ });
17040
+ continue;
17041
+ }
17042
+ protectedNow = driver.protectedSessionIds();
17043
+ if (protectedNow.has(id)) {
17044
+ skippedNewlyActive++;
17045
+ logActivity(state, {
17046
+ type: "info",
17047
+ message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
17048
+ });
17049
+ continue;
17050
+ }
17051
+ }
15469
17052
  const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
15470
17053
  if (outcome.ok) {
15471
17054
  deleted++;
17055
+ const lastActivityMs = lastActivityById.get(id) ?? null;
17056
+ deletionRecords.push({
17057
+ opencode_session_id: id,
17058
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
17059
+ reason: "auto_cleanup",
17060
+ policy: {
17061
+ max_age_ms: config.maxAgeMs ?? null,
17062
+ max_count: config.maxCount ?? null,
17063
+ last_activity_at: lastActivityMs === null ? null : new Date(lastActivityMs).toISOString()
17064
+ }
17065
+ });
15472
17066
  } else if (outcome.status === "threw") {
15473
17067
  failed++;
15474
17068
  failedThrew++;
@@ -15518,12 +17112,39 @@ async function runSweep(state, driver, config) {
15518
17112
  }
15519
17113
  const failedNote = failed > 0 ? `, failed ${failed}` : "";
15520
17114
  const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
17115
+ const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
15521
17116
  const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
15522
17117
  logActivity(state, {
15523
17118
  type: "info",
15524
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
17119
+ message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
15525
17120
  });
15526
17121
  }
17122
+ if (deletionRecords.length > 0) {
17123
+ try {
17124
+ const dropped = appendSessionDeletionRecords(
17125
+ sessionDeletionReportPath(homedir8(), process.env),
17126
+ deletionRecords
17127
+ );
17128
+ if (dropped > 0) {
17129
+ logSessionCleanupActivity(state, "sweep", {
17130
+ type: "info",
17131
+ level: "warn",
17132
+ message: `Session cleanup: dropped ${dropped} oldest pending deletion report(s) after reaching the spool limit`,
17133
+ metadata: { outcome: "deletion-reports-dropped", dropped_count: dropped }
17134
+ });
17135
+ }
17136
+ } catch (error2) {
17137
+ logSessionCleanupActivity(state, "sweep", {
17138
+ type: "info",
17139
+ level: "warn",
17140
+ message: `Session cleanup: could not persist ${deletionRecords.length} confirmed deletion report(s): ${error2 instanceof Error ? error2.message : String(error2)}`,
17141
+ metadata: {
17142
+ outcome: "deletion-report-spool-write-failed",
17143
+ records: deletionRecords.length
17144
+ }
17145
+ });
17146
+ }
17147
+ }
15527
17148
  const sessionMetadata = {
15528
17149
  sessions_inspected: sessions?.length ?? null,
15529
17150
  sessions_deleted: deleted,
@@ -15533,6 +17154,7 @@ async function runSweep(state, driver, config) {
15533
17154
  sessions_failed_other: failedOtherStatus,
15534
17155
  sessions_failed_threw: failedThrew,
15535
17156
  sessions_skipped_active: skippedNewlyActive,
17157
+ sessions_archive_held: skippedArchive,
15536
17158
  sessions_remaining: remaining,
15537
17159
  ...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
15538
17160
  };
@@ -15593,6 +17215,7 @@ async function runSweep(state, driver, config) {
15593
17215
  }
15594
17216
  });
15595
17217
  }
17218
+ await drainSessionDeletionReports(state);
15596
17219
  } catch (error2) {
15597
17220
  const message = error2 instanceof Error ? error2.message : String(error2);
15598
17221
  logSessionCleanupActivity(state, "sweep", {
@@ -15663,6 +17286,186 @@ function scheduleSessionCleanup(state, driver, options) {
15663
17286
  );
15664
17287
  state.sessionCleanupTimers.push(interval, firstSweep);
15665
17288
  }
17289
+ function runSessionArchiveTask(state, phase, budgetMs, work) {
17290
+ if (state.sessionArchiveInFlight) {
17291
+ logSessionArchiveActivity(state, phase, {
17292
+ type: "info",
17293
+ level: "debug",
17294
+ message: "Session archive skipped \u2014 previous archive run still in progress",
17295
+ metadata: { outcome: "skipped-in-flight" }
17296
+ });
17297
+ return Promise.resolve(null);
17298
+ }
17299
+ const controller = new AbortController();
17300
+ state.sessionArchiveAbortController = controller;
17301
+ const timeout = setTimeout(() => {
17302
+ controller.abort();
17303
+ logSessionArchiveActivity(state, phase, {
17304
+ type: "info",
17305
+ level: "warn",
17306
+ message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
17307
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17308
+ });
17309
+ }, budgetMs);
17310
+ const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
17311
+ logSessionArchiveActivity(state, phase, {
17312
+ type: "info",
17313
+ level: "warn",
17314
+ message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
17315
+ metadata: { outcome: "archive-run-failed" }
17316
+ });
17317
+ return null;
17318
+ }).finally(() => {
17319
+ clearTimeout(timeout);
17320
+ if (state.sessionArchiveInFlight === task) {
17321
+ state.sessionArchiveInFlight = null;
17322
+ state.sessionArchiveAbortController = null;
17323
+ }
17324
+ });
17325
+ state.sessionArchiveInFlight = task;
17326
+ return task;
17327
+ }
17328
+ async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
17329
+ const config = state.sessionArchiveConfig;
17330
+ const client = state.opencodeClient;
17331
+ const watermarkPath = state.sessionArchiveWatermarkPath;
17332
+ if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
17333
+ logSessionArchiveActivity(state, phase, {
17334
+ type: "info",
17335
+ level: "warn",
17336
+ message: "Session archive skipped because runner identity or archive configuration is unavailable",
17337
+ metadata: { outcome: "archive-configuration-unavailable" }
17338
+ });
17339
+ return null;
17340
+ }
17341
+ const summary = await runSessionArchive({
17342
+ sessions,
17343
+ deps: {
17344
+ client,
17345
+ runnerId: state.agentId,
17346
+ keyPrefix: config.keyPrefix,
17347
+ bindings: readSessionBindings(
17348
+ sessionBindingLedgerPath(homedir8(), process.env),
17349
+ (message) => logSessionArchiveActivity(state, phase, {
17350
+ type: "info",
17351
+ level: "warn",
17352
+ message: redactDiagnosticText(message),
17353
+ metadata: { outcome: "binding-ledger-read-failed" }
17354
+ })
17355
+ ),
17356
+ watermarks: state.sessionArchiveWatermarks,
17357
+ watermarkPath,
17358
+ signal,
17359
+ upload: uploadSessionArchive,
17360
+ exportMessages: exportSessionMessages,
17361
+ recordWatermark: recordUpload
17362
+ },
17363
+ budgetMs
17364
+ });
17365
+ logSessionArchiveSummary(state, phase, summary);
17366
+ return summary;
17367
+ }
17368
+ async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
17369
+ const startedAt = Date.now();
17370
+ const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
17371
+ if (enumeration.sessions === null) {
17372
+ const interrupted = signal.aborted;
17373
+ logSessionArchiveActivity(state, phase, {
17374
+ type: "info",
17375
+ level: "warn",
17376
+ 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`,
17377
+ metadata: {
17378
+ outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
17379
+ ...!interrupted ? { reason: enumeration.reason } : {}
17380
+ }
17381
+ });
17382
+ return null;
17383
+ }
17384
+ return runSessionArchiveBatch(
17385
+ state,
17386
+ phase,
17387
+ enumeration.sessions,
17388
+ Math.max(0, budgetMs - (Date.now() - startedAt)),
17389
+ signal
17390
+ );
17391
+ }
17392
+ async function runSessionArchiveShutdownFlush(state, budgetMs) {
17393
+ const deadline = Date.now() + budgetMs;
17394
+ const previousRun = state.sessionArchiveInFlight;
17395
+ if (previousRun) {
17396
+ state.sessionArchiveAbortController?.abort(
17397
+ new Error("graceful shutdown archive flush requested")
17398
+ );
17399
+ let timeout;
17400
+ const settled = await Promise.race([
17401
+ previousRun.then(() => true),
17402
+ new Promise((resolve5) => {
17403
+ timeout = setTimeout(() => resolve5(false), budgetMs);
17404
+ })
17405
+ ]);
17406
+ if (timeout) clearTimeout(timeout);
17407
+ if (!settled) {
17408
+ state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
17409
+ logSessionArchiveActivity(state, "shutdown", {
17410
+ type: "info",
17411
+ level: "warn",
17412
+ message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
17413
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17414
+ });
17415
+ return null;
17416
+ }
17417
+ }
17418
+ const remainingMs = Math.floor(deadline - Date.now());
17419
+ if (remainingMs <= 0) {
17420
+ logSessionArchiveActivity(state, "shutdown", {
17421
+ type: "info",
17422
+ level: "warn",
17423
+ message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
17424
+ metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
17425
+ });
17426
+ return null;
17427
+ }
17428
+ return runSessionArchiveTask(
17429
+ state,
17430
+ "shutdown",
17431
+ remainingMs,
17432
+ (signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
17433
+ );
17434
+ }
17435
+ function scheduleSessionArchive(state) {
17436
+ const config = state.sessionArchiveConfig;
17437
+ if (!config.enabled) return;
17438
+ state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
17439
+ state.sessionArchiveWatermarks = readWatermarks(
17440
+ state.sessionArchiveWatermarkPath,
17441
+ (message) => logSessionArchiveActivity(state, "startup", {
17442
+ type: "info",
17443
+ level: "warn",
17444
+ message: redactDiagnosticText(message),
17445
+ metadata: { outcome: "watermark-read-failed" }
17446
+ })
17447
+ );
17448
+ logSessionArchiveActivity(state, "startup", {
17449
+ type: "info",
17450
+ message: `Session archive enabled (interval=${config.intervalMs}ms)`,
17451
+ metadata: { outcome: "armed", interval_ms: config.intervalMs }
17452
+ });
17453
+ const cycleBudgetMs = Math.min(
17454
+ config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
17455
+ MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
17456
+ );
17457
+ const run2 = (phase = "scheduled") => {
17458
+ void runSessionArchiveTask(
17459
+ state,
17460
+ phase,
17461
+ cycleBudgetMs,
17462
+ (signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
17463
+ );
17464
+ };
17465
+ const interval = setInterval(run2, config.intervalMs);
17466
+ const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
17467
+ state.sessionArchiveTimers.push(interval, firstRun);
17468
+ }
15666
17469
  function scheduleUsageReporting(state, params) {
15667
17470
  const { mode, warnings } = params.resolved;
15668
17471
  for (const warning2 of warnings) {
@@ -15937,6 +17740,12 @@ async function cleanup(state, opts = {}) {
15937
17740
  clearTimeout(timer);
15938
17741
  }
15939
17742
  state.sessionCleanupTimers = [];
17743
+ for (const timer of state.sessionArchiveTimers) {
17744
+ clearInterval(timer);
17745
+ clearTimeout(timer);
17746
+ }
17747
+ state.sessionArchiveTimers = [];
17748
+ state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
15940
17749
  state.stopOpenCodeLogTail?.();
15941
17750
  state.stopOpenCodeLogTail = null;
15942
17751
  if (state.claudeUsageTimer) {
@@ -16004,6 +17813,15 @@ async function cleanup(state, opts = {}) {
16004
17813
  await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
16005
17814
  state.connection = null;
16006
17815
  }
17816
+ if (opts.graceful && state.sessionArchiveConfig.enabled) {
17817
+ const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
17818
+ await timeShutdownPhase(
17819
+ state,
17820
+ durations,
17821
+ "session_archive_flush",
17822
+ () => runSessionArchiveShutdownFlush(state, budgetMs)
17823
+ );
17824
+ }
16007
17825
  if (state.opencodeProcess) {
16008
17826
  const opencodeProcess = state.opencodeProcess;
16009
17827
  const result = await timeShutdownPhase(
@@ -16075,9 +17893,11 @@ async function run(options) {
16075
17893
  const interactive = isInteractive(options.json);
16076
17894
  let logLevel;
16077
17895
  let fileSyncDirectories;
17896
+ let fileSyncExactFiles;
16078
17897
  try {
16079
17898
  logLevel = resolveLogLevel(options);
16080
17899
  fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
17900
+ fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
16081
17901
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
16082
17902
  throw new Error(
16083
17903
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -16125,6 +17945,18 @@ async function run(options) {
16125
17945
  messageCount: 0,
16126
17946
  lastProxiedActivityAt: null,
16127
17947
  sessionCleanupTimers: [],
17948
+ sessionArchiveConfig: resolveSessionArchiveConfig(
17949
+ {
17950
+ enabled: options.sessionArchive,
17951
+ interval: options.sessionArchiveInterval
17952
+ },
17953
+ process.env
17954
+ ),
17955
+ sessionArchiveTimers: [],
17956
+ sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
17957
+ sessionArchiveWatermarkPath: null,
17958
+ sessionArchiveInFlight: null,
17959
+ sessionArchiveAbortController: null,
16128
17960
  sessionDeleteFailures: /* @__PURE__ */ new Map(),
16129
17961
  claudeUsageTimer: null,
16130
17962
  claudeUsageRearm: null,
@@ -16138,6 +17970,14 @@ async function run(options) {
16138
17970
  };
16139
17971
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
16140
17972
  setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
17973
+ for (const warning2 of state.sessionArchiveConfig.warnings) {
17974
+ logSessionArchiveActivity(state, "startup", {
17975
+ type: "info",
17976
+ level: "warn",
17977
+ message: `Session archive: ${warning2}`,
17978
+ metadata: { outcome: "configuration-warning" }
17979
+ });
17980
+ }
16141
17981
  if (options.credentialSyncMarker) {
16142
17982
  state.credentialSync = createCredentialSync({
16143
17983
  markerPath: options.credentialSyncMarker,
@@ -16153,7 +17993,11 @@ async function run(options) {
16153
17993
  }
16154
17994
  if (fileSyncDirectories.length > 0) {
16155
17995
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
16156
- } else {
17996
+ }
17997
+ if (fileSyncExactFiles.length > 0) {
17998
+ log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
17999
+ }
18000
+ if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
16157
18001
  log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
16158
18002
  }
16159
18003
  if (!options.runner && options.agent) {
@@ -16435,6 +18279,7 @@ async function run(options) {
16435
18279
  }
16436
18280
  }
16437
18281
  reportSessionDbRecovery(state);
18282
+ await drainSessionDeletionReports(state);
16438
18283
  if (sessionDbVerifyFatal) {
16439
18284
  throw new Error(
16440
18285
  sessionDbVerifyFatalReason === void 0 ? "SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk" : `SESSION-DB-VERIFY-TIMEOUT: session DB verification timed out (${sessionDbVerifyFatalReason}); refusing to start`
@@ -16454,13 +18299,75 @@ async function run(options) {
16454
18299
  for (const warning2 of opencodeStartTimeoutWarnings) {
16455
18300
  logActivity(state, { type: "info", level: "warn", message: warning2 });
16456
18301
  }
16457
- const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
16458
- options,
16459
- process.env
16460
- );
18302
+ const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
16461
18303
  for (const warning2 of opencodeVersionWarnings) {
16462
18304
  logActivity(state, { type: "info", level: "warn", message: warning2 });
16463
18305
  }
18306
+ let opencodeMajor;
18307
+ let opencodeV2Command;
18308
+ let opencodeDetectionSource;
18309
+ let opencodeDetectionLevel = "info";
18310
+ let opencodeDetectionMessage;
18311
+ if (opencodeVersionSelection === "v1") {
18312
+ opencodeMajor = "v1";
18313
+ opencodeDetectionSource = "explicit";
18314
+ opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
18315
+ } else if (opencodeVersionSelection === "v2") {
18316
+ const installed = resolveInstalledOpenCode();
18317
+ opencodeMajor = "v2";
18318
+ opencodeDetectionSource = "explicit";
18319
+ if (installed.kind === "installed" && installed.major === "v2") {
18320
+ opencodeV2Command = installed.command;
18321
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
18322
+ } else if (installed.kind === "unknown") {
18323
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
18324
+ } else if (installed.kind === "installed") {
18325
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
18326
+ } else {
18327
+ opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
18328
+ }
18329
+ } else {
18330
+ const running = await classifyRunningOpenCode(state.port);
18331
+ if (running.kind === "v1") {
18332
+ opencodeMajor = "v1";
18333
+ opencodeDetectionSource = "running-server";
18334
+ opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
18335
+ } else if (running.kind === "v2") {
18336
+ opencodeMajor = "v2";
18337
+ opencodeDetectionSource = "running-server";
18338
+ opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
18339
+ } else if (running.kind === "authenticated-unknown") {
18340
+ 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.`;
18341
+ logActivity(state, {
18342
+ type: "info",
18343
+ level: "warn",
18344
+ message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
18345
+ });
18346
+ throw new Error(message);
18347
+ } else {
18348
+ const installed = resolveInstalledOpenCode();
18349
+ opencodeDetectionLevel = "warn";
18350
+ if (installed.kind === "installed") {
18351
+ opencodeMajor = installed.major;
18352
+ opencodeDetectionSource = "installed-binary";
18353
+ if (installed.major === "v2") opencodeV2Command = installed.command;
18354
+ 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"}.`;
18355
+ } else if (installed.kind === "unknown") {
18356
+ opencodeMajor = "v1";
18357
+ opencodeDetectionSource = "fallback-v1";
18358
+ 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.`;
18359
+ } else {
18360
+ opencodeMajor = "v1";
18361
+ opencodeDetectionSource = "fallback-v1";
18362
+ 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.`;
18363
+ }
18364
+ }
18365
+ }
18366
+ logActivity(state, {
18367
+ type: "info",
18368
+ level: opencodeDetectionLevel,
18369
+ message: opencodeDetectionMessage
18370
+ });
16464
18371
  const managedSetupOptions = {
16465
18372
  agentId: state.agentId,
16466
18373
  apiUrl: getApiUrlConfig(),
@@ -16481,7 +18388,7 @@ async function run(options) {
16481
18388
  managedPreparation = managedDiscovery;
16482
18389
  managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
16483
18390
  } else if (managedDiscovery.kind === "setup") {
16484
- const managedHealth = opencodeVersion === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
18391
+ const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
16485
18392
  managedPreparation = await prepareManagedInferenceSetup(
16486
18393
  managedSetupOptions,
16487
18394
  managedDiscovery,
@@ -16506,13 +18413,14 @@ async function run(options) {
16506
18413
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
16507
18414
  const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
16508
18415
  try {
16509
- const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
18416
+ const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
16510
18417
  port: state.port,
16511
18418
  interactive: state.interactive,
16512
18419
  agentId: state.agentId,
16513
18420
  log: (message) => log2(state, message),
16514
18421
  startTimeoutMs: opencodeStartTimeoutMs,
16515
- inheritStdio: Boolean(options.opencodePidFile)
18422
+ inheritStdio: Boolean(options.opencodePidFile),
18423
+ command: opencodeV2Command
16516
18424
  }) : await ensureOpenCodeRunning({
16517
18425
  port: state.port,
16518
18426
  interactive: state.interactive,
@@ -16524,7 +18432,7 @@ async function run(options) {
16524
18432
  state.port = oc.port;
16525
18433
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
16526
18434
  state.opencodeVersion = oc.version;
16527
- state.opencodeApiVersion = opencodeVersion;
18435
+ state.opencodeApiVersion = opencodeMajor;
16528
18436
  let configuredPlugins = [];
16529
18437
  let resolvedPlugins = [];
16530
18438
  try {
@@ -16546,13 +18454,14 @@ async function run(options) {
16546
18454
  metadata: {
16547
18455
  opencode_version: state.opencodeVersion,
16548
18456
  opencode_api_version: state.opencodeApiVersion,
18457
+ opencode_detection_source: opencodeDetectionSource,
16549
18458
  configured_plugins: JSON.stringify(configuredPlugins),
16550
18459
  resolved_plugins: JSON.stringify(resolvedPlugins)
16551
18460
  },
16552
18461
  source: "cli.runtime-provenance"
16553
18462
  });
16554
18463
  let opencodePassword = null;
16555
- if (opencodeVersion === "v2" && "password" in oc) {
18464
+ if (opencodeMajor === "v2" && "password" in oc) {
16556
18465
  const value = oc.password;
16557
18466
  if (typeof value === "string" || value === null) opencodePassword = value;
16558
18467
  }
@@ -16565,7 +18474,7 @@ async function run(options) {
16565
18474
  state.opencodeClient = openCodeClient;
16566
18475
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
16567
18476
  try {
16568
- writeFileSync6(options.opencodePidFile, `${oc.process.pid}
18477
+ writeFileSync9(options.opencodePidFile, `${oc.process.pid}
16569
18478
  `, { mode: 384 });
16570
18479
  chmodSync3(options.opencodePidFile, 384);
16571
18480
  } catch (error2) {
@@ -16675,7 +18584,7 @@ async function run(options) {
16675
18584
  let existingPid;
16676
18585
  if (existsSync3(options.litestreamPidFile)) {
16677
18586
  try {
16678
- const rawPid = readFileSync8(options.litestreamPidFile, "utf8").trim();
18587
+ const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
16679
18588
  const parsedPid = Number(rawPid);
16680
18589
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
16681
18590
  existingPid = parsedPid;
@@ -16712,7 +18621,7 @@ async function run(options) {
16712
18621
  });
16713
18622
  try {
16714
18623
  if (litestreamProcess.pid !== void 0) {
16715
- writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
18624
+ writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
16716
18625
  `, {
16717
18626
  mode: 384
16718
18627
  });
@@ -16762,6 +18671,15 @@ async function run(options) {
16762
18671
  log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
16763
18672
  }
16764
18673
  const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
18674
+ const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
18675
+ sessionBindingLedgerPath(homedir8(), process.env),
18676
+ {
18677
+ conversation_id: conversationId,
18678
+ opencode_session_id: sessionId,
18679
+ observed_at: (/* @__PURE__ */ new Date()).toISOString()
18680
+ },
18681
+ (message) => logActivity(state, { type: "info", level: "warn", message })
18682
+ ) : void 0;
16765
18683
  const channelDriver = new ChannelDriver({
16766
18684
  agentId: state.agentId,
16767
18685
  port: state.port,
@@ -16771,9 +18689,11 @@ async function run(options) {
16771
18689
  conversationFilter: state.conversationFilter,
16772
18690
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
16773
18691
  sessionStoreHealth: state.sessionStoreHealth,
16774
- // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
16775
- // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
18692
+ recordBinding,
18693
+ // #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
18694
+ // `file_sync_disabled` on the ack, not silently ignored.
16776
18695
  fileSyncDirectories,
18696
+ fileSyncExactFiles,
16777
18697
  homeDir: homedir8(),
16778
18698
  maxActiveSessions,
16779
18699
  log: (entry) => (
@@ -16949,6 +18869,7 @@ async function run(options) {
16949
18869
  }
16950
18870
  if (!state.running) return;
16951
18871
  scheduleSessionCleanup(state, channelDriver, options);
18872
+ scheduleSessionArchive(state);
16952
18873
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
16953
18874
  state.openaiUsageRearm = scheduleUsageReporting(state, {
16954
18875
  label: "OpenAI",
@@ -17044,8 +18965,8 @@ program.command("run").description("Connect to Evident and process messages").op
17044
18965
  "--opencode-start-timeout <seconds>",
17045
18966
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
17046
18967
  ).option(
17047
- "--opencode-version <v1|v2>",
17048
- "Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
18968
+ "--opencode-version <v1|v2|auto>",
18969
+ "OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
17049
18970
  ).option("--json", "Output in JSON format").option(
17050
18971
  "--session-cleanup-max-age <duration>",
17051
18972
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -17058,6 +18979,12 @@ program.command("run").description("Connect to Evident and process messages").op
17058
18979
  ).option(
17059
18980
  "--session-cleanup-interval <duration>",
17060
18981
  "How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
18982
+ ).option(
18983
+ "--session-archive",
18984
+ "Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
18985
+ ).option(
18986
+ "--session-archive-interval <duration>",
18987
+ "How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
17061
18988
  ).option(
17062
18989
  "--claude-usage-reporting <mode>",
17063
18990
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
@@ -17072,6 +18999,11 @@ program.command("run").description("Connect to Evident and process messages").op
17072
18999
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
17073
19000
  (value, previous) => previous.concat([value]),
17074
19001
  []
19002
+ ).option(
19003
+ "--enable-file-sync-to-exact-file <path>",
19004
+ "Allow Evident to write files to exactly this path (repeatable).",
19005
+ (value, previous) => previous.concat([value]),
19006
+ []
17075
19007
  ).option(
17076
19008
  "--tunnel-ready-file <path>",
17077
19009
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
@@ -17124,6 +19056,8 @@ program.command("run").description("Connect to Evident and process messages").op
17124
19056
  sessionCleanupMaxCount: options.sessionCleanupMaxCount,
17125
19057
  maxActiveSessions: options.maxActiveSessions,
17126
19058
  sessionCleanupInterval: options.sessionCleanupInterval,
19059
+ sessionArchive: options.sessionArchive,
19060
+ sessionArchiveInterval: options.sessionArchiveInterval,
17127
19061
  // Raw string — the resolver in run.ts single-sources parsing
17128
19062
  // (resolveClaudeUsageReportingMode).
17129
19063
  claudeUsageReporting: options.claudeUsageReporting,
@@ -17134,6 +19068,9 @@ program.command("run").description("Connect to Evident and process messages").op
17134
19068
  // Raw values — expansion/validation is single-sourced in run.ts's
17135
19069
  // resolveFileSyncDirectories.
17136
19070
  enableFileSyncTo: options.enableFileSyncTo,
19071
+ // Raw values — expansion/validation is single-sourced in run.ts's
19072
+ // resolveFileSyncExactFiles.
19073
+ enableFileSyncToExactFile: options.enableFileSyncToExactFile,
17137
19074
  tunnelReadyFile: options.tunnelReadyFile,
17138
19075
  litestreamConfig: options.litestreamConfig,
17139
19076
  opencodePidFile: options.opencodePidFile,