@evident-ai/cli 3.6.1-dev.bbdfbd6 → 3.6.1-dev.c852a6f

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 readFileSync9, writeFileSync as writeFileSync7 } 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 join14, 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.bbdfbd6" : void 0) ?? process.env.npm_package_version ?? "unknown";
1448
+ var CLI_VERSION = (true ? "3.6.1-dev.c852a6f" : void 0) ?? process.env.npm_package_version ?? "unknown";
1446
1449
  function getCliVersion() {
1447
1450
  return CLI_VERSION;
1448
1451
  }
@@ -2638,6 +2641,122 @@ function isSessionDbRecoveryRecord(value) {
2638
2641
  );
2639
2642
  }
2640
2643
 
2644
+ // src/lib/opencode/session-deletion-report.ts
2645
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
2646
+ import { dirname as dirname4, join as join4 } from "node:path";
2647
+ var SESSION_DELETION_SPOOL_MAX_RECORDS = 1e3;
2648
+ var MAX_DIAGNOSTIC_LENGTH = 200;
2649
+ function sessionDeletionReportPath(homeDir, env) {
2650
+ const override = env.EVIDENT_SESSION_DELETION_REPORT?.trim();
2651
+ return override || join4(homeDir, ".local", "state", "evident", "session-deletions.jsonl");
2652
+ }
2653
+ function appendSessionDeletionRecords(path, records) {
2654
+ if (records.length === 0) return 0;
2655
+ mkdirSync(dirname4(path), { recursive: true });
2656
+ const current = readSessionDeletionRecords(path);
2657
+ if (current.readError !== null) {
2658
+ throw new Error(`cannot enforce the spool limit after reading ${path}: ${current.readError}`);
2659
+ }
2660
+ const combined = [...current.records, ...records];
2661
+ const kept = combined.slice(-SESSION_DELETION_SPOOL_MAX_RECORDS);
2662
+ const dropped = combined.length - kept.length;
2663
+ if (dropped > 0) {
2664
+ writeFileSync(path, `${kept.map((record) => JSON.stringify(record)).join("\n")}
2665
+ `, "utf8");
2666
+ } else {
2667
+ appendFileSync(path, `${records.map((record) => JSON.stringify(record)).join("\n")}
2668
+ `, "utf8");
2669
+ }
2670
+ return dropped;
2671
+ }
2672
+ function readSessionDeletionRecords(path) {
2673
+ let content;
2674
+ try {
2675
+ content = readFileSync3(path, "utf8");
2676
+ } catch (error2) {
2677
+ if (isMissingFile(error2)) return { records: [], skippedLines: 0, readError: null };
2678
+ const readError = boundedError(error2);
2679
+ console.error(`[session-deletion-report] could not read ${path}: ${readError}`);
2680
+ return { records: [], skippedLines: 0, readError };
2681
+ }
2682
+ let skippedLines = 0;
2683
+ const records = content.split("\n").flatMap((line) => {
2684
+ if (!line.trim()) return [];
2685
+ const record = parseRecord(line, path);
2686
+ if (record) return [record];
2687
+ skippedLines++;
2688
+ return [];
2689
+ });
2690
+ return { records, skippedLines, readError: null };
2691
+ }
2692
+ function removeDeliveredSessionDeletionRecords(path, delivered) {
2693
+ let content;
2694
+ try {
2695
+ content = readFileSync3(path, "utf8");
2696
+ } catch (error2) {
2697
+ if (isMissingFile(error2)) return;
2698
+ console.error(
2699
+ `[session-deletion-report] could not read ${path} before removing delivered records: ${boundedError(error2)}`
2700
+ );
2701
+ throw error2;
2702
+ }
2703
+ const toRemove = /* @__PURE__ */ new Map();
2704
+ for (const record of delivered) {
2705
+ toRemove.set(record.opencode_session_id, (toRemove.get(record.opencode_session_id) ?? 0) + 1);
2706
+ }
2707
+ const remaining = [];
2708
+ for (const line of content.split("\n")) {
2709
+ if (!line.trim()) continue;
2710
+ const record = parseRecord(line, path);
2711
+ const count = record ? toRemove.get(record.opencode_session_id) ?? 0 : 0;
2712
+ if (record && count > 0) {
2713
+ if (count === 1) toRemove.delete(record.opencode_session_id);
2714
+ else toRemove.set(record.opencode_session_id, count - 1);
2715
+ } else {
2716
+ remaining.push(line);
2717
+ }
2718
+ }
2719
+ try {
2720
+ if (remaining.length === 0) unlinkSync2(path);
2721
+ else writeFileSync(path, `${remaining.join("\n")}
2722
+ `, "utf8");
2723
+ } catch (error2) {
2724
+ if (isMissingFile(error2)) return;
2725
+ console.error(
2726
+ `[session-deletion-report] could not write ${path} after removing delivered records: ${boundedError(error2)}`
2727
+ );
2728
+ throw error2;
2729
+ }
2730
+ }
2731
+ function parseRecord(line, path) {
2732
+ try {
2733
+ const value = JSON.parse(line);
2734
+ if (isSessionDeletionRecord(value)) return value;
2735
+ console.error(`[session-deletion-report] skipped malformed record in ${path}: invalid shape`);
2736
+ return null;
2737
+ } catch (error2) {
2738
+ console.error(
2739
+ `[session-deletion-report] skipped malformed record in ${path}: ${boundedError(error2)}`
2740
+ );
2741
+ return null;
2742
+ }
2743
+ }
2744
+ function isSessionDeletionRecord(value) {
2745
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2746
+ const record = value;
2747
+ const policy = record.policy;
2748
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
2749
+ const fields = policy;
2750
+ 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");
2751
+ }
2752
+ function isMissingFile(error2) {
2753
+ return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
2754
+ }
2755
+ function boundedError(error2) {
2756
+ const message = error2 instanceof Error ? error2.message : String(error2);
2757
+ return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
2758
+ }
2759
+
2641
2760
  // src/lib/opencode/auth.ts
2642
2761
  function buildOpenCodeBasicAuthHeader(password) {
2643
2762
  return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
@@ -2656,8 +2775,26 @@ async function checkOpenCodeHealth(port) {
2656
2775
  if (!response.ok) {
2657
2776
  return { healthy: false, error: `HTTP ${response.status}` };
2658
2777
  }
2659
- const data = await response.json().catch(() => ({}));
2660
- return { healthy: true, version: data.version };
2778
+ const body = await response.text();
2779
+ let data;
2780
+ try {
2781
+ data = JSON.parse(body);
2782
+ } catch (error2) {
2783
+ return {
2784
+ healthy: false,
2785
+ okWithNonJsonBody: true,
2786
+ error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
2787
+ };
2788
+ }
2789
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
2790
+ return {
2791
+ healthy: false,
2792
+ okWithNonJsonBody: true,
2793
+ error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
2794
+ };
2795
+ }
2796
+ const version2 = data.version;
2797
+ return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
2661
2798
  } catch (error2) {
2662
2799
  const message = error2 instanceof Error ? error2.message : "Unknown error";
2663
2800
  return { healthy: false, error: message };
@@ -2673,10 +2810,17 @@ async function checkOpenCode2Health(port, password) {
2673
2810
  if (password !== void 0) {
2674
2811
  headers.Authorization = buildOpenCodeBasicAuthHeader(password);
2675
2812
  }
2676
- const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
2813
+ let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
2677
2814
  headers,
2678
2815
  signal: AbortSignal.timeout(2e3)
2679
2816
  });
2817
+ const usesHealthFallback = response.status === 404;
2818
+ if (usesHealthFallback) {
2819
+ response = await fetch(`http://127.0.0.1:${port}/api/health`, {
2820
+ headers,
2821
+ signal: AbortSignal.timeout(2e3)
2822
+ });
2823
+ }
2680
2824
  if (response.status === 401) {
2681
2825
  return { healthy: false, authFailed: true, error: "HTTP 401" };
2682
2826
  }
@@ -2691,23 +2835,29 @@ async function checkOpenCode2Health(port, password) {
2691
2835
  } catch (error2) {
2692
2836
  return {
2693
2837
  healthy: false,
2694
- error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
2838
+ error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
2695
2839
  };
2696
2840
  }
2697
2841
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
2698
2842
  return {
2699
2843
  healthy: false,
2700
- error: `Invalid health response${excerpt}: expected healthy=true`
2844
+ error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
2701
2845
  };
2702
2846
  }
2703
- const health = data;
2704
- if (health.healthy !== true) {
2847
+ const bodyObject = data;
2848
+ if (usesHealthFallback && bodyObject.healthy !== true) {
2705
2849
  return {
2706
2850
  healthy: false,
2707
2851
  error: `Invalid health response${excerpt}: expected healthy=true`
2708
2852
  };
2709
2853
  }
2710
- return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
2854
+ if (!usesHealthFallback) {
2855
+ return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
2856
+ healthy: false,
2857
+ error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
2858
+ };
2859
+ }
2860
+ return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
2711
2861
  } catch (error2) {
2712
2862
  const message = error2 instanceof Error ? error2.message : "Unknown error";
2713
2863
  return { healthy: false, error: message };
@@ -3209,6 +3359,52 @@ async function getModelAttachmentCapability(port, model, client) {
3209
3359
  return null;
3210
3360
  }
3211
3361
  }
3362
+ async function isPinnedProviderLoaded(port, model, client) {
3363
+ if (client?.version === "v2") return null;
3364
+ const { model: baseModel } = splitModelVariant(model);
3365
+ if (!baseModel) return null;
3366
+ const slash = baseModel.indexOf("/");
3367
+ if (slash <= 0) return null;
3368
+ const providerId = baseModel.slice(0, slash);
3369
+ try {
3370
+ const res = await requestWithClient(port, client, "/config/providers");
3371
+ if (!res.ok) {
3372
+ console.error(
3373
+ `[isPinnedProviderLoaded] GET /config/providers returned HTTP ${res.status} (port ${port})`
3374
+ );
3375
+ return null;
3376
+ }
3377
+ const body = await res.json();
3378
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
3379
+ console.error(
3380
+ `[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
3381
+ );
3382
+ return null;
3383
+ }
3384
+ const providers = body.providers;
3385
+ if (!Array.isArray(providers)) {
3386
+ console.error(
3387
+ `[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
3388
+ );
3389
+ return null;
3390
+ }
3391
+ const providerIds = providers.filter(
3392
+ (provider) => provider !== null && typeof provider === "object" && typeof provider.id === "string"
3393
+ ).map((provider) => provider.id);
3394
+ if (providerIds.length === 0) {
3395
+ console.error(
3396
+ `[isPinnedProviderLoaded] GET /config/providers had no provider entries with string ids (port ${port})`
3397
+ );
3398
+ return null;
3399
+ }
3400
+ return providerIds.includes(providerId);
3401
+ } catch (err) {
3402
+ console.error(
3403
+ `[isPinnedProviderLoaded] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3404
+ );
3405
+ return null;
3406
+ }
3407
+ }
3212
3408
  async function buildFileParts(attachments, capable) {
3213
3409
  const outcomes = [];
3214
3410
  const parts = [];
@@ -3623,9 +3819,17 @@ function isPreamblePinnedRunning(messages, userMessageId) {
3623
3819
  const reply = findLastAssistantReplyFor(messages, userMessageId);
3624
3820
  return completedOf(reply) != null && finishOf(reply) === "tool-calls";
3625
3821
  }
3822
+ function isMidFlightPinnedRunning(messages, userMessageId) {
3823
+ if (messageRunState(messages, userMessageId) !== "running") return false;
3824
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
3825
+ return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
3826
+ }
3626
3827
  function isB2AbandonmentConfirmed(params) {
3627
3828
  return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
3628
3829
  }
3830
+ function isMidFlightOrphanConfirmed(params) {
3831
+ return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
3832
+ }
3629
3833
  function isAmbiguousTerminalFinish(m) {
3630
3834
  if (completedOf(m) == null) return false;
3631
3835
  if (errorOf(m) != null) return false;
@@ -4491,13 +4695,13 @@ async function readV2SessionErrorStream(client, options) {
4491
4695
  }
4492
4696
 
4493
4697
  // src/lib/opencode/credential-import-v2.ts
4494
- import { readFileSync as readFileSync4 } from "node:fs";
4495
- import { join as join5 } from "node:path";
4698
+ import { readFileSync as readFileSync5 } from "node:fs";
4699
+ import { join as join6 } from "node:path";
4496
4700
 
4497
4701
  // src/lib/openai-usage.ts
4498
- import { readFileSync as readFileSync3 } from "node:fs";
4702
+ import { readFileSync as readFileSync4 } from "node:fs";
4499
4703
  import { homedir as homedir3 } from "node:os";
4500
- import { join as join4 } from "node:path";
4704
+ import { join as join5 } from "node:path";
4501
4705
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
4502
4706
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
4503
4707
  var OpenAiUsageError = class extends Error {
@@ -4511,7 +4715,7 @@ function isLocalCredentialProblem2(err) {
4511
4715
  }
4512
4716
  function readOpenCodeChatGptCredentials() {
4513
4717
  try {
4514
- const raw = readFileSync3(join4(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4718
+ const raw = readFileSync4(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4515
4719
  let parsed;
4516
4720
  try {
4517
4721
  parsed = JSON.parse(raw);
@@ -4743,7 +4947,7 @@ function emptyResult(unsupported = [], outcome = "completed") {
4743
4947
  }
4744
4948
  function readOpenCodeCredentials(homeDir, log3) {
4745
4949
  try {
4746
- const raw = readFileSync4(join5(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4950
+ const raw = readFileSync5(join6(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4747
4951
  let parsed;
4748
4952
  try {
4749
4953
  parsed = JSON.parse(raw);
@@ -4943,9 +5147,9 @@ async function importOpenCodeV2Credentials({
4943
5147
 
4944
5148
  // src/lib/opencode/session-db-boot.ts
4945
5149
  import { spawn as spawn2 } from "node:child_process";
4946
- import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
5150
+ import { mkdirSync as mkdirSync2, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
4947
5151
  import { homedir as homedir4 } from "node:os";
4948
- import { dirname as dirname4, resolve as resolvePath } from "node:path";
5152
+ import { dirname as dirname5, resolve as resolvePath } from "node:path";
4949
5153
 
4950
5154
  // src/lib/runner-synchroniser.ts
4951
5155
  import { spawn } from "node:child_process";
@@ -5061,7 +5265,7 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
5061
5265
  function clearMarker(options) {
5062
5266
  if (!options.noReplicateMarker) return;
5063
5267
  try {
5064
- unlinkSync2(options.noReplicateMarker);
5268
+ unlinkSync3(options.noReplicateMarker);
5065
5269
  } catch (error2) {
5066
5270
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
5067
5271
  options.log(
@@ -5073,8 +5277,8 @@ function clearMarker(options) {
5073
5277
  function markNoReplicate(options, message) {
5074
5278
  if (options.noReplicateMarker) {
5075
5279
  try {
5076
- mkdirSync(dirname4(options.noReplicateMarker), { recursive: true });
5077
- writeFileSync(options.noReplicateMarker, "");
5280
+ mkdirSync2(dirname5(options.noReplicateMarker), { recursive: true });
5281
+ writeFileSync2(options.noReplicateMarker, "");
5078
5282
  } catch (error2) {
5079
5283
  options.log(
5080
5284
  `Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5087,7 +5291,7 @@ function markNoReplicate(options, message) {
5087
5291
  function discardSessionDbDebris(options) {
5088
5292
  for (const path of [options.dbPath, `${options.dbPath}-wal`, `${options.dbPath}-shm`]) {
5089
5293
  try {
5090
- unlinkSync2(path);
5294
+ unlinkSync3(path);
5091
5295
  } catch (error2) {
5092
5296
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") continue;
5093
5297
  options.log(
@@ -5228,8 +5432,8 @@ async function ensureLitestreamConfig(options, env) {
5228
5432
  return null;
5229
5433
  }
5230
5434
  try {
5231
- mkdirSync(dirname4(configPath), { recursive: true });
5232
- writeFileSync(configPath, rendered.stdout);
5435
+ mkdirSync2(dirname5(configPath), { recursive: true });
5436
+ writeFileSync2(configPath, rendered.stdout);
5233
5437
  } catch (error2) {
5234
5438
  options.log(
5235
5439
  `Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
@@ -5494,8 +5698,8 @@ async function restoreAndVerifySessionDb(options) {
5494
5698
 
5495
5699
  // src/lib/opencode/session-db-provenance.ts
5496
5700
  import { createRequire } from "node:module";
5497
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
5498
- import { dirname as dirname5, join as join6 } from "node:path";
5701
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5702
+ import { dirname as dirname6, join as join7 } from "node:path";
5499
5703
  var require2 = createRequire(import.meta.url);
5500
5704
  function readSessionDbMigrationIds(dbPath) {
5501
5705
  let db;
@@ -5532,12 +5736,12 @@ function readSessionDbMigrationIds(dbPath) {
5532
5736
  }
5533
5737
  function sessionDbProvenanceStatePath(homeDir, env) {
5534
5738
  const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
5535
- return override || join6(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5739
+ return override || join7(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5536
5740
  }
5537
5741
  function loadSessionDbProvenanceState(path) {
5538
5742
  let value;
5539
5743
  try {
5540
- value = JSON.parse(readFileSync5(path, "utf8"));
5744
+ value = JSON.parse(readFileSync6(path, "utf8"));
5541
5745
  } catch (error2) {
5542
5746
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
5543
5747
  console.error(
@@ -5561,8 +5765,8 @@ function loadSessionDbProvenanceState(path) {
5561
5765
  }
5562
5766
  function saveSessionDbProvenanceState(path, state) {
5563
5767
  try {
5564
- mkdirSync2(dirname5(path), { recursive: true });
5565
- writeFileSync2(path, `${JSON.stringify(state, null, 2)}
5768
+ mkdirSync3(dirname6(path), { recursive: true });
5769
+ writeFileSync3(path, `${JSON.stringify(state, null, 2)}
5566
5770
  `, "utf8");
5567
5771
  } catch (error2) {
5568
5772
  console.error(
@@ -5617,6 +5821,26 @@ function isSessionDbProvenanceRecord(value) {
5617
5821
  return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
5618
5822
  }
5619
5823
 
5824
+ // src/lib/opencode/detect.ts
5825
+ function describeProbe(route, result) {
5826
+ if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
5827
+ if (result.error) return `${route}: no HTTP response (${result.error})`;
5828
+ return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
5829
+ }
5830
+ async function classifyRunningOpenCode(port) {
5831
+ const v1 = await checkOpenCodeHealth(port);
5832
+ if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
5833
+ const v2 = await checkOpenCode2Health(port);
5834
+ if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
5835
+ const detail = [
5836
+ describeProbe("V1 /global/health", v1),
5837
+ describeProbe("V2 /api/info (or /api/health fallback)", v2)
5838
+ ].join("; ");
5839
+ if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
5840
+ if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
5841
+ return { kind: "no-server-identified", detail };
5842
+ }
5843
+
5620
5844
  // src/lib/opencode/opencode2-package.ts
5621
5845
  var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
5622
5846
 
@@ -5925,26 +6149,28 @@ async function startOpenCode(port, options = {}) {
5925
6149
  }
5926
6150
  async function startOpenCode2(port, options = {}) {
5927
6151
  const password = randomBytes(24).toString("hex");
5928
- let command = "opencode2";
6152
+ let command = options.command ?? "opencode2";
5929
6153
  const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
5930
6154
  let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
5931
- try {
5932
- execSync("which opencode2", { stdio: "ignore" });
5933
- } catch {
5934
- command = "npx";
5935
- args = [
5936
- "-y",
5937
- "-p",
5938
- OPENCODE2_PACKAGE_SPEC,
5939
- "--",
5940
- "opencode2",
5941
- "serve",
5942
- "--port",
5943
- port.toString(),
5944
- "--hostname",
5945
- "127.0.0.1",
5946
- ...logLevel
5947
- ];
6155
+ if (options.command === void 0) {
6156
+ try {
6157
+ execSync("which opencode2", { stdio: "ignore" });
6158
+ } catch {
6159
+ command = "npx";
6160
+ args = [
6161
+ "-y",
6162
+ "-p",
6163
+ OPENCODE2_PACKAGE_SPEC,
6164
+ "--",
6165
+ "opencode2",
6166
+ "serve",
6167
+ "--port",
6168
+ port.toString(),
6169
+ "--hostname",
6170
+ "127.0.0.1",
6171
+ ...logLevel
6172
+ ];
6173
+ }
5948
6174
  }
5949
6175
  const child = spawn3(command, args, {
5950
6176
  env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
@@ -6001,6 +6227,56 @@ function isOpenCode2Installed() {
6001
6227
  return false;
6002
6228
  }
6003
6229
  }
6230
+ function readVersionOutput(command) {
6231
+ try {
6232
+ return execSync2(`${command} --version`, {
6233
+ encoding: "utf-8",
6234
+ stdio: ["ignore", "pipe", "pipe"]
6235
+ }).trim();
6236
+ } catch (error2) {
6237
+ const output = typeof error2 === "object" && error2 !== null ? [
6238
+ "stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
6239
+ "stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
6240
+ ].filter(Boolean).join("\n") : "";
6241
+ return output || (error2 instanceof Error ? error2.message : String(error2));
6242
+ }
6243
+ }
6244
+ function parseOpenCodeVersion(rawVersion) {
6245
+ const output = rawVersion.trim();
6246
+ const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
6247
+ const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
6248
+ if (prefix === "opencode2") return { major: "v2", version: version2 };
6249
+ const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
6250
+ if (!major) return null;
6251
+ if (Number(major) === 1) return { major: "v1", version: version2 };
6252
+ if (Number(major) >= 2) return { major: "v2", version: version2 };
6253
+ return null;
6254
+ }
6255
+ function resolveInstalledOpenCode() {
6256
+ let opencode2Available = false;
6257
+ if (isOpenCodeInstalled()) {
6258
+ const rawVersion = readVersionOutput("opencode");
6259
+ const parsed = parseOpenCodeVersion(rawVersion);
6260
+ if (parsed?.major === "v1") {
6261
+ return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
6262
+ }
6263
+ if (parsed?.major === "v2") {
6264
+ return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
6265
+ }
6266
+ opencode2Available = isOpenCode2Installed();
6267
+ if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
6268
+ }
6269
+ if (opencode2Available || isOpenCode2Installed()) {
6270
+ const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
6271
+ return {
6272
+ kind: "installed",
6273
+ major: "v2",
6274
+ command: "opencode2",
6275
+ version: parsed?.version
6276
+ };
6277
+ }
6278
+ return { kind: "none" };
6279
+ }
6004
6280
  async function promptOpenCodeInstall(interactive) {
6005
6281
  if (!interactive) {
6006
6282
  console.log(
@@ -6210,15 +6486,15 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
6210
6486
 
6211
6487
  // src/lib/opencode/session-db-size.ts
6212
6488
  import { statSync as statSync4 } from "node:fs";
6213
- import { join as join7 } from "node:path";
6489
+ import { join as join8 } from "node:path";
6214
6490
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
6215
6491
  function statSessionDbBytes(homeDir) {
6216
- const dbPath = join7(homeDir, ".local", "share", "opencode", "opencode.db");
6492
+ const dbPath = join8(homeDir, ".local", "share", "opencode", "opencode.db");
6217
6493
  try {
6218
6494
  return statSync4(dbPath).size;
6219
6495
  } catch (err) {
6220
- const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
6221
- if (!isMissingFile) {
6496
+ const isMissingFile2 = err instanceof Error && "code" in err && err.code === "ENOENT";
6497
+ if (!isMissingFile2) {
6222
6498
  console.error(
6223
6499
  `[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
6224
6500
  );
@@ -6243,12 +6519,12 @@ function buildSessionStoreSizeWarning(input) {
6243
6519
  // src/lib/opencode/log-tail.ts
6244
6520
  import { statSync as statSync5 } from "node:fs";
6245
6521
  import { homedir as homedir5 } from "node:os";
6246
- import { join as join8 } from "node:path";
6522
+ import { join as join9 } from "node:path";
6247
6523
  import { open as open2, stat } from "node:fs/promises";
6248
6524
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
6249
6525
  function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
6250
- const dataDir = env.XDG_DATA_HOME || join8(homeDir, ".local", "share");
6251
- return join8(dataDir, "opencode", "log", "opencode.log");
6526
+ const dataDir = env.XDG_DATA_HOME || join9(homeDir, ".local", "share");
6527
+ return join9(dataDir, "opencode", "log", "opencode.log");
6252
6528
  }
6253
6529
  function isEnoent(error2) {
6254
6530
  return error2?.code === "ENOENT";
@@ -6915,10 +7191,10 @@ var RunnerConnection = class _RunnerConnection {
6915
7191
  };
6916
7192
 
6917
7193
  // src/lib/tunnel/ready-marker.ts
6918
- import { writeFileSync as writeFileSync3 } from "node:fs";
7194
+ import { writeFileSync as writeFileSync4 } from "node:fs";
6919
7195
  function writeTunnelReadyMarker(path, agentId) {
6920
7196
  try {
6921
- writeFileSync3(path, `${agentId}
7197
+ writeFileSync4(path, `${agentId}
6922
7198
  `);
6923
7199
  return { ok: true };
6924
7200
  } catch (error2) {
@@ -6943,7 +7219,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
6943
7219
  }
6944
7220
 
6945
7221
  // src/lib/process-liveness.ts
6946
- import { readFileSync as readFileSync6 } from "node:fs";
7222
+ import { readFileSync as readFileSync7 } from "node:fs";
6947
7223
  function isProcessAlive(pid) {
6948
7224
  try {
6949
7225
  process.kill(pid, 0);
@@ -6958,7 +7234,7 @@ function isProcessAlive(pid) {
6958
7234
  }
6959
7235
  if (process.platform !== "linux") return true;
6960
7236
  try {
6961
- const status2 = readFileSync6(`/proc/${pid}/status`, "utf8");
7237
+ const status2 = readFileSync7(`/proc/${pid}/status`, "utf8");
6962
7238
  return !/^State:\s+Z(?:\s|$)/m.test(status2);
6963
7239
  } catch (error2) {
6964
7240
  console.error(
@@ -7317,18 +7593,18 @@ function createSessionStoreHealth(options = {}) {
7317
7593
  import { homedir as homedir6 } from "node:os";
7318
7594
 
7319
7595
  // src/lib/runner-file-sync.ts
7320
- import { join as join10 } from "node:path";
7596
+ import { join as join11 } from "node:path";
7321
7597
 
7322
7598
  // src/lib/file-push.ts
7323
7599
  import { randomUUID } from "node:crypto";
7324
7600
  import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
7325
- 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";
7601
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
7326
7602
  var FILE_MODE = 384;
7327
7603
  var DIRECTORY_MODE = 448;
7328
7604
  async function writePushedFile(request3) {
7329
- const { requestedPath, content, allowedDirectories, homeDir } = request3;
7605
+ const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
7330
7606
  const bytes = content.byteLength;
7331
- if (allowedDirectories.length === 0) {
7607
+ if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
7332
7608
  return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
7333
7609
  path: requestedPath,
7334
7610
  bytes
@@ -7353,10 +7629,10 @@ async function writePushedFile(request3) {
7353
7629
  }
7354
7630
  try {
7355
7631
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
7356
- dirname6(candidate)
7632
+ dirname7(candidate)
7357
7633
  );
7358
- const realTarget = join9(existingAncestor, ...missingSegments, basename2(candidate));
7359
- const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
7634
+ const realTarget = join10(existingAncestor, ...missingSegments, basename2(candidate));
7635
+ const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
7360
7636
  if (allowedDirectory === null) {
7361
7637
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7362
7638
  path: realTarget,
@@ -7365,8 +7641,8 @@ async function writePushedFile(request3) {
7365
7641
  }
7366
7642
  if (missingSegments.length > 0) {
7367
7643
  await createMissingDirectories(existingAncestor, missingSegments);
7368
- const realParent = await realpath(dirname6(realTarget));
7369
- if (realParent !== dirname6(realTarget) || !contains(allowedDirectory, realTarget)) {
7644
+ const realParent = await realpath(dirname7(realTarget));
7645
+ if (realParent !== dirname7(realTarget) || !contains(allowedDirectory, realTarget)) {
7370
7646
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
7371
7647
  path: realTarget,
7372
7648
  bytes,
@@ -7391,7 +7667,7 @@ function expandAndValidate(requestedPath, homeDir) {
7391
7667
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
7392
7668
  return null;
7393
7669
  }
7394
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
7670
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7395
7671
  if (expanded.split(/[/\\]/).includes("..")) {
7396
7672
  return null;
7397
7673
  }
@@ -7409,7 +7685,7 @@ async function resolveNearestExistingAncestor(directory) {
7409
7685
  try {
7410
7686
  return { existingAncestor: await realpath(current), missingSegments };
7411
7687
  } catch (err) {
7412
- const parent = dirname6(current);
7688
+ const parent = dirname7(current);
7413
7689
  if (err.code !== "ENOENT" || parent === current) {
7414
7690
  throw err;
7415
7691
  }
@@ -7431,6 +7707,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
7431
7707
  }
7432
7708
  return null;
7433
7709
  }
7710
+ async function findMatchingAllowedFile(allowedFiles, realTarget) {
7711
+ for (const entry of allowedFiles) {
7712
+ if (!isAbsolute2(entry)) {
7713
+ log("warn", "file_push_allowed_directory_skipped", {
7714
+ directory: entry,
7715
+ reason: "not_absolute_file"
7716
+ });
7717
+ continue;
7718
+ }
7719
+ const realParent = await realpathCreatingIfMissing(dirname7(entry));
7720
+ if (realParent !== null && join10(realParent, basename2(entry)) === realTarget) {
7721
+ return realParent;
7722
+ }
7723
+ }
7724
+ return null;
7725
+ }
7434
7726
  async function realpathCreatingIfMissing(directory) {
7435
7727
  try {
7436
7728
  return await realpath(directory);
@@ -7464,13 +7756,13 @@ function contains(realDirectory, realTarget) {
7464
7756
  async function createMissingDirectories(existingAncestor, missingSegments) {
7465
7757
  let current = existingAncestor;
7466
7758
  for (const segment of missingSegments) {
7467
- current = join9(current, segment);
7759
+ current = join10(current, segment);
7468
7760
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
7469
7761
  await chmod(current, DIRECTORY_MODE);
7470
7762
  }
7471
7763
  }
7472
7764
  async function writeAtomically(realTarget, content) {
7473
- const temporaryPath = join9(dirname6(realTarget), `.evident-push-${randomUUID()}.tmp`);
7765
+ const temporaryPath = join10(dirname7(realTarget), `.evident-push-${randomUUID()}.tmp`);
7474
7766
  let handle;
7475
7767
  try {
7476
7768
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -7600,16 +7892,16 @@ var NOT_APPLIED = {
7600
7892
  opencodeAuthApplied: false
7601
7893
  };
7602
7894
  function isClaudeCredentialPath(requestedPath, homeDir) {
7603
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7604
- return expanded === join10(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7895
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7896
+ return expanded === join11(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7605
7897
  }
7606
7898
  function isOpenCodeAuthPath(requestedPath, homeDir) {
7607
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7608
- return expanded === join10(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7899
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
7900
+ return expanded === join11(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7609
7901
  }
7610
7902
  async function applyOne(options, file) {
7611
7903
  const label = `${file.id.slice(0, 8)} (${file.path})`;
7612
- if (options.allowedDirectories.length === 0) {
7904
+ if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
7613
7905
  options.log({
7614
7906
  level: "warn",
7615
7907
  message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
@@ -7636,6 +7928,7 @@ async function applyOne(options, file) {
7636
7928
  requestedPath: file.path,
7637
7929
  content: download.content,
7638
7930
  allowedDirectories: options.allowedDirectories,
7931
+ allowedFiles: options.allowedFiles,
7639
7932
  homeDir: options.homeDir
7640
7933
  });
7641
7934
  } catch (err) {
@@ -7742,6 +8035,212 @@ function describe(err) {
7742
8035
  return err instanceof Error ? err.message : String(err);
7743
8036
  }
7744
8037
 
8038
+ // src/lib/vault-file-sync.ts
8039
+ import { createHash } from "node:crypto";
8040
+ async function syncVaultFiles(options) {
8041
+ const files = await listVaultFiles(options);
8042
+ if (files === null) return { applied: 0 };
8043
+ const listedIds = new Set(files.map((file) => file.id));
8044
+ for (const fileId of options.outcomes.keys()) {
8045
+ if (!listedIds.has(fileId)) options.outcomes.delete(fileId);
8046
+ }
8047
+ let applied = 0;
8048
+ for (const file of files) {
8049
+ try {
8050
+ if (await reconcileVaultFile(options, file)) applied += 1;
8051
+ } catch (err) {
8052
+ options.log({
8053
+ level: "error",
8054
+ message: `Vault file ${file.id} reconciliation failed unexpectedly (message processing is unaffected): ${describe2(err)}`
8055
+ });
8056
+ }
8057
+ }
8058
+ return { applied };
8059
+ }
8060
+ async function listVaultFiles(options) {
8061
+ let response;
8062
+ try {
8063
+ response = await options.fetchImpl(`${options.apiUrl}/runners/${options.agentId}/vault-files`, {
8064
+ headers: { Authorization: options.getAuthHeader() }
8065
+ });
8066
+ } catch (err) {
8067
+ options.log({
8068
+ level: "warn",
8069
+ message: `Could not list vault files \u2014 retrying on the next drain: ${describe2(err)}`
8070
+ });
8071
+ return null;
8072
+ }
8073
+ if (!response.ok) {
8074
+ options.log({
8075
+ level: response.status === 404 ? "debug" : "warn",
8076
+ message: `Listing vault files returned HTTP ${response.status} \u2014 retrying on the next drain`
8077
+ });
8078
+ return null;
8079
+ }
8080
+ let body;
8081
+ try {
8082
+ body = await response.json();
8083
+ } catch (err) {
8084
+ options.log({
8085
+ level: "warn",
8086
+ message: `Vault file list was not readable JSON \u2014 retrying on the next drain: ${describe2(err)}`
8087
+ });
8088
+ return null;
8089
+ }
8090
+ if (body === null || typeof body !== "object" || !Array.isArray(body.files)) {
8091
+ options.log({
8092
+ level: "warn",
8093
+ message: "Vault file list was not an object with a files array \u2014 ignoring it for this drain"
8094
+ });
8095
+ return null;
8096
+ }
8097
+ const files = [];
8098
+ for (const entry of body.files) {
8099
+ const file = asVaultFile(entry);
8100
+ if (file === null) {
8101
+ options.log({
8102
+ level: "warn",
8103
+ message: "Ignoring a malformed vault file entry"
8104
+ });
8105
+ continue;
8106
+ }
8107
+ files.push(file);
8108
+ }
8109
+ return files;
8110
+ }
8111
+ function asVaultFile(entry) {
8112
+ if (entry === null || typeof entry !== "object") return null;
8113
+ const value = entry;
8114
+ 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") {
8115
+ return null;
8116
+ }
8117
+ return {
8118
+ id: value.id,
8119
+ destination_path: value.destination_path,
8120
+ size_bytes: value.size_bytes,
8121
+ content_sha256: value.content_sha256,
8122
+ created_at: value.created_at,
8123
+ updated_at: value.updated_at
8124
+ };
8125
+ }
8126
+ async function reconcileVaultFile(options, file) {
8127
+ const existing = options.outcomes.get(file.id);
8128
+ if (existing?.sha256 === file.content_sha256) {
8129
+ if (existing.reported || existing.reportAttempts >= MAX_ACK_ATTEMPTS) return false;
8130
+ await reportOutcome(options, file, existing);
8131
+ return false;
8132
+ }
8133
+ const content = await downloadVaultFile(options, file);
8134
+ if (content === null) return false;
8135
+ const actualSha256 = createHash("sha256").update(content).digest("hex");
8136
+ if (actualSha256 !== file.content_sha256) {
8137
+ options.log({
8138
+ level: "warn",
8139
+ message: `Vault file ${file.id} content hash mismatch: expected ${file.content_sha256}, received ${actualSha256} \u2014 retrying on the next drain`
8140
+ });
8141
+ return false;
8142
+ }
8143
+ const outcome = await applyVaultFile(options, file, content);
8144
+ const entry = {
8145
+ sha256: file.content_sha256,
8146
+ outcome: outcome.ok ? "applied" : "refused",
8147
+ reason: outcome.ok ? null : outcome.code,
8148
+ reported: false,
8149
+ reportAttempts: 0
8150
+ };
8151
+ options.outcomes.set(file.id, entry);
8152
+ await reportOutcome(options, file, entry);
8153
+ return outcome.ok;
8154
+ }
8155
+ async function downloadVaultFile(options, file) {
8156
+ try {
8157
+ const response = await options.fetchImpl(
8158
+ `${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/content`,
8159
+ { headers: { Authorization: options.getAuthHeader() } }
8160
+ );
8161
+ if (!response.ok) {
8162
+ options.log({
8163
+ level: "warn",
8164
+ message: `Downloading vault file ${file.id} returned HTTP ${response.status} \u2014 retrying on the next drain`
8165
+ });
8166
+ return null;
8167
+ }
8168
+ return Buffer.from(await response.arrayBuffer());
8169
+ } catch (err) {
8170
+ options.log({
8171
+ level: "warn",
8172
+ message: `Downloading vault file ${file.id} failed \u2014 retrying on the next drain: ${describe2(err)}`
8173
+ });
8174
+ return null;
8175
+ }
8176
+ }
8177
+ async function applyVaultFile(options, file, content) {
8178
+ try {
8179
+ const outcome = await writePushedFile({
8180
+ requestedPath: file.destination_path,
8181
+ content,
8182
+ allowedDirectories: options.allowedDirectories,
8183
+ allowedFiles: options.allowedFiles,
8184
+ homeDir: options.homeDir
8185
+ });
8186
+ if (!outcome.ok) {
8187
+ options.log({
8188
+ level: "warn",
8189
+ message: `Vault file ${file.id} refused (${outcome.code}): ${outcome.message}`
8190
+ });
8191
+ } else {
8192
+ options.log({
8193
+ level: "info",
8194
+ message: `Vault file ${file.id} applied (${content.byteLength} bytes)`
8195
+ });
8196
+ }
8197
+ return outcome;
8198
+ } catch (err) {
8199
+ options.log({
8200
+ level: "error",
8201
+ message: `Vault file ${file.id} could not be written: ${describe2(err)}`
8202
+ });
8203
+ return { ok: false, code: "write_failed", message: "The runner could not write the file." };
8204
+ }
8205
+ }
8206
+ async function reportOutcome(options, file, entry) {
8207
+ const body = {
8208
+ status: entry.outcome === "applied" ? "applied" : "rejected",
8209
+ reported_sha256: entry.sha256
8210
+ };
8211
+ if (entry.outcome === "refused" && entry.reason !== null) body.reason = entry.reason;
8212
+ try {
8213
+ const response = await options.fetchImpl(
8214
+ `${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/status`,
8215
+ {
8216
+ method: "POST",
8217
+ headers: {
8218
+ Authorization: options.getAuthHeader(),
8219
+ "Content-Type": "application/json"
8220
+ },
8221
+ body: JSON.stringify(body)
8222
+ }
8223
+ );
8224
+ if (response.ok) {
8225
+ entry.reported = true;
8226
+ return;
8227
+ }
8228
+ recordReportFailure(options, file, entry, `HTTP ${response.status}`);
8229
+ } catch (err) {
8230
+ recordReportFailure(options, file, entry, describe2(err));
8231
+ }
8232
+ }
8233
+ function recordReportFailure(options, file, entry, reason) {
8234
+ entry.reportAttempts += 1;
8235
+ options.log({
8236
+ level: "error",
8237
+ 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})`
8238
+ });
8239
+ }
8240
+ function describe2(err) {
8241
+ return err instanceof Error ? err.message : String(err);
8242
+ }
8243
+
7745
8244
  // src/lib/channels/driver.ts
7746
8245
  function messageIdOf(m) {
7747
8246
  if (!m || typeof m !== "object") return void 0;
@@ -7750,7 +8249,7 @@ function messageIdOf(m) {
7750
8249
  return typeof infoId === "string" ? infoId : void 0;
7751
8250
  }
7752
8251
  function composeAbandonmentNote(finish, replyShape, outcome) {
7753
- 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.");
8252
+ 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.");
7754
8253
  if (replyShape === null) return base;
7755
8254
  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)` : ""}.`;
7756
8255
  }
@@ -7787,6 +8286,9 @@ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
7787
8286
  var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
7788
8287
  var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
7789
8288
  var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
8289
+ var MIDFLIGHT_ORPHAN_MIN_PINNED_MS = 3 * 6e4;
8290
+ var MIDFLIGHT_ORPHAN_RECHECK_MS = 6e4;
8291
+ var MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK = 2;
7790
8292
  var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
7791
8293
  var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
7792
8294
  var WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;
@@ -7838,6 +8340,7 @@ var ChannelDriver = class _ChannelDriver {
7838
8340
  stuckQueuedMs;
7839
8341
  now;
7840
8342
  fileSyncDirectories;
8343
+ fileSyncExactFiles;
7841
8344
  homeDir;
7842
8345
  maxActiveSessions;
7843
8346
  watcherStallMs;
@@ -8150,6 +8653,7 @@ var ChannelDriver = class _ChannelDriver {
8150
8653
  * re-downloaded and re-written every ~2s until the server expires it.
8151
8654
  */
8152
8655
  fileAckFailures = /* @__PURE__ */ new Map();
8656
+ vaultFileOutcomes = /* @__PURE__ */ new Map();
8153
8657
  /**
8154
8658
  * Monotonic count of files this runner has pulled and written (#559). Only
8155
8659
  * ever increases, so `run.ts` detects work by comparing it against the value
@@ -8206,6 +8710,7 @@ var ChannelDriver = class _ChannelDriver {
8206
8710
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
8207
8711
  this.now = config.now ?? (() => Date.now());
8208
8712
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
8713
+ this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
8209
8714
  this.homeDir = config.homeDir ?? homedir6();
8210
8715
  this.maxActiveSessions = config.maxActiveSessions;
8211
8716
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
@@ -8303,14 +8808,27 @@ var ChannelDriver = class _ChannelDriver {
8303
8808
  getAuthHeader: this.getAuthHeader,
8304
8809
  fetchImpl: this.fetchImpl,
8305
8810
  allowedDirectories: this.fileSyncDirectories,
8811
+ allowedFiles: this.fileSyncExactFiles,
8306
8812
  homeDir: this.homeDir,
8307
8813
  ackFailures: this.fileAckFailures,
8308
8814
  log: this.log
8309
8815
  });
8310
- this.appliedFileCount += result.applied;
8816
+ const vaultResult = await syncVaultFiles({
8817
+ agentId: this.agentId,
8818
+ apiUrl: this.apiUrl,
8819
+ getAuthHeader: this.getAuthHeader,
8820
+ fetchImpl: this.fetchImpl,
8821
+ allowedDirectories: this.fileSyncDirectories,
8822
+ allowedFiles: this.fileSyncExactFiles,
8823
+ homeDir: this.homeDir,
8824
+ outcomes: this.vaultFileOutcomes,
8825
+ log: this.log
8826
+ });
8827
+ const applied = result.applied + vaultResult.applied;
8828
+ this.appliedFileCount += applied;
8311
8829
  if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
8312
8830
  if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
8313
- return result.applied;
8831
+ return applied;
8314
8832
  } catch (err) {
8315
8833
  this.log({
8316
8834
  level: "error",
@@ -8722,6 +9240,47 @@ var ChannelDriver = class _ChannelDriver {
8722
9240
  agent: message.opencode_agent ?? void 0,
8723
9241
  model: message.opencode_model ?? void 0
8724
9242
  };
9243
+ const pinnedProviderLoaded = await isPinnedProviderLoaded(
9244
+ this.port,
9245
+ options.model,
9246
+ this.openCodeClient
9247
+ );
9248
+ if (pinnedProviderLoaded === false) {
9249
+ const dispatchedModel = message.opencode_model;
9250
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
9251
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
9252
+ const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
9253
+ const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
9254
+ const errorMessage3 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
9255
+ this.log({
9256
+ level: "error",
9257
+ 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`,
9258
+ conversation_id: conv.id,
9259
+ message_id: message.id
9260
+ });
9261
+ try {
9262
+ await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
9263
+ classification: {
9264
+ kind: "model_auth",
9265
+ providerId,
9266
+ modelId,
9267
+ reason: "missing"
9268
+ },
9269
+ errorShape: { name: null, statusCode: null }
9270
+ });
9271
+ this.signalDispatchNotStarted(conv, message, "model_provider_absent");
9272
+ } catch (markErr) {
9273
+ this.log({
9274
+ level: "warn",
9275
+ message: `markFailed PATCH for refused message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
9276
+ conversation_id: conv.id,
9277
+ message_id: message.id
9278
+ });
9279
+ this.signalDispatchNotStarted(conv, message, "failure_unreported");
9280
+ }
9281
+ await this.repairProviderRegistry(conv.id, message.id);
9282
+ break;
9283
+ }
8725
9284
  let opencodeMessageId;
8726
9285
  let preDispatchUserIds = null;
8727
9286
  try {
@@ -9846,6 +10405,12 @@ var ChannelDriver = class _ChannelDriver {
9846
10405
  b2AbandonedWithoutAnswer: false,
9847
10406
  ambiguousPinnedSinceMs: 0,
9848
10407
  ambiguousResolved: false,
10408
+ midFlightPinnedSinceMs: 0,
10409
+ midFlightNotOngoingStreak: 0,
10410
+ midFlightLastStatusCheckMs: 0,
10411
+ midFlightOrphanSignalled: false,
10412
+ midFlightOrphanWithoutAnswer: false,
10413
+ midFlightHeldLogged: false,
9849
10414
  sessionStoreFailurePending: false,
9850
10415
  neverStartedResolved: false
9851
10416
  });
@@ -9985,6 +10550,12 @@ var ChannelDriver = class _ChannelDriver {
9985
10550
  b2AbandonedWithoutAnswer: false,
9986
10551
  ambiguousPinnedSinceMs: 0,
9987
10552
  ambiguousResolved: false,
10553
+ midFlightPinnedSinceMs: 0,
10554
+ midFlightNotOngoingStreak: 0,
10555
+ midFlightLastStatusCheckMs: 0,
10556
+ midFlightOrphanSignalled: false,
10557
+ midFlightOrphanWithoutAnswer: false,
10558
+ midFlightHeldLogged: false,
9988
10559
  sessionStoreFailurePending: false,
9989
10560
  neverStartedResolved: false
9990
10561
  });
@@ -10756,7 +11327,11 @@ var ChannelDriver = class _ChannelDriver {
10756
11327
  watcher,
10757
11328
  inFlight,
10758
11329
  messages,
10759
- finish
11330
+ composeAbandonmentNote(
11331
+ finish,
11332
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11333
+ "without-answer"
11334
+ )
10760
11335
  );
10761
11336
  } else {
10762
11337
  await this.settleMessageDone(
@@ -10815,7 +11390,11 @@ var ChannelDriver = class _ChannelDriver {
10815
11390
  watcher,
10816
11391
  inFlight,
10817
11392
  messages,
10818
- finish
11393
+ composeAbandonmentNote(
11394
+ finish,
11395
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11396
+ "without-answer"
11397
+ )
10819
11398
  );
10820
11399
  return;
10821
11400
  }
@@ -10909,6 +11488,106 @@ var ChannelDriver = class _ChannelDriver {
10909
11488
  return;
10910
11489
  }
10911
11490
  }
11491
+ const midFlightPinnedNow = activelyRunning && isMidFlightPinnedRunning(messages, inFlight.opencodeMessageId);
11492
+ if (!midFlightPinnedNow) {
11493
+ if (snapshotReadable) {
11494
+ inFlight.midFlightPinnedSinceMs = 0;
11495
+ inFlight.midFlightNotOngoingStreak = 0;
11496
+ inFlight.midFlightLastStatusCheckMs = 0;
11497
+ inFlight.midFlightOrphanSignalled = false;
11498
+ inFlight.midFlightOrphanWithoutAnswer = false;
11499
+ inFlight.midFlightHeldLogged = false;
11500
+ }
11501
+ } else {
11502
+ if (inFlight.midFlightOrphanSignalled) {
11503
+ const note = composeAbandonmentNote(
11504
+ void 0,
11505
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11506
+ inFlight.midFlightOrphanWithoutAnswer ? "midflight-without-answer" : "midflight-with-answer"
11507
+ );
11508
+ if (inFlight.midFlightOrphanWithoutAnswer) {
11509
+ await this.settleMessageAbandonedWithoutAnswer(
11510
+ sessionId,
11511
+ watcher,
11512
+ inFlight,
11513
+ messages,
11514
+ note
11515
+ );
11516
+ } else {
11517
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
11518
+ }
11519
+ return;
11520
+ }
11521
+ if (inFlight.midFlightPinnedSinceMs === 0) {
11522
+ inFlight.midFlightPinnedSinceMs = this.now();
11523
+ }
11524
+ const pinnedForMs = this.now() - inFlight.midFlightPinnedSinceMs;
11525
+ if (pinnedForMs >= MIDFLIGHT_ORPHAN_MIN_PINNED_MS && this.now() - inFlight.midFlightLastStatusCheckMs >= MIDFLIGHT_ORPHAN_RECHECK_MS) {
11526
+ inFlight.midFlightLastStatusCheckMs = this.now();
11527
+ const rootOngoing = await this.isSessionOngoing(sessionId);
11528
+ inFlight.midFlightNotOngoingStreak = rootOngoing === false ? inFlight.midFlightNotOngoingStreak + 1 : 0;
11529
+ if (isMidFlightOrphanConfirmed({
11530
+ pinnedForMs,
11531
+ minPinnedMs: MIDFLIGHT_ORPHAN_MIN_PINNED_MS,
11532
+ notOngoingStreak: inFlight.midFlightNotOngoingStreak,
11533
+ minNotOngoingStreak: MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK
11534
+ })) {
11535
+ if (this.sessionStoreHealth?.isUnwritable()) {
11536
+ inFlight.sessionStoreFailurePending = true;
11537
+ void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
11538
+ watched_for_ms: pinnedForMs,
11539
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
11540
+ opencode_message_id: inFlight.opencodeMessageId,
11541
+ run_state: "failed"
11542
+ });
11543
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
11544
+ return;
11545
+ }
11546
+ inFlight.midFlightOrphanSignalled = true;
11547
+ const noteOutcome = extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null ? "midflight-without-answer" : "midflight-with-answer";
11548
+ inFlight.midFlightOrphanWithoutAnswer = noteOutcome === "midflight-without-answer";
11549
+ void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
11550
+ watched_for_ms: pinnedForMs,
11551
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
11552
+ opencode_message_id: inFlight.opencodeMessageId,
11553
+ ...inFlight.midFlightOrphanWithoutAnswer ? { run_state: "failed" } : {}
11554
+ });
11555
+ const note = composeAbandonmentNote(
11556
+ void 0,
11557
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11558
+ noteOutcome
11559
+ );
11560
+ this.log({
11561
+ level: "warn",
11562
+ 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`,
11563
+ conversation_id: conv.id,
11564
+ message_id: id
11565
+ });
11566
+ if (inFlight.midFlightOrphanWithoutAnswer) {
11567
+ await this.settleMessageAbandonedWithoutAnswer(
11568
+ sessionId,
11569
+ watcher,
11570
+ inFlight,
11571
+ messages,
11572
+ note
11573
+ );
11574
+ } else {
11575
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
11576
+ }
11577
+ return;
11578
+ }
11579
+ if (rootOngoing === true && !inFlight.midFlightHeldLogged) {
11580
+ inFlight.midFlightHeldLogged = true;
11581
+ const replyShape = describeAbandonedReplyShape(messages, inFlight.opencodeMessageId);
11582
+ this.log({
11583
+ level: "warn",
11584
+ 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)"}`,
11585
+ conversation_id: conv.id,
11586
+ message_id: id
11587
+ });
11588
+ }
11589
+ }
11590
+ }
10912
11591
  if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
10913
11592
  await this.settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick);
10914
11593
  return;
@@ -11108,13 +11787,8 @@ var ChannelDriver = class _ChannelDriver {
11108
11787
  this.removeInFlight(watcher, inFlight.evidentMessageId);
11109
11788
  }
11110
11789
  /** Report a confirmed b2 abandonment that produced no answer text. */
11111
- async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
11790
+ async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, error2) {
11112
11791
  const conv = watcher.conv;
11113
- const error2 = composeAbandonmentNote(
11114
- finish,
11115
- describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
11116
- "without-answer"
11117
- );
11118
11792
  this.anchorDeliveryDeadline(inFlight);
11119
11793
  this.log({
11120
11794
  level: "warn",
@@ -13500,7 +14174,7 @@ import ora3 from "ora";
13500
14174
  import { select as select3 } from "@inquirer/prompts";
13501
14175
  function unknownPasswordError2(port) {
13502
14176
  return new Error(
13503
- `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
14177
+ `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.`
13504
14178
  );
13505
14179
  }
13506
14180
  var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
@@ -13518,9 +14192,9 @@ async function ensureOpenCode2Running(ctx) {
13518
14192
  password: null
13519
14193
  };
13520
14194
  }
13521
- if (!isOpenCode2Installed()) {
14195
+ if (!ctx.command && !isOpenCode2Installed()) {
13522
14196
  throw new Error(
13523
- `OpenCode V2 (opencode2) is not installed. Install it with: npm install -g ${OPENCODE2_PACKAGE_SPEC}`
14197
+ "OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
13524
14198
  );
13525
14199
  }
13526
14200
  let port = ctx.port;
@@ -13548,7 +14222,8 @@ Port ${port} is already in use.`));
13548
14222
  if (!ctx.interactive) {
13549
14223
  ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
13550
14224
  const { child: proc, password } = await startOpenCode2(port, {
13551
- inheritStdio: ctx.inheritStdio
14225
+ inheritStdio: ctx.inheritStdio,
14226
+ command: ctx.command
13552
14227
  });
13553
14228
  const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
13554
14229
  if (!health.healthy) {
@@ -13569,13 +14244,14 @@ Port ${port} is already in use.`));
13569
14244
  password
13570
14245
  };
13571
14246
  }
14247
+ const command = ctx.command ?? "opencode2";
13572
14248
  const action = await select3({
13573
14249
  message: "OpenCode V2 is not running. What would you like to do?",
13574
14250
  choices: [
13575
14251
  {
13576
14252
  name: "Start OpenCode V2 for me",
13577
14253
  value: "start",
13578
- description: `Run 'opencode2 serve --port ${port}'`
14254
+ description: `Run '${command} serve --port ${port}'`
13579
14255
  },
13580
14256
  {
13581
14257
  name: "Show me the command",
@@ -13593,14 +14269,15 @@ Port ${port} is already in use.`));
13593
14269
  blank();
13594
14270
  console.log(chalk6.bold("Run this command in another terminal:"));
13595
14271
  blank();
13596
- console.log(` ${chalk6.cyan(`opencode2 serve --port ${port}`)}`);
14272
+ console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
13597
14273
  blank();
13598
14274
  throw new Error("Please start OpenCode V2 manually");
13599
14275
  }
13600
14276
  if (action === "start") {
13601
14277
  const spinner = ora3("Starting OpenCode V2...").start();
13602
14278
  const { child: proc, password } = await startOpenCode2(port, {
13603
- inheritStdio: ctx.inheritStdio
14279
+ inheritStdio: ctx.inheritStdio,
14280
+ command: ctx.command
13604
14281
  });
13605
14282
  const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
13606
14283
  if (!health.healthy) {
@@ -13626,7 +14303,7 @@ Port ${port} is already in use.`));
13626
14303
  }
13627
14304
 
13628
14305
  // src/lib/runner-credentials.ts
13629
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "node:fs";
14306
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync5 } from "node:fs";
13630
14307
  import { spawn as spawn5 } from "node:child_process";
13631
14308
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
13632
14309
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
@@ -13869,8 +14546,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
13869
14546
  }
13870
14547
  try {
13871
14548
  env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
13872
- writeFileSync4(GIT_CONFIG_GLOBAL, "");
13873
- writeFileSync4(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
14549
+ writeFileSync5(GIT_CONFIG_GLOBAL, "");
14550
+ writeFileSync5(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
13874
14551
  chmodSync2(GIT_CREDENTIAL_HELPER, 448);
13875
14552
  const config = [
13876
14553
  ["user.name", env.GIT_USER_NAME ?? "evident-bot"],
@@ -13903,7 +14580,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
13903
14580
  // src/lib/opencode/config-overlay.ts
13904
14581
  import { execFileSync as execFileSync2 } from "node:child_process";
13905
14582
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
13906
- import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
14583
+ import { isAbsolute as isAbsolute3, join as join12, resolve as resolve4 } from "node:path";
13907
14584
  function parseConfigContent(content) {
13908
14585
  if (!content?.trim()) return {};
13909
14586
  const parsed = JSON.parse(content);
@@ -13933,7 +14610,7 @@ function applyRunnerOpenCodeConfig({
13933
14610
  return;
13934
14611
  }
13935
14612
  const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
13936
- const target = isFile(join11(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
14613
+ const target = isFile(join12(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
13937
14614
  if (!isFile(source)) {
13938
14615
  log3(
13939
14616
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -13941,7 +14618,7 @@ function applyRunnerOpenCodeConfig({
13941
14618
  );
13942
14619
  return;
13943
14620
  }
13944
- copyFileSync(source, join11(cwd, target));
14621
+ copyFileSync(source, join12(cwd, target));
13945
14622
  try {
13946
14623
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
13947
14624
  stdio: "ignore"
@@ -13950,13 +14627,13 @@ function applyRunnerOpenCodeConfig({
13950
14627
  const detail = error2 instanceof Error ? error2.message : String(error2);
13951
14628
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
13952
14629
  }
13953
- log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
14630
+ log3(`Applied runner OpenCode config ${source} to ${join12(cwd, target)}`);
13954
14631
  }
13955
14632
 
13956
14633
  // src/lib/opencode/runtime-provenance.ts
13957
- import { readFileSync as readFileSync7, readdirSync } from "node:fs";
14634
+ import { readFileSync as readFileSync8, readdirSync } from "node:fs";
13958
14635
  import { homedir as homedir7 } from "node:os";
13959
- import { join as join12 } from "node:path";
14636
+ import { join as join13 } from "node:path";
13960
14637
  var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
13961
14638
  var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
13962
14639
  function errorKind(error2) {
@@ -14024,7 +14701,7 @@ function parseJsonc(value) {
14024
14701
  return JSON.parse(output);
14025
14702
  }
14026
14703
  function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
14027
- return join12(env.XDG_CACHE_HOME ?? join12(homeDir, ".cache"), "opencode");
14704
+ return join13(env.XDG_CACHE_HOME ?? join13(homeDir, ".cache"), "opencode");
14028
14705
  }
14029
14706
  function readConfiguredPluginSpecs({
14030
14707
  cwd = process.cwd(),
@@ -14033,10 +14710,10 @@ function readConfiguredPluginSpecs({
14033
14710
  let config;
14034
14711
  try {
14035
14712
  try {
14036
- config = parseJsonc(readFileSync7(join12(cwd, "opencode.jsonc"), "utf8"));
14713
+ config = parseJsonc(readFileSync8(join13(cwd, "opencode.jsonc"), "utf8"));
14037
14714
  } catch (error2) {
14038
14715
  if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
14039
- config = parseJsonc(readFileSync7(join12(cwd, "opencode.json"), "utf8"));
14716
+ config = parseJsonc(readFileSync8(join13(cwd, "opencode.json"), "utf8"));
14040
14717
  }
14041
14718
  } catch (error2) {
14042
14719
  log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
@@ -14064,7 +14741,7 @@ function resolveInstalledPluginVersions({
14064
14741
  homeDir = homedir7(),
14065
14742
  log: log3
14066
14743
  }) {
14067
- const packagesRoot = join12(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14744
+ const packagesRoot = join13(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
14068
14745
  let directories;
14069
14746
  try {
14070
14747
  directories = readdirSync(packagesRoot, { withFileTypes: true });
@@ -14085,8 +14762,8 @@ function resolveInstalledPluginVersions({
14085
14762
  let resolvedVersion = null;
14086
14763
  try {
14087
14764
  const packageJson = JSON.parse(
14088
- readFileSync7(
14089
- join12(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14765
+ readFileSync8(
14766
+ join13(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
14090
14767
  "utf8"
14091
14768
  )
14092
14769
  );
@@ -14107,7 +14784,7 @@ function resolveInstalledPluginVersions({
14107
14784
  }
14108
14785
 
14109
14786
  // src/lib/credential-sync.ts
14110
- import { renameSync, writeFileSync as writeFileSync5 } from "node:fs";
14787
+ import { renameSync, writeFileSync as writeFileSync6 } from "node:fs";
14111
14788
  var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
14112
14789
  var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
14113
14790
  var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
@@ -14141,7 +14818,7 @@ function writeMarker(markerPath, outcomes, log3) {
14141
14818
  `;
14142
14819
  const temporaryPath = `${markerPath}.tmp`;
14143
14820
  try {
14144
- writeFileSync5(temporaryPath, body, { mode: 384 });
14821
+ writeFileSync6(temporaryPath, body, { mode: 384 });
14145
14822
  renameSync(temporaryPath, markerPath);
14146
14823
  } catch (error2) {
14147
14824
  log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
@@ -14381,15 +15058,25 @@ async function responseJson(response) {
14381
15058
  function readSetup(value) {
14382
15059
  if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
14383
15060
  const setup = value;
14384
- if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string") {
15061
+ if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string" || typeof setup.acknowledged_at !== "string" && setup.acknowledged_at !== null) {
14385
15062
  return null;
14386
15063
  }
14387
15064
  return setup;
14388
15065
  }
14389
15066
  function clearLocalSetup(options) {
14390
15067
  const env = options.env ?? process.env;
14391
- (options.store ?? defaultStore).clear(options.agentId);
14392
- delete env.EVIDENT_PROXY_TOKEN;
15068
+ const store = options.store ?? defaultStore;
15069
+ const stored = store.get(options.agentId);
15070
+ store.clear(options.agentId);
15071
+ const token = env.EVIDENT_PROXY_TOKEN;
15072
+ if (token !== void 0 && token === stored?.token) {
15073
+ delete env.EVIDENT_PROXY_TOKEN;
15074
+ } else if (token !== void 0) {
15075
+ options.log(
15076
+ `MANAGED-INFERENCE-CLEANUP-PRESERVED-EXTERNAL-TOKEN: runner=${options.agentId}`,
15077
+ "debug"
15078
+ );
15079
+ }
14393
15080
  }
14394
15081
  async function discoverManagedInferenceSetup(options) {
14395
15082
  const headers = requestHeaders(options);
@@ -14413,6 +15100,25 @@ async function discoverManagedInferenceSetup(options) {
14413
15100
  const setup = readSetup(body?.setup);
14414
15101
  return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed setup discovery returned an invalid response" };
14415
15102
  }
15103
+ async function enrollManagedInferenceSetup(options) {
15104
+ const response = await post(options, "enroll", {});
15105
+ if (!response) {
15106
+ return { kind: "failed", reason: safeFailure("Managed credential enrollment") };
15107
+ }
15108
+ if (response.status === 404) {
15109
+ return { kind: "none" };
15110
+ }
15111
+ const body = await responseJson(response);
15112
+ if (!response.ok) {
15113
+ const serverError = body?.error;
15114
+ return {
15115
+ kind: "failed",
15116
+ reason: typeof serverError === "string" && serverError.trim() ? redactDiagnosticText(serverError).slice(0, 500) : safeFailure("Managed credential enrollment", response.status)
15117
+ };
15118
+ }
15119
+ const setup = readSetup(body?.setup);
15120
+ return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed credential enrollment returned an invalid response" };
15121
+ }
14416
15122
  async function post(options, operation, body) {
14417
15123
  const headers = requestHeaders(options);
14418
15124
  try {
@@ -14508,7 +15214,7 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
14508
15214
  }
14509
15215
  return reaffirmStored(options, stored);
14510
15216
  }
14511
- if (discovery.setup.status === "acknowledged" && stored?.generation === discovery.setup.generation && stored.tokenId === discovery.setup.token_id) {
15217
+ if (discovery.setup.acknowledged_at !== null && stored?.generation === discovery.setup.generation && stored.tokenId === discovery.setup.token_id) {
14512
15218
  return reaffirmStored(options, stored);
14513
15219
  }
14514
15220
  const response = await post(options, "bootstrap", { generation: discovery.setup.generation });
@@ -14594,33 +15300,49 @@ function resolveLogLevel(options) {
14594
15300
  }
14595
15301
  return "info";
14596
15302
  }
14597
- function resolveFileSyncDirectories(raw, homeDir) {
14598
- const directories = [];
15303
+ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
15304
+ const trimmed = entry.trim();
15305
+ if (trimmed === "") {
15306
+ throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
15307
+ }
15308
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join14(homeDir, trimmed.slice(2)) : trimmed;
15309
+ if (!isAbsolute4(expanded)) {
15310
+ throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
15311
+ }
15312
+ const normalized = resolvePath2(expanded);
15313
+ if (parse(normalized).root === normalized) {
15314
+ throw new Error(
15315
+ `${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
15316
+ );
15317
+ }
15318
+ return normalized;
15319
+ }
15320
+ function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
15321
+ const paths = [];
14599
15322
  for (const entry of raw ?? []) {
14600
- const trimmed = entry.trim();
14601
- if (trimmed === "") {
14602
- throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
14603
- }
14604
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
14605
- if (!isAbsolute4(expanded)) {
14606
- throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
14607
- }
14608
- const normalized = resolvePath2(expanded);
14609
- if (parse(normalized).root === normalized) {
14610
- throw new Error(
14611
- `--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
14612
- );
14613
- }
14614
- if (!directories.includes(normalized)) {
14615
- directories.push(normalized);
14616
- }
15323
+ const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
15324
+ if (!paths.includes(normalized)) paths.push(normalized);
14617
15325
  }
14618
- if (directories.length > MAX_FILE_SYNC_DIRECTORIES) {
15326
+ if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
14619
15327
  throw new Error(
14620
- `--enable-file-sync-to accepts at most ${MAX_FILE_SYNC_DIRECTORIES} directories; got ${directories.length}`
15328
+ `${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
14621
15329
  );
14622
15330
  }
14623
- return directories;
15331
+ return paths;
15332
+ }
15333
+ function resolveFileSyncDirectories(raw, homeDir) {
15334
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
15335
+ noun: "directory",
15336
+ plural: "directories",
15337
+ rootExample: "~/.claude"
15338
+ });
15339
+ }
15340
+ function resolveFileSyncExactFiles(raw, homeDir) {
15341
+ return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
15342
+ noun: "file",
15343
+ plural: "files",
15344
+ rootExample: "~/.local/share/opencode/mcp-auth.json"
15345
+ });
14624
15346
  }
14625
15347
  var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
14626
15348
  var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
@@ -14636,13 +15358,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
14636
15358
  raw = env[OPENCODE_VERSION_ENV];
14637
15359
  source = OPENCODE_VERSION_ENV;
14638
15360
  } else {
14639
- return { version: "v1", warnings: [] };
15361
+ return { version: "auto", warnings: [] };
14640
15362
  }
14641
15363
  const normalized = raw.trim().toLowerCase();
14642
- if (normalized !== "v1" && normalized !== "v2") {
15364
+ if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
14643
15365
  return {
14644
- version: "v1",
14645
- warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or v2; using the default v1`]
15366
+ version: "auto",
15367
+ warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
14646
15368
  };
14647
15369
  }
14648
15370
  return { version: normalized, warnings: [] };
@@ -15257,7 +15979,7 @@ function shouldWarnForReclaimSkip(reason) {
15257
15979
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
15258
15980
  }
15259
15981
  function sessionDbPath() {
15260
- return join13(homedir8(), ".local", "share", "opencode", "opencode.db");
15982
+ return join14(homedir8(), ".local", "share", "opencode", "opencode.db");
15261
15983
  }
15262
15984
  function logSessionCleanupActivity(state, phase, entry) {
15263
15985
  logActivity(state, {
@@ -15266,6 +15988,47 @@ function logSessionCleanupActivity(state, phase, entry) {
15266
15988
  metadata: { ...entry.metadata, phase }
15267
15989
  });
15268
15990
  }
15991
+ async function drainSessionDeletionReports(state) {
15992
+ const path = sessionDeletionReportPath(homedir8(), process.env);
15993
+ let delivered = 0;
15994
+ let failure = null;
15995
+ try {
15996
+ const spool = readSessionDeletionRecords(path);
15997
+ if (spool.readError !== null) {
15998
+ failure = `could not read spool: ${spool.readError}`;
15999
+ } else if (spool.records.length === 0) {
16000
+ return;
16001
+ } else if (!state.agentId || !state.authHeader) {
16002
+ failure = "runner authentication is not available";
16003
+ } else {
16004
+ for (let offset = 0; offset < spool.records.length; offset += 200) {
16005
+ const batch = spool.records.slice(offset, offset + 200);
16006
+ const result = await reportSessionDeletions(state.agentId, state.authHeader, batch);
16007
+ if (!result.ok) {
16008
+ failure = result.error ?? "unknown API error";
16009
+ break;
16010
+ }
16011
+ removeDeliveredSessionDeletionRecords(path, batch);
16012
+ delivered += batch.length;
16013
+ }
16014
+ }
16015
+ } catch (error2) {
16016
+ failure = error2 instanceof Error ? error2.message : String(error2);
16017
+ }
16018
+ const pending = readSessionDeletionRecords(path);
16019
+ const pendingCount = pending.readError === null ? pending.records.length : null;
16020
+ const message = `Session deletion reports: delivered ${delivered}, ${pendingCount === null ? "pending count unavailable" : `${pendingCount} pending`}` + (failure === null ? "." : `; delivery failed: ${failure}.`);
16021
+ logActivity(state, {
16022
+ type: "info",
16023
+ level: failure === null ? "info" : "warn",
16024
+ message,
16025
+ metadata: {
16026
+ outcome: failure === null ? "session-deletion-reports-drained" : "session-deletion-reports-pending",
16027
+ delivered,
16028
+ pending: pendingCount
16029
+ }
16030
+ });
16031
+ }
15269
16032
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
15270
16033
  const record = {
15271
16034
  v: 1,
@@ -15323,17 +16086,22 @@ async function runSweep(state, driver, config) {
15323
16086
  let failedThrew = 0;
15324
16087
  let skippedNewlyActive = 0;
15325
16088
  let remaining = 0;
16089
+ const deletionRecords = [];
15326
16090
  let protectedNow = driver.protectedSessionIds();
15327
16091
  if (sessions !== null) {
15328
- const toDelete = selectSessionsToDelete(
15329
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
15330
- {
15331
- maxAgeMs: config.maxAgeMs,
15332
- maxCount: config.maxCount,
15333
- nowMs: Date.now(),
15334
- protectedIds: protectedNow
15335
- }
16092
+ const snapshots = sessions.map((session) => ({
16093
+ id: session.id,
16094
+ lastActivityMs: sessionLastActivityMs(session)
16095
+ }));
16096
+ const lastActivityById = new Map(
16097
+ snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
15336
16098
  );
16099
+ const toDelete = selectSessionsToDelete(snapshots, {
16100
+ maxAgeMs: config.maxAgeMs,
16101
+ maxCount: config.maxCount,
16102
+ nowMs: Date.now(),
16103
+ protectedIds: protectedNow
16104
+ });
15337
16105
  const eligibleToDelete = toDelete.filter(
15338
16106
  (id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
15339
16107
  );
@@ -15353,6 +16121,17 @@ async function runSweep(state, driver, config) {
15353
16121
  const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
15354
16122
  if (outcome.ok) {
15355
16123
  deleted++;
16124
+ const lastActivityMs = lastActivityById.get(id) ?? null;
16125
+ deletionRecords.push({
16126
+ opencode_session_id: id,
16127
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
16128
+ reason: "auto_cleanup",
16129
+ policy: {
16130
+ max_age_ms: config.maxAgeMs ?? null,
16131
+ max_count: config.maxCount ?? null,
16132
+ last_activity_at: lastActivityMs === null ? null : new Date(lastActivityMs).toISOString()
16133
+ }
16134
+ });
15356
16135
  } else if (outcome.status === "threw") {
15357
16136
  failed++;
15358
16137
  failedThrew++;
@@ -15408,6 +16187,32 @@ async function runSweep(state, driver, config) {
15408
16187
  message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
15409
16188
  });
15410
16189
  }
16190
+ if (deletionRecords.length > 0) {
16191
+ try {
16192
+ const dropped = appendSessionDeletionRecords(
16193
+ sessionDeletionReportPath(homedir8(), process.env),
16194
+ deletionRecords
16195
+ );
16196
+ if (dropped > 0) {
16197
+ logSessionCleanupActivity(state, "sweep", {
16198
+ type: "info",
16199
+ level: "warn",
16200
+ message: `Session cleanup: dropped ${dropped} oldest pending deletion report(s) after reaching the spool limit`,
16201
+ metadata: { outcome: "deletion-reports-dropped", dropped_count: dropped }
16202
+ });
16203
+ }
16204
+ } catch (error2) {
16205
+ logSessionCleanupActivity(state, "sweep", {
16206
+ type: "info",
16207
+ level: "warn",
16208
+ message: `Session cleanup: could not persist ${deletionRecords.length} confirmed deletion report(s): ${error2 instanceof Error ? error2.message : String(error2)}`,
16209
+ metadata: {
16210
+ outcome: "deletion-report-spool-write-failed",
16211
+ records: deletionRecords.length
16212
+ }
16213
+ });
16214
+ }
16215
+ }
15411
16216
  const sessionMetadata = {
15412
16217
  sessions_inspected: sessions?.length ?? null,
15413
16218
  sessions_deleted: deleted,
@@ -15477,6 +16282,7 @@ async function runSweep(state, driver, config) {
15477
16282
  }
15478
16283
  });
15479
16284
  }
16285
+ await drainSessionDeletionReports(state);
15480
16286
  } catch (error2) {
15481
16287
  const message = error2 instanceof Error ? error2.message : String(error2);
15482
16288
  logSessionCleanupActivity(state, "sweep", {
@@ -15959,9 +16765,11 @@ async function run(options) {
15959
16765
  const interactive = isInteractive(options.json);
15960
16766
  let logLevel;
15961
16767
  let fileSyncDirectories;
16768
+ let fileSyncExactFiles;
15962
16769
  try {
15963
16770
  logLevel = resolveLogLevel(options);
15964
16771
  fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
16772
+ fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
15965
16773
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
15966
16774
  throw new Error(
15967
16775
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -16037,7 +16845,11 @@ async function run(options) {
16037
16845
  }
16038
16846
  if (fileSyncDirectories.length > 0) {
16039
16847
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
16040
- } else {
16848
+ }
16849
+ if (fileSyncExactFiles.length > 0) {
16850
+ log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
16851
+ }
16852
+ if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
16041
16853
  log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
16042
16854
  }
16043
16855
  if (!options.runner && options.agent) {
@@ -16319,6 +17131,7 @@ async function run(options) {
16319
17131
  }
16320
17132
  }
16321
17133
  reportSessionDbRecovery(state);
17134
+ await drainSessionDeletionReports(state);
16322
17135
  if (sessionDbVerifyFatal) {
16323
17136
  throw new Error(
16324
17137
  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`
@@ -16338,13 +17151,75 @@ async function run(options) {
16338
17151
  for (const warning2 of opencodeStartTimeoutWarnings) {
16339
17152
  logActivity(state, { type: "info", level: "warn", message: warning2 });
16340
17153
  }
16341
- const { version: opencodeVersion, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(
16342
- options,
16343
- process.env
16344
- );
17154
+ const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
16345
17155
  for (const warning2 of opencodeVersionWarnings) {
16346
17156
  logActivity(state, { type: "info", level: "warn", message: warning2 });
16347
17157
  }
17158
+ let opencodeMajor;
17159
+ let opencodeV2Command;
17160
+ let opencodeDetectionSource;
17161
+ let opencodeDetectionLevel = "info";
17162
+ let opencodeDetectionMessage;
17163
+ if (opencodeVersionSelection === "v1") {
17164
+ opencodeMajor = "v1";
17165
+ opencodeDetectionSource = "explicit";
17166
+ opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
17167
+ } else if (opencodeVersionSelection === "v2") {
17168
+ const installed = resolveInstalledOpenCode();
17169
+ opencodeMajor = "v2";
17170
+ opencodeDetectionSource = "explicit";
17171
+ if (installed.kind === "installed" && installed.major === "v2") {
17172
+ opencodeV2Command = installed.command;
17173
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
17174
+ } else if (installed.kind === "unknown") {
17175
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
17176
+ } else if (installed.kind === "installed") {
17177
+ opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
17178
+ } else {
17179
+ opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
17180
+ }
17181
+ } else {
17182
+ const running = await classifyRunningOpenCode(state.port);
17183
+ if (running.kind === "v1") {
17184
+ opencodeMajor = "v1";
17185
+ opencodeDetectionSource = "running-server";
17186
+ opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
17187
+ } else if (running.kind === "v2") {
17188
+ opencodeMajor = "v2";
17189
+ opencodeDetectionSource = "running-server";
17190
+ opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
17191
+ } else if (running.kind === "authenticated-unknown") {
17192
+ 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.`;
17193
+ logActivity(state, {
17194
+ type: "info",
17195
+ level: "warn",
17196
+ message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
17197
+ });
17198
+ throw new Error(message);
17199
+ } else {
17200
+ const installed = resolveInstalledOpenCode();
17201
+ opencodeDetectionLevel = "warn";
17202
+ if (installed.kind === "installed") {
17203
+ opencodeMajor = installed.major;
17204
+ opencodeDetectionSource = "installed-binary";
17205
+ if (installed.major === "v2") opencodeV2Command = installed.command;
17206
+ 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"}.`;
17207
+ } else if (installed.kind === "unknown") {
17208
+ opencodeMajor = "v1";
17209
+ opencodeDetectionSource = "fallback-v1";
17210
+ 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.`;
17211
+ } else {
17212
+ opencodeMajor = "v1";
17213
+ opencodeDetectionSource = "fallback-v1";
17214
+ 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.`;
17215
+ }
17216
+ }
17217
+ }
17218
+ logActivity(state, {
17219
+ type: "info",
17220
+ level: opencodeDetectionLevel,
17221
+ message: opencodeDetectionMessage
17222
+ });
16348
17223
  const managedSetupOptions = {
16349
17224
  agentId: state.agentId,
16350
17225
  apiUrl: getApiUrlConfig(),
@@ -16356,13 +17231,16 @@ async function run(options) {
16356
17231
  ...level === "error" ? { error: message } : { message }
16357
17232
  })
16358
17233
  };
16359
- const managedDiscovery = await discoverManagedInferenceSetup(managedSetupOptions);
17234
+ let managedDiscovery = await discoverManagedInferenceSetup(managedSetupOptions);
17235
+ if (managedDiscovery.kind === "none") {
17236
+ managedDiscovery = await enrollManagedInferenceSetup(managedSetupOptions);
17237
+ }
16360
17238
  let managedPreparation = { kind: "none" };
16361
17239
  if (managedDiscovery.kind === "failed") {
16362
17240
  managedPreparation = managedDiscovery;
16363
17241
  managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
16364
17242
  } else if (managedDiscovery.kind === "setup") {
16365
- const managedHealth = opencodeVersion === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
17243
+ const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
16366
17244
  managedPreparation = await prepareManagedInferenceSetup(
16367
17245
  managedSetupOptions,
16368
17246
  managedDiscovery,
@@ -16387,13 +17265,14 @@ async function run(options) {
16387
17265
  const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
16388
17266
  const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
16389
17267
  try {
16390
- const oc = opencodeVersion === "v2" ? await ensureOpenCode2Running({
17268
+ const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
16391
17269
  port: state.port,
16392
17270
  interactive: state.interactive,
16393
17271
  agentId: state.agentId,
16394
17272
  log: (message) => log2(state, message),
16395
17273
  startTimeoutMs: opencodeStartTimeoutMs,
16396
- inheritStdio: Boolean(options.opencodePidFile)
17274
+ inheritStdio: Boolean(options.opencodePidFile),
17275
+ command: opencodeV2Command
16397
17276
  }) : await ensureOpenCodeRunning({
16398
17277
  port: state.port,
16399
17278
  interactive: state.interactive,
@@ -16405,7 +17284,7 @@ async function run(options) {
16405
17284
  state.port = oc.port;
16406
17285
  state.opencodeProcess = options.opencodePidFile ? null : oc.process;
16407
17286
  state.opencodeVersion = oc.version;
16408
- state.opencodeApiVersion = opencodeVersion;
17287
+ state.opencodeApiVersion = opencodeMajor;
16409
17288
  let configuredPlugins = [];
16410
17289
  let resolvedPlugins = [];
16411
17290
  try {
@@ -16427,13 +17306,14 @@ async function run(options) {
16427
17306
  metadata: {
16428
17307
  opencode_version: state.opencodeVersion,
16429
17308
  opencode_api_version: state.opencodeApiVersion,
17309
+ opencode_detection_source: opencodeDetectionSource,
16430
17310
  configured_plugins: JSON.stringify(configuredPlugins),
16431
17311
  resolved_plugins: JSON.stringify(resolvedPlugins)
16432
17312
  },
16433
17313
  source: "cli.runtime-provenance"
16434
17314
  });
16435
17315
  let opencodePassword = null;
16436
- if (opencodeVersion === "v2" && "password" in oc) {
17316
+ if (opencodeMajor === "v2" && "password" in oc) {
16437
17317
  const value = oc.password;
16438
17318
  if (typeof value === "string" || value === null) opencodePassword = value;
16439
17319
  }
@@ -16446,7 +17326,7 @@ async function run(options) {
16446
17326
  state.opencodeClient = openCodeClient;
16447
17327
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
16448
17328
  try {
16449
- writeFileSync6(options.opencodePidFile, `${oc.process.pid}
17329
+ writeFileSync7(options.opencodePidFile, `${oc.process.pid}
16450
17330
  `, { mode: 384 });
16451
17331
  chmodSync3(options.opencodePidFile, 384);
16452
17332
  } catch (error2) {
@@ -16556,7 +17436,7 @@ async function run(options) {
16556
17436
  let existingPid;
16557
17437
  if (existsSync3(options.litestreamPidFile)) {
16558
17438
  try {
16559
- const rawPid = readFileSync8(options.litestreamPidFile, "utf8").trim();
17439
+ const rawPid = readFileSync9(options.litestreamPidFile, "utf8").trim();
16560
17440
  const parsedPid = Number(rawPid);
16561
17441
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
16562
17442
  existingPid = parsedPid;
@@ -16593,7 +17473,7 @@ async function run(options) {
16593
17473
  });
16594
17474
  try {
16595
17475
  if (litestreamProcess.pid !== void 0) {
16596
- writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
17476
+ writeFileSync7(options.litestreamPidFile, `${litestreamProcess.pid}
16597
17477
  `, {
16598
17478
  mode: 384
16599
17479
  });
@@ -16652,9 +17532,10 @@ async function run(options) {
16652
17532
  conversationFilter: state.conversationFilter,
16653
17533
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
16654
17534
  sessionStoreHealth: state.sessionStoreHealth,
16655
- // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
16656
- // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
17535
+ // #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
17536
+ // `file_sync_disabled` on the ack, not silently ignored.
16657
17537
  fileSyncDirectories,
17538
+ fileSyncExactFiles,
16658
17539
  homeDir: homedir8(),
16659
17540
  maxActiveSessions,
16660
17541
  log: (entry) => (
@@ -16925,8 +17806,8 @@ program.command("run").description("Connect to Evident and process messages").op
16925
17806
  "--opencode-start-timeout <seconds>",
16926
17807
  "Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
16927
17808
  ).option(
16928
- "--opencode-version <v1|v2>",
16929
- "Which OpenCode major version to launch: v1 or v2 (default: v1). Env: EVIDENT_OPENCODE_VERSION"
17809
+ "--opencode-version <v1|v2|auto>",
17810
+ "OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
16930
17811
  ).option("--json", "Output in JSON format").option(
16931
17812
  "--session-cleanup-max-age <duration>",
16932
17813
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
@@ -16953,6 +17834,11 @@ program.command("run").description("Connect to Evident and process messages").op
16953
17834
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
16954
17835
  (value, previous) => previous.concat([value]),
16955
17836
  []
17837
+ ).option(
17838
+ "--enable-file-sync-to-exact-file <path>",
17839
+ "Allow Evident to write files to exactly this path (repeatable).",
17840
+ (value, previous) => previous.concat([value]),
17841
+ []
16956
17842
  ).option(
16957
17843
  "--tunnel-ready-file <path>",
16958
17844
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
@@ -17015,6 +17901,9 @@ program.command("run").description("Connect to Evident and process messages").op
17015
17901
  // Raw values — expansion/validation is single-sourced in run.ts's
17016
17902
  // resolveFileSyncDirectories.
17017
17903
  enableFileSyncTo: options.enableFileSyncTo,
17904
+ // Raw values — expansion/validation is single-sourced in run.ts's
17905
+ // resolveFileSyncExactFiles.
17906
+ enableFileSyncToExactFile: options.enableFileSyncToExactFile,
17018
17907
  tunnelReadyFile: options.tunnelReadyFile,
17019
17908
  litestreamConfig: options.litestreamConfig,
17020
17909
  opencodePidFile: options.opencodePidFile,