@evident-ai/cli 3.4.1-dev.31006db → 3.4.1-dev.48f83ae

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createRequire } from "module";
4
+ import { createRequire as createRequire2 } from "module";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/login.ts
@@ -763,6 +763,13 @@ function toReportedOpenAiWindow(window) {
763
763
  resets_at: window.resetsAt
764
764
  };
765
765
  }
766
+ function toReportedOpenAiSubscription(snapshot) {
767
+ if (!snapshot.subscription) return null;
768
+ return {
769
+ owner_email: snapshot.subscription.ownerEmail,
770
+ plan_type: snapshot.subscription.planType
771
+ };
772
+ }
766
773
  async function reportOpenAiUsage(agentId, authHeader, snapshot) {
767
774
  try {
768
775
  const apiUrl = getApiUrlConfig();
@@ -773,7 +780,8 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
773
780
  primary: toReportedOpenAiWindow(snapshot.primary),
774
781
  secondary: toReportedOpenAiWindow(snapshot.secondary),
775
782
  has_credits: snapshot.hasCredits,
776
- credits_unlimited: snapshot.creditsUnlimited
783
+ credits_unlimited: snapshot.creditsUnlimited,
784
+ subscription: toReportedOpenAiSubscription(snapshot)
777
785
  }),
778
786
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
779
787
  });
@@ -1183,9 +1191,9 @@ async function claudeUsage() {
1183
1191
  }
1184
1192
 
1185
1193
  // src/commands/run.ts
1186
- import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1194
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
1187
1195
  import { homedir as homedir5 } from "os";
1188
- import { isAbsolute as isAbsolute3, join as join8, parse, resolve as resolvePath2 } from "path";
1196
+ import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
1189
1197
  import chalk6 from "chalk";
1190
1198
 
1191
1199
  // ../../packages/types/src/agents/index.ts
@@ -1525,7 +1533,14 @@ function drainSessionDbRecoveryReport({
1525
1533
  skippedLines++;
1526
1534
  return [];
1527
1535
  }
1528
- return [{ ...value, replication_suspended: value.replication_suspended ?? false }];
1536
+ return [
1537
+ {
1538
+ ...value,
1539
+ provenance_reason: value.provenance_reason ?? null,
1540
+ provenance_migration_delta: value.provenance_migration_delta ?? null,
1541
+ replication_suspended: value.replication_suspended ?? false
1542
+ }
1543
+ ];
1529
1544
  } catch (error2) {
1530
1545
  skippedLines++;
1531
1546
  console.error(
@@ -1628,6 +1643,12 @@ function buildSessionDbRecoveryActivity(record) {
1628
1643
  metadata: withoutContractFields(record),
1629
1644
  message: `This runner did not come online because its damaged session database could not be safely separated from its active backup or proven removed. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them. See the runner boot logs for SESSION-DB-LOCAL-DISCARD-FAILED details.`
1630
1645
  };
1646
+ case "schema_provenance_mismatch":
1647
+ return {
1648
+ level,
1649
+ metadata: withoutContractFields(record),
1650
+ message: `Session database schema provenance mismatch for ${record.dbPath ?? record.db_path ?? "unknown"}: recorded version=${record.recorded_version ?? "unknown"}, current version=${record.current_version ?? "unknown"}, reason=${record.provenance_reason ?? "unknown"}, migration delta=${record.provenance_migration_delta ?? "unknown"}. Inspect the session database and runner backup before continuing.`
1651
+ };
1631
1652
  default:
1632
1653
  return null;
1633
1654
  }
@@ -1642,7 +1663,8 @@ var OUTCOMES = /* @__PURE__ */ new Set([
1642
1663
  "fresh_session_db",
1643
1664
  "history_rolled_back",
1644
1665
  "restore_misconfigured",
1645
- "session_db_boot_refused"
1666
+ "session_db_boot_refused",
1667
+ "schema_provenance_mismatch"
1646
1668
  ]);
1647
1669
  var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
1648
1670
  var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
@@ -1660,7 +1682,7 @@ var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"]
1660
1682
  function isSessionDbRecoveryRecord(value) {
1661
1683
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1662
1684
  const record = value;
1663
- return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
1685
+ return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && (record.provenance_reason === void 0 || record.provenance_reason === null || typeof record.provenance_reason === "string") && (record.provenance_migration_delta === void 0 || record.provenance_migration_delta === null || Number.isInteger(record.provenance_migration_delta)) && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
1664
1686
  (field) => record[field] === null || typeof record[field] === "string"
1665
1687
  );
1666
1688
  }
@@ -1714,10 +1736,14 @@ function runSynchroniser(args, opts) {
1714
1736
  let stderr = "";
1715
1737
  let settled = false;
1716
1738
  const timer = {};
1739
+ let abortListener;
1740
+ let spawnListener;
1717
1741
  const finish = (result) => {
1718
1742
  if (settled) return;
1719
1743
  settled = true;
1720
1744
  if (timer.handle) clearTimeout(timer.handle);
1745
+ if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
1746
+ if (spawnListener) child.removeListener("spawn", spawnListener);
1721
1747
  resolve4(result);
1722
1748
  };
1723
1749
  try {
@@ -1743,6 +1769,25 @@ function runSynchroniser(args, opts) {
1743
1769
  child.once("close", (code) => {
1744
1770
  finish({ code, stdout, stderr, timedOut: false });
1745
1771
  });
1772
+ if (opts.signal) {
1773
+ const killChild = () => {
1774
+ if (child.pid === void 0) {
1775
+ if (!spawnListener) {
1776
+ spawnListener = killChild;
1777
+ child.once("spawn", spawnListener);
1778
+ }
1779
+ return;
1780
+ }
1781
+ child.kill("SIGKILL");
1782
+ };
1783
+ abortListener = killChild;
1784
+ if (opts.signal.aborted) {
1785
+ abortListener();
1786
+ } else {
1787
+ opts.signal.addEventListener("abort", abortListener, { once: true });
1788
+ if (opts.signal.aborted) abortListener();
1789
+ }
1790
+ }
1746
1791
  timer.handle = setTimeout(
1747
1792
  () => {
1748
1793
  child.kill("SIGKILL");
@@ -1780,6 +1825,8 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
1780
1825
  quarantined_bytes: null,
1781
1826
  verified_restore_point: null,
1782
1827
  restore_points_tried: null,
1828
+ provenance_reason: null,
1829
+ provenance_migration_delta: null,
1783
1830
  replication_suspended: stage === "restore"
1784
1831
  });
1785
1832
  }
@@ -2200,6 +2247,131 @@ async function restoreAndVerifySessionDb(options) {
2200
2247
  return { verifyFatal: await verifySessionDb(options, configPath, env) };
2201
2248
  }
2202
2249
 
2250
+ // src/lib/opencode/session-db-provenance.ts
2251
+ import { createRequire } from "module";
2252
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2253
+ import { dirname as dirname3, join as join3 } from "path";
2254
+ var require2 = createRequire(import.meta.url);
2255
+ function readSessionDbMigrationIds(dbPath) {
2256
+ let db;
2257
+ try {
2258
+ const { DatabaseSync } = require2("node:sqlite");
2259
+ db = new DatabaseSync(dbPath, { readOnly: true });
2260
+ const columns = db.prepare("PRAGMA table_info(migration)").all();
2261
+ const hasExpectedShape = columns.length === 2 && columns.some(
2262
+ (column) => column.name === "id" && typeof column.type === "string" && column.type.toUpperCase() === "TEXT" && column.pk === 1
2263
+ ) && columns.some(
2264
+ (column) => column.name === "time_completed" && typeof column.type === "string" && column.type.toUpperCase() === "INTEGER" && column.notnull === 1 && column.pk === 0
2265
+ );
2266
+ if (!hasExpectedShape) {
2267
+ console.warn(`[readSessionDbMigrationIds] unsupported migration table shape in ${dbPath}`);
2268
+ return null;
2269
+ }
2270
+ const rows = db.prepare("SELECT id FROM migration ORDER BY id").all();
2271
+ if (rows.some((row) => typeof row.id !== "string")) return null;
2272
+ return rows.map((row) => row.id);
2273
+ } catch (error2) {
2274
+ console.warn(
2275
+ `[readSessionDbMigrationIds] could not read migration history from ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
2276
+ );
2277
+ return null;
2278
+ } finally {
2279
+ try {
2280
+ db?.close();
2281
+ } catch (error2) {
2282
+ console.warn(
2283
+ `[readSessionDbMigrationIds] could not close ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
2284
+ );
2285
+ }
2286
+ }
2287
+ }
2288
+ function sessionDbProvenanceStatePath(homeDir, env) {
2289
+ const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
2290
+ return override || join3(homeDir, ".local", "state", "evident", "session-db-provenance.json");
2291
+ }
2292
+ function loadSessionDbProvenanceState(path) {
2293
+ let value;
2294
+ try {
2295
+ value = JSON.parse(readFileSync3(path, "utf8"));
2296
+ } catch (error2) {
2297
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
2298
+ console.error(
2299
+ `[session-db-provenance] could not read ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
2300
+ );
2301
+ return {};
2302
+ }
2303
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2304
+ console.error(`[session-db-provenance] ignored malformed state in ${path}`);
2305
+ return {};
2306
+ }
2307
+ const state = {};
2308
+ for (const [dbPath, record] of Object.entries(value)) {
2309
+ if (!isSessionDbProvenanceRecord(record)) {
2310
+ console.error(`[session-db-provenance] ignored malformed state in ${path}`);
2311
+ return {};
2312
+ }
2313
+ state[dbPath] = record;
2314
+ }
2315
+ return state;
2316
+ }
2317
+ function saveSessionDbProvenanceState(path, state) {
2318
+ try {
2319
+ mkdirSync2(dirname3(path), { recursive: true });
2320
+ writeFileSync2(path, `${JSON.stringify(state, null, 2)}
2321
+ `, "utf8");
2322
+ } catch (error2) {
2323
+ console.error(
2324
+ `[session-db-provenance] could not write ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
2325
+ );
2326
+ }
2327
+ }
2328
+ function evaluateSessionDbProvenance(input) {
2329
+ const { currentVersion, currentIds, previous } = input;
2330
+ if (!previous) return { anomaly: false, reason: null };
2331
+ const current = new Set(currentIds);
2332
+ const prior = new Set(previous.migrationIds);
2333
+ for (const id of prior) {
2334
+ if (!current.has(id)) return { anomaly: true, reason: "migration-history-regressed" };
2335
+ }
2336
+ if (current.size > prior.size && previous.opencodeVersion === currentVersion) {
2337
+ return { anomaly: true, reason: "foreign-version-migrations" };
2338
+ }
2339
+ return { anomaly: false, reason: null };
2340
+ }
2341
+ function checkSessionDbProvenance(input) {
2342
+ const { dbPath, currentVersion, homeDir, env } = input;
2343
+ const path = sessionDbProvenanceStatePath(homeDir, env);
2344
+ const state = loadSessionDbProvenanceState(path);
2345
+ const previous = state[dbPath];
2346
+ const currentIds = readSessionDbMigrationIds(dbPath);
2347
+ if (currentIds === null) {
2348
+ return {
2349
+ anomaly: false,
2350
+ reason: null,
2351
+ recordedVersion: previous?.opencodeVersion ?? null,
2352
+ migrationDelta: null
2353
+ };
2354
+ }
2355
+ const decision = evaluateSessionDbProvenance({ currentVersion, currentIds, previous });
2356
+ const migrationDelta = previous ? new Set(currentIds).size - new Set(previous.migrationIds).size : null;
2357
+ state[dbPath] = {
2358
+ opencodeVersion: currentVersion,
2359
+ migrationIds: currentIds,
2360
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2361
+ };
2362
+ saveSessionDbProvenanceState(path, state);
2363
+ return {
2364
+ ...decision,
2365
+ recordedVersion: previous?.opencodeVersion ?? null,
2366
+ migrationDelta
2367
+ };
2368
+ }
2369
+ function isSessionDbProvenanceRecord(value) {
2370
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2371
+ const record = value;
2372
+ return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
2373
+ }
2374
+
2203
2375
  // src/lib/opencode/opencode-version-gate.ts
2204
2376
  var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
2205
2377
  function isQueueValidatedVersion(version2) {
@@ -3286,10 +3458,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
3286
3458
 
3287
3459
  // src/lib/opencode/session-db-size.ts
3288
3460
  import { statSync as statSync3 } from "fs";
3289
- import { join as join3 } from "path";
3461
+ import { join as join4 } from "path";
3290
3462
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
3291
3463
  function statSessionDbBytes(homeDir) {
3292
- const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
3464
+ const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
3293
3465
  try {
3294
3466
  return statSync3(dbPath).size;
3295
3467
  } catch (err) {
@@ -3318,10 +3490,10 @@ function buildSessionStoreSizeWarning(input) {
3318
3490
 
3319
3491
  // src/lib/opencode/session-db-reclaim.ts
3320
3492
  import { statSync as statSync4, statfsSync } from "fs";
3321
- import { dirname as dirname3 } from "path";
3493
+ import { dirname as dirname4 } from "path";
3322
3494
  function insufficientSpaceReason(dbPath, requiredBytes) {
3323
3495
  try {
3324
- const fsStats = statfsSync(dirname3(dbPath));
3496
+ const fsStats = statfsSync(dirname4(dbPath));
3325
3497
  const availableBytes = fsStats.bavail * fsStats.bsize;
3326
3498
  if (availableBytes < requiredBytes) {
3327
3499
  return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
@@ -3703,8 +3875,8 @@ function connectTunnel(options) {
3703
3875
  try {
3704
3876
  message = JSON.parse(data.toString());
3705
3877
  } catch (error2) {
3706
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
3707
- onError?.(`Failed to handle message: ${errorMessage}`);
3878
+ const errorMessage2 = error2 instanceof Error ? error2.message : "Unknown error";
3879
+ onError?.(`Failed to handle message: ${errorMessage2}`);
3708
3880
  return;
3709
3881
  }
3710
3882
  if (isStreamFrame(message)) {
@@ -3847,10 +4019,10 @@ var RunnerConnection = class {
3847
4019
  };
3848
4020
 
3849
4021
  // src/lib/tunnel/ready-marker.ts
3850
- import { writeFileSync as writeFileSync2 } from "fs";
4022
+ import { writeFileSync as writeFileSync3 } from "fs";
3851
4023
  function writeTunnelReadyMarker(path, agentId) {
3852
4024
  try {
3853
- writeFileSync2(path, `${agentId}
4025
+ writeFileSync3(path, `${agentId}
3854
4026
  `);
3855
4027
  return { ok: true };
3856
4028
  } catch (error2) {
@@ -3875,7 +4047,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
3875
4047
  }
3876
4048
 
3877
4049
  // src/lib/process-liveness.ts
3878
- import { readFileSync as readFileSync3 } from "fs";
4050
+ import { readFileSync as readFileSync4 } from "fs";
3879
4051
  function isProcessAlive(pid) {
3880
4052
  try {
3881
4053
  process.kill(pid, 0);
@@ -3890,7 +4062,7 @@ function isProcessAlive(pid) {
3890
4062
  }
3891
4063
  if (process.platform !== "linux") return true;
3892
4064
  try {
3893
- const status2 = readFileSync3(`/proc/${pid}/status`, "utf8");
4065
+ const status2 = readFileSync4(`/proc/${pid}/status`, "utf8");
3894
4066
  return !/^State:\s+Z(?:\s|$)/m.test(status2);
3895
4067
  } catch (error2) {
3896
4068
  console.error(
@@ -3901,9 +4073,9 @@ function isProcessAlive(pid) {
3901
4073
  }
3902
4074
 
3903
4075
  // src/lib/openai-usage.ts
3904
- import { readFileSync as readFileSync4 } from "fs";
4076
+ import { readFileSync as readFileSync5 } from "fs";
3905
4077
  import { homedir as homedir3 } from "os";
3906
- import { join as join4 } from "path";
4078
+ import { join as join5 } from "path";
3907
4079
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3908
4080
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
3909
4081
  var OpenAiUsageError = class extends Error {
@@ -3917,7 +4089,7 @@ function isLocalCredentialProblem2(err) {
3917
4089
  }
3918
4090
  function readOpenCodeChatGptCredentials() {
3919
4091
  try {
3920
- const raw = readFileSync4(join4(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4092
+ const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3921
4093
  let parsed;
3922
4094
  try {
3923
4095
  parsed = JSON.parse(raw);
@@ -3939,6 +4111,23 @@ function readOpenCodeChatGptCredentials() {
3939
4111
  return null;
3940
4112
  }
3941
4113
  }
4114
+ function parseChatGptIdentity(accessToken) {
4115
+ const segments = accessToken.split(".");
4116
+ if (segments.length !== 3) return null;
4117
+ let payload;
4118
+ try {
4119
+ const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
4120
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
4121
+ payload = parsed;
4122
+ } catch {
4123
+ return null;
4124
+ }
4125
+ const profile = payload["https://api.openai.com/profile"];
4126
+ const auth = payload["https://api.openai.com/auth"];
4127
+ const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
4128
+ const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
4129
+ return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
4130
+ }
3942
4131
  function toWindow2(headers, name) {
3943
4132
  const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
3944
4133
  const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
@@ -4014,6 +4203,7 @@ async function getOpenAiUsage(port) {
4014
4203
  "credentials_expired"
4015
4204
  );
4016
4205
  }
4206
+ const subscription = parseChatGptIdentity(credentials2.accessToken);
4017
4207
  const models = await resolveProbeModels(port);
4018
4208
  if (models.length === 0) {
4019
4209
  throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
@@ -4046,7 +4236,7 @@ async function getOpenAiUsage(port) {
4046
4236
  "no_usable_window"
4047
4237
  );
4048
4238
  }
4049
- return usage;
4239
+ return { ...usage, subscription };
4050
4240
  }
4051
4241
  if (res.status === 401) {
4052
4242
  throw new OpenAiUsageError(
@@ -4293,12 +4483,12 @@ function createResourceUsageCollector(homeDir) {
4293
4483
  import { homedir as homedir4 } from "os";
4294
4484
 
4295
4485
  // src/lib/runner-file-sync.ts
4296
- import { join as join6 } from "path";
4486
+ import { join as join7 } from "path";
4297
4487
 
4298
4488
  // src/lib/file-push.ts
4299
4489
  import { randomUUID } from "crypto";
4300
4490
  import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
4301
- import { basename, dirname as dirname4, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
4491
+ import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
4302
4492
  var FILE_MODE = 384;
4303
4493
  var DIRECTORY_MODE = 448;
4304
4494
  async function writePushedFile(request) {
@@ -4329,9 +4519,9 @@ async function writePushedFile(request) {
4329
4519
  }
4330
4520
  try {
4331
4521
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
4332
- dirname4(candidate)
4522
+ dirname5(candidate)
4333
4523
  );
4334
- const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
4524
+ const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
4335
4525
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
4336
4526
  if (allowedDirectory === null) {
4337
4527
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -4341,8 +4531,8 @@ async function writePushedFile(request) {
4341
4531
  }
4342
4532
  if (missingSegments.length > 0) {
4343
4533
  await createMissingDirectories(existingAncestor, missingSegments);
4344
- const realParent = await realpath(dirname4(realTarget));
4345
- if (realParent !== dirname4(realTarget) || !contains(allowedDirectory, realTarget)) {
4534
+ const realParent = await realpath(dirname5(realTarget));
4535
+ if (realParent !== dirname5(realTarget) || !contains(allowedDirectory, realTarget)) {
4346
4536
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
4347
4537
  path: realTarget,
4348
4538
  bytes,
@@ -4367,7 +4557,7 @@ function expandAndValidate(requestedPath, homeDir) {
4367
4557
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
4368
4558
  return null;
4369
4559
  }
4370
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
4560
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
4371
4561
  if (expanded.split(/[/\\]/).includes("..")) {
4372
4562
  return null;
4373
4563
  }
@@ -4385,7 +4575,7 @@ async function resolveNearestExistingAncestor(directory) {
4385
4575
  try {
4386
4576
  return { existingAncestor: await realpath(current), missingSegments };
4387
4577
  } catch (err) {
4388
- const parent = dirname4(current);
4578
+ const parent = dirname5(current);
4389
4579
  if (err.code !== "ENOENT" || parent === current) {
4390
4580
  throw err;
4391
4581
  }
@@ -4440,13 +4630,13 @@ function contains(realDirectory, realTarget) {
4440
4630
  async function createMissingDirectories(existingAncestor, missingSegments) {
4441
4631
  let current = existingAncestor;
4442
4632
  for (const segment of missingSegments) {
4443
- current = join5(current, segment);
4633
+ current = join6(current, segment);
4444
4634
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
4445
4635
  await chmod(current, DIRECTORY_MODE);
4446
4636
  }
4447
4637
  }
4448
4638
  async function writeAtomically(realTarget, content) {
4449
- const temporaryPath = join5(dirname4(realTarget), `.evident-push-${randomUUID()}.tmp`);
4639
+ const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
4450
4640
  let handle;
4451
4641
  try {
4452
4642
  handle = await open2(temporaryPath, "wx", FILE_MODE);
@@ -4576,12 +4766,12 @@ var NOT_APPLIED = {
4576
4766
  opencodeAuthApplied: false
4577
4767
  };
4578
4768
  function isClaudeCredentialPath(requestedPath, homeDir) {
4579
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
4580
- return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4769
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4770
+ return expanded === join7(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4581
4771
  }
4582
4772
  function isOpenCodeAuthPath(requestedPath, homeDir) {
4583
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
4584
- return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
4773
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
4774
+ return expanded === join7(homeDir, ...OPENCODE_AUTH_SEGMENTS);
4585
4775
  }
4586
4776
  async function applyOne(options, file) {
4587
4777
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -5108,6 +5298,7 @@ var ChannelDriver = class _ChannelDriver {
5108
5298
  * and stops opencode.
5109
5299
  */
5110
5300
  stopped = false;
5301
+ recycleRequestedFlag = false;
5111
5302
  constructor(config) {
5112
5303
  this.agentId = config.agentId;
5113
5304
  this.port = config.port;
@@ -5213,6 +5404,9 @@ var ChannelDriver = class _ChannelDriver {
5213
5404
  let dispatched = 0;
5214
5405
  try {
5215
5406
  const conversations = await this.getPendingConversations();
5407
+ if (this.recycleRequestedFlag) {
5408
+ this.stop();
5409
+ }
5216
5410
  if (conversations.length > 0) {
5217
5411
  const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
5218
5412
  this.log({
@@ -5341,6 +5535,14 @@ var ChannelDriver = class _ChannelDriver {
5341
5535
  stop() {
5342
5536
  this.stopped = true;
5343
5537
  }
5538
+ /**
5539
+ * The server clears this request when a new MicroVM identity is recorded, so a
5540
+ * same-VM tunnel reconnect does not consume it. This is a plain read rather
5541
+ * than a consume; `run.ts` guards the action once-only.
5542
+ */
5543
+ get recycleRequested() {
5544
+ return this.recycleRequestedFlag;
5545
+ }
5344
5546
  /**
5345
5547
  * Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
5346
5548
  * graceful shutdown, so a turn whose reply is ready — or completes within the
@@ -5478,7 +5680,7 @@ var ChannelDriver = class _ChannelDriver {
5478
5680
  this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
5479
5681
  break;
5480
5682
  }
5481
- const errorMessage = err instanceof Error ? err.message : String(err);
5683
+ const errorMessage2 = err instanceof Error ? err.message : String(err);
5482
5684
  this.sessions.delete(conv.id);
5483
5685
  this.supersede(conv.id, sessionId);
5484
5686
  this.log({
@@ -5487,7 +5689,7 @@ var ChannelDriver = class _ChannelDriver {
5487
5689
  conversation_id: conv.id,
5488
5690
  message_id: message.id
5489
5691
  });
5490
- await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
5692
+ await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
5491
5693
  this.log({
5492
5694
  level: "warn",
5493
5695
  message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -5498,7 +5700,7 @@ var ChannelDriver = class _ChannelDriver {
5498
5700
  });
5499
5701
  this.log({
5500
5702
  level: "error",
5501
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage}`,
5703
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage2}`,
5502
5704
  conversation_id: conv.id,
5503
5705
  message_id: message.id
5504
5706
  });
@@ -5519,14 +5721,14 @@ var ChannelDriver = class _ChannelDriver {
5519
5721
  this.unconfirmedDispatchFailures.delete(message.id);
5520
5722
  this.sessions.delete(conv.id);
5521
5723
  this.supersede(conv.id, sessionId);
5522
- const errorMessage = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
5724
+ const errorMessage2 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
5523
5725
  this.log({
5524
5726
  level: "error",
5525
- message: errorMessage,
5727
+ message: errorMessage2,
5526
5728
  conversation_id: conv.id,
5527
5729
  message_id: message.id
5528
5730
  });
5529
- await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
5731
+ await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
5530
5732
  this.log({
5531
5733
  level: "warn",
5532
5734
  message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -7490,14 +7692,14 @@ var ChannelDriver = class _ChannelDriver {
7490
7692
  this.unconfirmedDispatchFailures.delete(row.id);
7491
7693
  this.sessions.delete(readoptConv.id);
7492
7694
  this.supersede(readoptConv.id, sessionId);
7493
- const errorMessage = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
7695
+ const errorMessage2 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
7494
7696
  this.log({
7495
7697
  level: "error",
7496
- message: errorMessage,
7698
+ message: errorMessage2,
7497
7699
  conversation_id: row.conversation_id,
7498
7700
  message_id: row.id
7499
7701
  });
7500
- await this.markFailed(row.conversation_id, row.id, null, errorMessage).catch((markErr) => {
7702
+ await this.markFailed(row.conversation_id, row.id, null, errorMessage2).catch((markErr) => {
7501
7703
  this.log({
7502
7704
  level: "warn",
7503
7705
  message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
@@ -8112,6 +8314,7 @@ var ChannelDriver = class _ChannelDriver {
8112
8314
  throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
8113
8315
  }
8114
8316
  const data = await res.json();
8317
+ this.recycleRequestedFlag = data.recycle_requested === true;
8115
8318
  let conversations = data.conversations;
8116
8319
  if (this.conversationFilter) {
8117
8320
  conversations = conversations.filter((c) => c.id === this.conversationFilter);
@@ -8629,7 +8832,7 @@ Port ${port} is already in use.`));
8629
8832
  }
8630
8833
 
8631
8834
  // src/lib/runner-credentials.ts
8632
- import { chmodSync as chmodSync2, writeFileSync as writeFileSync3 } from "fs";
8835
+ import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
8633
8836
  import { spawn as spawn5 } from "child_process";
8634
8837
  var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
8635
8838
  var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
@@ -8870,8 +9073,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
8870
9073
  }
8871
9074
  try {
8872
9075
  env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
8873
- writeFileSync3(GIT_CONFIG_GLOBAL, "");
8874
- writeFileSync3(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
9076
+ writeFileSync4(GIT_CONFIG_GLOBAL, "");
9077
+ writeFileSync4(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
8875
9078
  chmodSync2(GIT_CREDENTIAL_HELPER, 448);
8876
9079
  const config = [
8877
9080
  ["user.name", env.GIT_USER_NAME ?? "evident-bot"],
@@ -8904,7 +9107,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
8904
9107
  // src/lib/opencode/config-overlay.ts
8905
9108
  import { execFileSync as execFileSync2 } from "child_process";
8906
9109
  import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
8907
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve3 } from "path";
9110
+ import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
8908
9111
  function isFile(filePath) {
8909
9112
  return existsSync2(filePath) && statSync5(filePath).isFile();
8910
9113
  }
@@ -8918,7 +9121,7 @@ function applyRunnerOpenCodeConfig({
8918
9121
  return;
8919
9122
  }
8920
9123
  const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
8921
- const target = isFile(join7(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
9124
+ const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
8922
9125
  if (!isFile(source)) {
8923
9126
  log3(
8924
9127
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -8926,7 +9129,7 @@ function applyRunnerOpenCodeConfig({
8926
9129
  );
8927
9130
  return;
8928
9131
  }
8929
- copyFileSync(source, join7(cwd, target));
9132
+ copyFileSync(source, join8(cwd, target));
8930
9133
  try {
8931
9134
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
8932
9135
  stdio: "ignore"
@@ -8935,7 +9138,242 @@ function applyRunnerOpenCodeConfig({
8935
9138
  const detail = error2 instanceof Error ? error2.message : String(error2);
8936
9139
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
8937
9140
  }
8938
- log3(`Applied runner OpenCode config ${source} to ${join7(cwd, target)}`);
9141
+ log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
9142
+ }
9143
+
9144
+ // src/lib/credential-sync.ts
9145
+ import { renameSync, writeFileSync as writeFileSync5 } from "fs";
9146
+ var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
9147
+ var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
9148
+ var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
9149
+ var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
9150
+ var STORES = ["claude", "opencode"];
9151
+ var MAX_FLUSH_PASSES = 2;
9152
+ function outcomesWith(outcome) {
9153
+ return { claude: outcome, opencode: outcome };
9154
+ }
9155
+ function errorMessage(error2) {
9156
+ return error2 instanceof Error ? error2.message : String(error2);
9157
+ }
9158
+ function waitForSettlement(promise, timeoutMs) {
9159
+ return new Promise((resolve4) => {
9160
+ let settled = false;
9161
+ const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
9162
+ const finish = (value) => {
9163
+ if (settled) return;
9164
+ settled = true;
9165
+ clearTimeout(timer);
9166
+ resolve4(value);
9167
+ };
9168
+ promise.then(
9169
+ () => finish(true),
9170
+ () => finish(true)
9171
+ );
9172
+ });
9173
+ }
9174
+ function writeMarker(markerPath, outcomes, log3) {
9175
+ const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
9176
+ `;
9177
+ const temporaryPath = `${markerPath}.tmp`;
9178
+ try {
9179
+ writeFileSync5(temporaryPath, body, { mode: 384 });
9180
+ renameSync(temporaryPath, markerPath);
9181
+ } catch (error2) {
9182
+ log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage(error2)}`, "warn");
9183
+ }
9184
+ }
9185
+ function intervalSeconds(env, log3) {
9186
+ const raw = env.CREDS_SYNC_INTERVAL;
9187
+ if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
9188
+ return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
9189
+ }
9190
+ log3(
9191
+ `CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
9192
+ "warn"
9193
+ );
9194
+ return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
9195
+ }
9196
+ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
9197
+ const remainingMs = deadlineAt - Date.now();
9198
+ if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
9199
+ const controller = new AbortController();
9200
+ let result;
9201
+ let failed = false;
9202
+ const completion = Promise.resolve().then(
9203
+ () => synchroniserRunner(["sync-once", store], {
9204
+ timeoutMs: remainingMs,
9205
+ env,
9206
+ signal: controller.signal
9207
+ })
9208
+ ).then(
9209
+ (value) => {
9210
+ result = value;
9211
+ },
9212
+ (error2) => {
9213
+ failed = true;
9214
+ log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage(error2)}`, "warn");
9215
+ }
9216
+ );
9217
+ const abortTimer = setTimeout(() => controller.abort(), remainingMs);
9218
+ const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
9219
+ clearTimeout(abortTimer);
9220
+ if (!settledBeforeDeadline) {
9221
+ controller.abort();
9222
+ const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
9223
+ if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
9224
+ return { outcome: "timeout", orphaned: false };
9225
+ }
9226
+ if (failed || !result) return { outcome: "failed", orphaned: false };
9227
+ if (result.timedOut || Date.now() >= deadlineAt) {
9228
+ return { outcome: "timeout", orphaned: false };
9229
+ }
9230
+ return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
9231
+ }
9232
+ function createCredentialSync({
9233
+ markerPath,
9234
+ env,
9235
+ log: log3,
9236
+ synchroniserRunner = runSynchroniser
9237
+ }) {
9238
+ const persistenceDisabled = !env.PERSISTENCE_BUCKET;
9239
+ let disabled = persistenceDisabled;
9240
+ let armed = false;
9241
+ let stopped = false;
9242
+ let timer;
9243
+ let inFlight;
9244
+ let activeTickAbort;
9245
+ let lastTickFailed;
9246
+ let flushPromise;
9247
+ const scheduleTick = (intervalMs, startTick2) => {
9248
+ if (stopped) return;
9249
+ timer = setTimeout(() => {
9250
+ timer = void 0;
9251
+ startTick2();
9252
+ }, intervalMs);
9253
+ };
9254
+ const startTick = (intervalMs) => {
9255
+ if (stopped) return;
9256
+ const controller = new AbortController();
9257
+ activeTickAbort = controller;
9258
+ const tick = (async () => {
9259
+ const outcomes = {
9260
+ claude: "failed",
9261
+ opencode: "failed"
9262
+ };
9263
+ for (const store of STORES) {
9264
+ if (controller.signal.aborted) break;
9265
+ try {
9266
+ const result = await synchroniserRunner(["sync-once", store], {
9267
+ timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
9268
+ env,
9269
+ signal: controller.signal
9270
+ });
9271
+ outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
9272
+ } catch (error2) {
9273
+ outcomes[store] = "failed";
9274
+ log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage(error2)}`, "debug");
9275
+ }
9276
+ }
9277
+ const failed = STORES.some((store) => outcomes[store] === "failed");
9278
+ log3(
9279
+ `CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
9280
+ "debug"
9281
+ );
9282
+ if (failed && lastTickFailed !== true) {
9283
+ log3(
9284
+ "CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
9285
+ "warn"
9286
+ );
9287
+ } else if (!failed && lastTickFailed === true) {
9288
+ log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
9289
+ }
9290
+ lastTickFailed = failed;
9291
+ })().finally(() => {
9292
+ if (activeTickAbort === controller) activeTickAbort = void 0;
9293
+ if (inFlight === tick) inFlight = void 0;
9294
+ scheduleTick(intervalMs, () => startTick(intervalMs));
9295
+ });
9296
+ inFlight = tick;
9297
+ };
9298
+ const performFlush = async () => {
9299
+ stopped = true;
9300
+ if (timer) {
9301
+ clearTimeout(timer);
9302
+ timer = void 0;
9303
+ }
9304
+ const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
9305
+ if (inFlight) {
9306
+ const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
9307
+ if (!settled) {
9308
+ activeTickAbort?.abort();
9309
+ const settledAfterAbort = await waitForSettlement(
9310
+ inFlight,
9311
+ CREDENTIAL_FLUSH_ABORT_GRACE_MS
9312
+ );
9313
+ if (!settledAfterAbort) {
9314
+ log3(
9315
+ "CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
9316
+ "warn"
9317
+ );
9318
+ return { outcomes: outcomesWith("timeout"), orphaned: true };
9319
+ }
9320
+ }
9321
+ }
9322
+ if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
9323
+ const outcomes = outcomesWith("timeout");
9324
+ for (const store of STORES) {
9325
+ const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
9326
+ if (result.orphaned) {
9327
+ log3(
9328
+ "CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
9329
+ "warn"
9330
+ );
9331
+ return { outcomes: outcomesWith("timeout"), orphaned: true };
9332
+ }
9333
+ outcomes[store] = result.outcome;
9334
+ }
9335
+ return { outcomes, orphaned: false };
9336
+ };
9337
+ let flushPasses = 0;
9338
+ let lastFlush;
9339
+ return {
9340
+ arm() {
9341
+ if (stopped || armed) return;
9342
+ armed = true;
9343
+ if (persistenceDisabled) {
9344
+ disabled = true;
9345
+ log3(
9346
+ "CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
9347
+ "warn"
9348
+ );
9349
+ return;
9350
+ }
9351
+ disabled = false;
9352
+ const intervalMs = intervalSeconds(env, log3) * 1e3;
9353
+ scheduleTick(intervalMs, () => startTick(intervalMs));
9354
+ },
9355
+ async stopAndFlush(publish) {
9356
+ let result;
9357
+ const runningFlush = flushPromise;
9358
+ if (runningFlush) {
9359
+ result = await runningFlush;
9360
+ } else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
9361
+ result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
9362
+ } else {
9363
+ flushPasses++;
9364
+ const currentFlush = performFlush();
9365
+ flushPromise = currentFlush;
9366
+ try {
9367
+ result = await currentFlush;
9368
+ lastFlush = result;
9369
+ } finally {
9370
+ if (flushPromise === currentFlush) flushPromise = void 0;
9371
+ }
9372
+ }
9373
+ if (publish) writeMarker(markerPath, result.outcomes, log3);
9374
+ return result.outcomes;
9375
+ }
9376
+ };
8939
9377
  }
8940
9378
 
8941
9379
  // src/commands/run.ts
@@ -8975,7 +9413,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
8975
9413
  if (trimmed === "") {
8976
9414
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
8977
9415
  }
8978
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join8(homeDir, trimmed.slice(2)) : trimmed;
9416
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join9(homeDir, trimmed.slice(2)) : trimmed;
8979
9417
  if (!isAbsolute3(expanded)) {
8980
9418
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
8981
9419
  }
@@ -9235,6 +9673,10 @@ async function driveChannels(state, driver) {
9235
9673
  consecutiveDrainFailures = 0;
9236
9674
  unreachableMs = 0;
9237
9675
  state.messageCount += processed;
9676
+ if (driver.recycleRequested) {
9677
+ await beginGracefulShutdown(state, "recycle");
9678
+ return;
9679
+ }
9238
9680
  const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
9239
9681
  lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
9240
9682
  const fileActivitySnapshot = driver.fileSyncActivity();
@@ -9277,8 +9719,8 @@ async function driveChannels(state, driver) {
9277
9719
  state.running = false;
9278
9720
  break;
9279
9721
  }
9280
- const errorMessage = error2 instanceof Error ? error2.message : String(error2);
9281
- logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage}` });
9722
+ const errorMessage2 = error2 instanceof Error ? error2.message : String(error2);
9723
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage2}` });
9282
9724
  if (state.interactive) displayStatus(state);
9283
9725
  if (driver.hasInFlightWatchers()) {
9284
9726
  consecutiveDrainFailures = 0;
@@ -9318,7 +9760,43 @@ async function driveChannels(state, driver) {
9318
9760
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
9319
9761
  var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
9320
9762
  function sessionDbPath() {
9321
- return join8(homedir5(), ".local", "share", "opencode", "opencode.db");
9763
+ return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
9764
+ }
9765
+ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
9766
+ const record = {
9767
+ v: 1,
9768
+ event: "session_db_recovery",
9769
+ at: (/* @__PURE__ */ new Date()).toISOString(),
9770
+ stage: "verify",
9771
+ outcome: "schema_provenance_mismatch",
9772
+ severity: "error",
9773
+ reason: provenance.reason ?? "schema-provenance-mismatch",
9774
+ litestream_exit_code: null,
9775
+ attempt: null,
9776
+ replica_objects: null,
9777
+ replica_bytes: null,
9778
+ quarantine_destination: null,
9779
+ quarantined_objects: null,
9780
+ quarantine_failed_objects: null,
9781
+ quarantined_bytes: null,
9782
+ verified_restore_point: null,
9783
+ restore_points_tried: null,
9784
+ provenance_reason: provenance.reason,
9785
+ provenance_migration_delta: provenance.migrationDelta,
9786
+ replication_suspended: false,
9787
+ dbPath: sessionDbPath(),
9788
+ recorded_version: provenance.recordedVersion,
9789
+ current_version: currentVersion,
9790
+ provenance_pre_boot_migration_count: preBootMigrationCount
9791
+ };
9792
+ const activity = buildSessionDbRecoveryActivity(record);
9793
+ if (!activity) throw new Error("could not map session-DB provenance activity");
9794
+ logActivity(state, {
9795
+ type: activity.level === "error" ? "error" : "info",
9796
+ level: activity.level,
9797
+ ...activity.level === "error" ? { error: activity.message } : { message: activity.message },
9798
+ metadata: activity.metadata
9799
+ });
9322
9800
  }
9323
9801
  async function runSweep(state, driver, config) {
9324
9802
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
@@ -9365,7 +9843,7 @@ async function runSweep(state, driver, config) {
9365
9843
  const reclaimResult = await reclaimSessionDbSpace({
9366
9844
  dbPath: sessionDbPath(),
9367
9845
  maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
9368
- allowFullVacuum: protectedNow.size === 0
9846
+ allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
9369
9847
  });
9370
9848
  if (reclaimResult.ok) {
9371
9849
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
@@ -9712,21 +10190,39 @@ async function cleanup(state, opts = {}) {
9712
10190
  clearTimeout(state.resourceUsageTimer);
9713
10191
  state.resourceUsageTimer = null;
9714
10192
  }
10193
+ const credentialSync = state.credentialSync;
10194
+ const flushCredentials = credentialSync ? async (phase, publish) => {
10195
+ await timeShutdownPhase(state, durations, phase, async () => {
10196
+ const outcomes = await credentialSync.stopAndFlush(publish);
10197
+ const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
10198
+ log2(
10199
+ state,
10200
+ `Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
10201
+ level
10202
+ );
10203
+ });
10204
+ } : void 0;
10205
+ let drainSettled = true;
9715
10206
  if (opts.graceful && state.channelDriver) {
9716
10207
  state.channelDriver.stop();
10208
+ }
10209
+ if (flushCredentials) {
10210
+ await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
10211
+ }
10212
+ if (opts.graceful && state.channelDriver) {
9717
10213
  log2(state, "Draining in-flight channel work before shutdown...");
9718
10214
  if (state.interactive) {
9719
10215
  logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
9720
10216
  displayStatus(state);
9721
10217
  }
9722
10218
  const driver = state.channelDriver;
9723
- const settled = await timeShutdownPhase(
10219
+ drainSettled = await timeShutdownPhase(
9724
10220
  state,
9725
10221
  durations,
9726
10222
  "drain",
9727
10223
  () => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
9728
10224
  );
9729
- if (!settled) {
10225
+ if (!drainSettled) {
9730
10226
  logActivity(state, {
9731
10227
  type: "info",
9732
10228
  message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
@@ -9734,6 +10230,9 @@ async function cleanup(state, opts = {}) {
9734
10230
  if (state.interactive) displayStatus(state);
9735
10231
  }
9736
10232
  }
10233
+ if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
10234
+ await flushCredentials("credential_flush_final", true);
10235
+ }
9737
10236
  await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
9738
10237
  if (state.connection) {
9739
10238
  const connection = state.connection;
@@ -9769,6 +10268,44 @@ async function cleanup(state, opts = {}) {
9769
10268
  }
9770
10269
  return durations;
9771
10270
  }
10271
+ async function beginGracefulShutdown(state, trigger) {
10272
+ if (state.shuttingDown) return;
10273
+ state.shuttingDown = true;
10274
+ const shutdownStartedAt = Date.now();
10275
+ const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
10276
+ if (state.interactive) {
10277
+ logActivity(state, { type: "info", message: shutdownMessage });
10278
+ displayStatus(state);
10279
+ } else {
10280
+ log2(state, shutdownMessage);
10281
+ }
10282
+ const durations = await cleanup(state, { graceful: true });
10283
+ const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
10284
+ await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
10285
+ let timer;
10286
+ const flushed = shutdownTelemetry().then(
10287
+ () => true,
10288
+ (error2) => {
10289
+ log2(
10290
+ state,
10291
+ `Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
10292
+ "warn"
10293
+ );
10294
+ return true;
10295
+ }
10296
+ );
10297
+ const timedOut = new Promise((resolve4) => {
10298
+ timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
10299
+ });
10300
+ if (!await Promise.race([flushed, timedOut])) {
10301
+ log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
10302
+ }
10303
+ clearTimeout(timer);
10304
+ });
10305
+ const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
10306
+ log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
10307
+ process.exit(0);
10308
+ }
9772
10309
  async function run(options) {
9773
10310
  const interactive = isInteractive(options.json);
9774
10311
  let logLevel;
@@ -9804,6 +10341,7 @@ async function run(options) {
9804
10341
  connected: false,
9805
10342
  opencodeConnected: false,
9806
10343
  opencodeVersion: null,
10344
+ sessionDbProvenanceAnomaly: false,
9807
10345
  opencodeProcess: null,
9808
10346
  litestreamProcess: null,
9809
10347
  connection: null,
@@ -9819,9 +10357,23 @@ async function run(options) {
9819
10357
  openaiUsageTimer: null,
9820
10358
  openaiUsageRearm: null,
9821
10359
  resourceUsageTimer: null,
10360
+ credentialSync: null,
9822
10361
  authHeader: ""
9823
10362
  };
9824
10363
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
10364
+ if (options.credentialSyncMarker) {
10365
+ state.credentialSync = createCredentialSync({
10366
+ markerPath: options.credentialSyncMarker,
10367
+ env: process.env,
10368
+ log: (message, level = "info") => {
10369
+ if (level === "error") {
10370
+ logActivity(state, { type: "error", error: message });
10371
+ } else {
10372
+ logActivity(state, { type: "info", level, message });
10373
+ }
10374
+ }
10375
+ });
10376
+ }
9825
10377
  if (fileSyncDirectories.length > 0) {
9826
10378
  log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
9827
10379
  } else {
@@ -9847,43 +10399,7 @@ async function run(options) {
9847
10399
  "warn"
9848
10400
  );
9849
10401
  }
9850
- const handleSignal = async () => {
9851
- if (state.shuttingDown) return;
9852
- state.shuttingDown = true;
9853
- const shutdownStartedAt = Date.now();
9854
- if (state.interactive) {
9855
- logActivity(state, { type: "info", message: "Shutting down..." });
9856
- displayStatus(state);
9857
- } else {
9858
- log2(state, "Shutting down...");
9859
- }
9860
- const durations = await cleanup(state, { graceful: true });
9861
- const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
9862
- await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
9863
- let timer;
9864
- const flushed = shutdownTelemetry().then(
9865
- () => true,
9866
- (error2) => {
9867
- log2(
9868
- state,
9869
- `Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
9870
- "warn"
9871
- );
9872
- return true;
9873
- }
9874
- );
9875
- const timedOut = new Promise((resolve4) => {
9876
- timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
9877
- });
9878
- if (!await Promise.race([flushed, timedOut])) {
9879
- log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
9880
- }
9881
- clearTimeout(timer);
9882
- });
9883
- const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
9884
- log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
9885
- process.exit(0);
9886
- };
10402
+ const handleSignal = () => beginGracefulShutdown(state, "signal");
9887
10403
  process.on("SIGINT", handleSignal);
9888
10404
  process.on("SIGTERM", handleSignal);
9889
10405
  try {
@@ -10029,6 +10545,7 @@ async function run(options) {
10029
10545
  await restoreCredentialStores(credentialContext);
10030
10546
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
10031
10547
  }
10548
+ state.credentialSync?.arm();
10032
10549
  let sessionDbVerifyFatal = false;
10033
10550
  if (!options.restoreSessionDb) {
10034
10551
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -10082,6 +10599,7 @@ async function run(options) {
10082
10599
  for (const warning2 of maxActiveSessionsWarnings) {
10083
10600
  logActivity(state, { type: "info", level: "warn", message: warning2 });
10084
10601
  }
10602
+ const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
10085
10603
  const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
10086
10604
  try {
10087
10605
  const oc = await ensureOpenCodeRunning({
@@ -10097,7 +10615,7 @@ async function run(options) {
10097
10615
  state.opencodeVersion = oc.version;
10098
10616
  if (options.opencodePidFile && oc.process?.pid !== void 0) {
10099
10617
  try {
10100
- writeFileSync4(options.opencodePidFile, `${oc.process.pid}
10618
+ writeFileSync6(options.opencodePidFile, `${oc.process.pid}
10101
10619
  `, { mode: 384 });
10102
10620
  chmodSync3(options.opencodePidFile, 384);
10103
10621
  } catch (error2) {
@@ -10107,6 +10625,23 @@ async function run(options) {
10107
10625
  });
10108
10626
  }
10109
10627
  }
10628
+ if (state.opencodeVersion !== null) {
10629
+ const provenance = checkSessionDbProvenance({
10630
+ dbPath: sessionDbPath(),
10631
+ currentVersion: state.opencodeVersion,
10632
+ homeDir: homedir5(),
10633
+ env: process.env
10634
+ });
10635
+ if (provenance.anomaly) {
10636
+ state.sessionDbProvenanceAnomaly = true;
10637
+ logSessionDbProvenanceMismatch(
10638
+ state,
10639
+ provenance,
10640
+ state.opencodeVersion,
10641
+ preBootMigrationIds?.length ?? null
10642
+ );
10643
+ }
10644
+ }
10110
10645
  state.opencodeConnected = oc.notReadyReason === null;
10111
10646
  const version2 = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
10112
10647
  ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
@@ -10159,7 +10694,7 @@ async function run(options) {
10159
10694
  let existingPid;
10160
10695
  if (existsSync3(options.litestreamPidFile)) {
10161
10696
  try {
10162
- const rawPid = readFileSync5(options.litestreamPidFile, "utf8").trim();
10697
+ const rawPid = readFileSync6(options.litestreamPidFile, "utf8").trim();
10163
10698
  const parsedPid = Number(rawPid);
10164
10699
  if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
10165
10700
  existingPid = parsedPid;
@@ -10196,7 +10731,7 @@ async function run(options) {
10196
10731
  });
10197
10732
  try {
10198
10733
  if (litestreamProcess.pid !== void 0) {
10199
- writeFileSync4(options.litestreamPidFile, `${litestreamProcess.pid}
10734
+ writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
10200
10735
  `, {
10201
10736
  mode: 384
10202
10737
  });
@@ -10402,7 +10937,17 @@ async function run(options) {
10402
10937
  setTimer: (timer) => {
10403
10938
  state.openaiUsageTimer = timer;
10404
10939
  },
10405
- fetchUsage: () => getOpenAiUsage(state.port),
10940
+ fetchUsage: async () => {
10941
+ const usage = await getOpenAiUsage(state.port);
10942
+ if (usage.subscription === null) {
10943
+ logActivity(state, {
10944
+ type: "info",
10945
+ level: "debug",
10946
+ message: "OpenAI usage subscription could not be identified from the local credential"
10947
+ });
10948
+ }
10949
+ return usage;
10950
+ },
10406
10951
  report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
10407
10952
  isLocalCredentialProblem: isLocalCredentialProblem2,
10408
10953
  forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
@@ -10448,7 +10993,7 @@ async function run(options) {
10448
10993
  }
10449
10994
 
10450
10995
  // src/index.ts
10451
- var { version } = createRequire(import.meta.url)("../package.json");
10996
+ var { version } = createRequire2(import.meta.url)("../package.json");
10452
10997
  var program = new Command();
10453
10998
  program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
10454
10999
  "--endpoint <url>",
@@ -10526,6 +11071,9 @@ program.command("run").description("Connect to Evident and process messages").op
10526
11071
  ).option(
10527
11072
  "--opencode-config-overlay <path>",
10528
11073
  "Apply this runner-provided OpenCode config before starting OpenCode."
11074
+ ).option(
11075
+ "--credential-sync-marker <path>",
11076
+ "Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
10529
11077
  ).action(
10530
11078
  (options) => {
10531
11079
  run({
@@ -10564,7 +11112,8 @@ program.command("run").description("Connect to Evident and process messages").op
10564
11112
  sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
10565
11113
  restoreSessionDb: options.restoreSessionDb,
10566
11114
  restoreRunnerCredentials: options.restoreRunnerCredentials,
10567
- opencodeConfigOverlay: options.opencodeConfigOverlay
11115
+ opencodeConfigOverlay: options.opencodeConfigOverlay,
11116
+ credentialSyncMarker: options.credentialSyncMarker
10568
11117
  });
10569
11118
  }
10570
11119
  );