@evident-ai/cli 3.4.1-dev.aa71e9a → 3.4.1-dev.b45408b

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/README.md CHANGED
@@ -98,6 +98,10 @@ Options:
98
98
  healthy when the runner starts it itself (default: `180`). On expiry the runner
99
99
  warns and comes online anyway rather than failing. Env:
100
100
  `EVIDENT_OPENCODE_START_TIMEOUT` (seconds).
101
+ - `--litestream-config <path>` — Start `litestream replicate` for the OpenCode
102
+ session database with this config file. Use this for runner images that persist
103
+ the session database; the file is produced by `runner-synchroniser litestream-config`.
104
+ Omit it to disable replication.
101
105
  - `--claude-usage-reporting <mode>` — Whether to report the local Claude Code
102
106
  subscription's rate-limit usage to Evident, so it shows on the runner page:
103
107
  `auto` (default) reports it when a usable Claude Code login is found on this
package/dist/index.js CHANGED
@@ -722,6 +722,14 @@ function toReportedWindow(window) {
722
722
  if (!window) return null;
723
723
  return { utilization: window.utilization, resets_at: window.resetsAt };
724
724
  }
725
+ function toReportedOwner(snapshot) {
726
+ if (!snapshot.owner) return null;
727
+ return {
728
+ email: snapshot.owner.email,
729
+ organization_name: snapshot.owner.organizationName,
730
+ rate_limit_tier: snapshot.owner.rateLimitTier
731
+ };
732
+ }
725
733
  async function reportClaudeUsage(agentId, authHeader, snapshot) {
726
734
  try {
727
735
  const apiUrl = getApiUrlConfig();
@@ -730,7 +738,8 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
730
738
  headers: { Authorization: authHeader, "Content-Type": "application/json" },
731
739
  body: JSON.stringify({
732
740
  five_hour: toReportedWindow(snapshot.fiveHour),
733
- seven_day: toReportedWindow(snapshot.sevenDay)
741
+ seven_day: toReportedWindow(snapshot.sevenDay),
742
+ owner: toReportedOwner(snapshot)
734
743
  }),
735
744
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
736
745
  });
@@ -996,7 +1005,10 @@ import { readFileSync } from "fs";
996
1005
  import { homedir } from "os";
997
1006
  import { join } from "path";
998
1007
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
1008
+ var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
1009
+ var CLAUDE_PROFILE_TIMEOUT_MS = 2e3;
999
1010
  var KEYCHAIN_SERVICE = "Claude Code-credentials";
1011
+ var cachedOwner = null;
1000
1012
  var CLAUDE_CREDENTIALS_SEGMENTS = [".claude", ".credentials.json"];
1001
1013
  function parseClaudeCliCredentials(raw) {
1002
1014
  let parsed;
@@ -1070,6 +1082,47 @@ function toWindow(value) {
1070
1082
  }
1071
1083
  return { utilization: window.utilization, resetsAt };
1072
1084
  }
1085
+ function ownerLookupFailure(error2) {
1086
+ const name = error2?.name;
1087
+ return name === "TimeoutError" || name === "AbortError" ? "timed out" : "request failed";
1088
+ }
1089
+ async function getClaudeUsageOwner(accessToken) {
1090
+ if (cachedOwner?.accessToken === accessToken) {
1091
+ return { owner: cachedOwner.owner, ownerLookupError: null };
1092
+ }
1093
+ try {
1094
+ const response = await fetch(CLAUDE_PROFILE_URL, {
1095
+ headers: {
1096
+ Authorization: `Bearer ${accessToken}`,
1097
+ "Content-Type": "application/json",
1098
+ "anthropic-version": "2023-06-01"
1099
+ },
1100
+ signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1101
+ });
1102
+ if (!response.ok) {
1103
+ return { owner: null, ownerLookupError: `HTTP ${response.status}` };
1104
+ }
1105
+ let body;
1106
+ try {
1107
+ body = await response.json();
1108
+ } catch (error2) {
1109
+ return { owner: null, ownerLookupError: "malformed response" };
1110
+ }
1111
+ const profile = body;
1112
+ if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
1113
+ return { owner: null, ownerLookupError: "malformed response" };
1114
+ }
1115
+ const owner = {
1116
+ email: profile.account.email,
1117
+ organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
1118
+ rateLimitTier: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
1119
+ };
1120
+ cachedOwner = { accessToken, owner };
1121
+ return { owner, ownerLookupError: null };
1122
+ } catch (error2) {
1123
+ return { owner: null, ownerLookupError: ownerLookupFailure(error2) };
1124
+ }
1125
+ }
1073
1126
  async function getClaudeUsage() {
1074
1127
  const credentials2 = readClaudeCliCredentials();
1075
1128
  if (!credentials2) {
@@ -1089,15 +1142,19 @@ async function getClaudeUsage() {
1089
1142
  Authorization: `Bearer ${credentials2.accessToken}`,
1090
1143
  "Content-Type": "application/json",
1091
1144
  "anthropic-version": "2023-06-01"
1092
- }
1145
+ },
1146
+ signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1093
1147
  });
1094
1148
  if (!res.ok) {
1095
1149
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1096
1150
  }
1097
1151
  const body = await res.json();
1152
+ const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1098
1153
  return {
1099
1154
  fiveHour: toWindow(body.five_hour),
1100
- sevenDay: toWindow(body.seven_day)
1155
+ sevenDay: toWindow(body.seven_day),
1156
+ owner,
1157
+ ownerLookupError
1101
1158
  };
1102
1159
  }
1103
1160
 
@@ -1537,6 +1594,12 @@ function buildSessionDbRecoveryActivity(record) {
1537
1594
  metadata: withoutContractFields(record),
1538
1595
  message: "This runner started with a fresh session database because it could not verify where its session history is backed up. Nothing in the backup was changed; check the runner backup configuration and permissions."
1539
1596
  };
1597
+ case "session_db_boot_refused":
1598
+ return {
1599
+ level,
1600
+ metadata: withoutContractFields(record),
1601
+ 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.`
1602
+ };
1540
1603
  default:
1541
1604
  return null;
1542
1605
  }
@@ -1550,7 +1613,8 @@ var OUTCOMES = /* @__PURE__ */ new Set([
1550
1613
  "restore_retried",
1551
1614
  "fresh_session_db",
1552
1615
  "history_rolled_back",
1553
- "restore_misconfigured"
1616
+ "restore_misconfigured",
1617
+ "session_db_boot_refused"
1554
1618
  ]);
1555
1619
  var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
1556
1620
  var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
@@ -1617,6 +1681,62 @@ function buildOpenCodeVersionWarning(version2) {
1617
1681
 
1618
1682
  // src/lib/opencode/process.ts
1619
1683
  import { execSync, spawn } from "child_process";
1684
+
1685
+ // src/lib/process-stop.ts
1686
+ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
1687
+ if (!child.pid) {
1688
+ return { outcome: "not-running", code: child.exitCode, signal: child.signalCode };
1689
+ }
1690
+ if (child.exitCode !== null || child.signalCode !== null) {
1691
+ return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
1692
+ }
1693
+ return new Promise((resolve3, reject) => {
1694
+ let forced = false;
1695
+ let settled = false;
1696
+ const timer = setTimeout(() => {
1697
+ forced = true;
1698
+ try {
1699
+ sendKill();
1700
+ } catch (error2) {
1701
+ if (error2.code === "ESRCH") {
1702
+ finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
1703
+ } else {
1704
+ fail(error2);
1705
+ }
1706
+ }
1707
+ }, timeoutMs);
1708
+ const finish = (result) => {
1709
+ if (settled) return;
1710
+ settled = true;
1711
+ clearTimeout(timer);
1712
+ child.removeListener("exit", onExit);
1713
+ resolve3(result);
1714
+ };
1715
+ const fail = (error2) => {
1716
+ if (settled) return;
1717
+ settled = true;
1718
+ clearTimeout(timer);
1719
+ child.removeListener("exit", onExit);
1720
+ reject(error2);
1721
+ };
1722
+ const onExit = (code, signal) => {
1723
+ finish({ outcome: forced ? "killed" : "exited", code, signal });
1724
+ };
1725
+ child.once("exit", onExit);
1726
+ try {
1727
+ sendTerm();
1728
+ } catch (error2) {
1729
+ if (error2.code === "ESRCH") {
1730
+ finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
1731
+ } else {
1732
+ fail(error2);
1733
+ }
1734
+ return;
1735
+ }
1736
+ });
1737
+ }
1738
+
1739
+ // src/lib/opencode/process.ts
1620
1740
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
1621
1741
  function getProcessCwd(pid) {
1622
1742
  const platform = process.platform;
@@ -1788,23 +1908,20 @@ async function startOpenCode(port) {
1788
1908
  });
1789
1909
  return child;
1790
1910
  }
1791
- function stopOpenCode(opencodeProcess) {
1792
- if (!opencodeProcess || !opencodeProcess.pid) {
1793
- return;
1794
- }
1795
- try {
1911
+ function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
1912
+ const sendSignal = (signal) => {
1796
1913
  if (process.platform === "win32") {
1797
- opencodeProcess.kill("SIGTERM");
1914
+ opencodeProcess.kill(signal);
1798
1915
  } else {
1799
- process.kill(-opencodeProcess.pid, "SIGTERM");
1800
- }
1801
- } catch (err) {
1802
- if (err.code !== "ESRCH") {
1803
- console.warn(
1804
- `stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`
1805
- );
1916
+ process.kill(-opencodeProcess.pid, signal);
1806
1917
  }
1807
- }
1918
+ };
1919
+ return stopProcessAndWait(
1920
+ opencodeProcess,
1921
+ timeoutMs,
1922
+ () => sendSignal("SIGTERM"),
1923
+ () => sendSignal("SIGKILL")
1924
+ );
1808
1925
  }
1809
1926
 
1810
1927
  // src/lib/opencode/install.ts
@@ -2387,7 +2504,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
2387
2504
  return completedOf(reply) != null && finishOf(reply) === "tool-calls";
2388
2505
  }
2389
2506
  function isB2AbandonmentConfirmed(params) {
2390
- return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
2507
+ return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
2391
2508
  }
2392
2509
  function isAmbiguousTerminalFinish(m) {
2393
2510
  if (completedOf(m) == null) return false;
@@ -2400,7 +2517,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
2400
2517
  return isAmbiguousTerminalFinish(reply);
2401
2518
  }
2402
2519
  function isAmbiguousFinishResolved(params) {
2403
- return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
2520
+ return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
2404
2521
  }
2405
2522
  function messageError(messages, userMessageId) {
2406
2523
  const reply = findLastAssistantReplyFor(messages, userMessageId);
@@ -3182,6 +3299,22 @@ function writeTunnelReadyMarker(path, agentId) {
3182
3299
  }
3183
3300
  }
3184
3301
 
3302
+ // src/lib/replication.ts
3303
+ import { spawn as spawn2 } from "child_process";
3304
+ function startSessionDbReplication(configPath) {
3305
+ return spawn2("litestream", ["replicate", "-config", configPath], {
3306
+ stdio: "inherit"
3307
+ });
3308
+ }
3309
+ async function stopSessionDbReplication(child, timeoutMs) {
3310
+ return stopProcessAndWait(
3311
+ child,
3312
+ timeoutMs,
3313
+ () => child.kill("SIGTERM"),
3314
+ () => child.kill("SIGKILL")
3315
+ );
3316
+ }
3317
+
3185
3318
  // src/lib/openai-usage.ts
3186
3319
  import { readFileSync as readFileSync3 } from "fs";
3187
3320
  import { homedir as homedir2 } from "os";
@@ -5676,6 +5809,7 @@ var ChannelDriver = class _ChannelDriver {
5676
5809
  deliveryDeadlineAnchored: false,
5677
5810
  b2PinnedSinceMs: 0,
5678
5811
  b2LastDescendantCheckMs: 0,
5812
+ b2RootOngoingHeldLogged: false,
5679
5813
  b2AbandonedSignalled: false,
5680
5814
  ambiguousPinnedSinceMs: 0,
5681
5815
  ambiguousResolved: false
@@ -5770,6 +5904,7 @@ var ChannelDriver = class _ChannelDriver {
5770
5904
  deliveryDeadlineAnchored: false,
5771
5905
  b2PinnedSinceMs: 0,
5772
5906
  b2LastDescendantCheckMs: 0,
5907
+ b2RootOngoingHeldLogged: false,
5773
5908
  b2AbandonedSignalled: false,
5774
5909
  ambiguousPinnedSinceMs: 0,
5775
5910
  ambiguousResolved: false
@@ -6123,6 +6258,7 @@ var ChannelDriver = class _ChannelDriver {
6123
6258
  if (snapshotReadable) {
6124
6259
  inFlight.b2PinnedSinceMs = 0;
6125
6260
  inFlight.b2LastDescendantCheckMs = 0;
6261
+ inFlight.b2RootOngoingHeldLogged = false;
6126
6262
  inFlight.b2AbandonedSignalled = false;
6127
6263
  }
6128
6264
  } else {
@@ -6134,11 +6270,15 @@ var ChannelDriver = class _ChannelDriver {
6134
6270
  const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
6135
6271
  if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
6136
6272
  inFlight.b2LastDescendantCheckMs = this.now();
6137
- const descendantOngoing = await this.isAnyDescendantSessionOngoing(sessionId);
6273
+ const [descendantOngoing, rootOngoing] = await Promise.all([
6274
+ this.isAnyDescendantSessionOngoing(sessionId),
6275
+ isSessionOngoing(this.port, sessionId)
6276
+ ]);
6138
6277
  if (isB2AbandonmentConfirmed({
6139
6278
  pinnedForMs,
6140
6279
  minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
6141
- descendantOngoing
6280
+ descendantOngoing,
6281
+ rootOngoing
6142
6282
  })) {
6143
6283
  inFlight.b2AbandonedSignalled = true;
6144
6284
  this.log({
@@ -6147,12 +6287,26 @@ var ChannelDriver = class _ChannelDriver {
6147
6287
  conversation_id: conv.id,
6148
6288
  message_id: id
6149
6289
  });
6290
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
6150
6291
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
6151
- watched_for_ms: pinnedForMs
6292
+ watched_for_ms: pinnedForMs,
6293
+ finish: reply?.info?.finish ?? reply?.finish,
6294
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
6295
+ ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
6296
+ opencode_message_id: inFlight.opencodeMessageId
6152
6297
  });
6153
6298
  await this.settleMessageDone(sessionId, watcher, inFlight, messages);
6154
6299
  return;
6155
6300
  }
6301
+ if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
6302
+ inFlight.b2RootOngoingHeldLogged = true;
6303
+ this.log({
6304
+ level: "warn",
6305
+ message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=${rootOngoing} and descendant_ongoing=${descendantOngoing} \u2014 holding until OpenCode confirms the root is idle`,
6306
+ conversation_id: conv.id,
6307
+ message_id: id
6308
+ });
6309
+ }
6156
6310
  }
6157
6311
  }
6158
6312
  const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
@@ -7895,6 +8049,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
7895
8049
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
7896
8050
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
7897
8051
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
8052
+ var CHILD_STOP_TIMEOUT_MS = 1e4;
7898
8053
  function resolveLogLevel(options) {
7899
8054
  const accepted = Object.keys(LOG_LEVELS);
7900
8055
  const validate = (value, source) => {
@@ -8500,7 +8655,17 @@ function scheduleClaudeUsageReporting(state, options) {
8500
8655
  setTimer: (timer) => {
8501
8656
  state.claudeUsageTimer = timer;
8502
8657
  },
8503
- fetchUsage: getClaudeUsage,
8658
+ fetchUsage: async () => {
8659
+ const usage = await getClaudeUsage();
8660
+ if (usage.ownerLookupError) {
8661
+ logActivity(state, {
8662
+ type: "info",
8663
+ level: "debug",
8664
+ message: `Claude usage owner lookup failed: ${usage.ownerLookupError}`
8665
+ });
8666
+ }
8667
+ return usage;
8668
+ },
8504
8669
  report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
8505
8670
  isLocalCredentialProblem,
8506
8671
  forcedOnHint: "run `claude` to sign in",
@@ -8672,15 +8837,31 @@ async function cleanup(state, opts = {}) {
8672
8837
  }
8673
8838
  if (state.opencodeProcess) {
8674
8839
  const opencodeProcess = state.opencodeProcess;
8675
- await timeShutdownPhase(state, durations, "opencode_stop", () => stopOpenCode(opencodeProcess));
8840
+ const result = await timeShutdownPhase(
8841
+ state,
8842
+ durations,
8843
+ "opencode_stop",
8844
+ () => stopOpenCodeAndWait(opencodeProcess, CHILD_STOP_TIMEOUT_MS)
8845
+ );
8676
8846
  if (state.interactive) {
8677
- logActivity(state, { type: "info", message: "Stopped OpenCode process" });
8847
+ logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
8678
8848
  displayStatus(state);
8679
8849
  } else {
8680
- log2(state, "Stopped OpenCode process");
8850
+ log2(state, `Stopped OpenCode process (${result.outcome})`);
8681
8851
  }
8682
8852
  state.opencodeProcess = null;
8683
8853
  }
8854
+ if (state.litestreamProcess) {
8855
+ const litestreamProcess = state.litestreamProcess;
8856
+ const result = await timeShutdownPhase(
8857
+ state,
8858
+ durations,
8859
+ "litestream_stop",
8860
+ () => stopSessionDbReplication(litestreamProcess, CHILD_STOP_TIMEOUT_MS)
8861
+ );
8862
+ log2(state, `Stopped litestream replication (${result.outcome})`);
8863
+ state.litestreamProcess = null;
8864
+ }
8684
8865
  return durations;
8685
8866
  }
8686
8867
  async function run(options) {
@@ -8714,6 +8895,7 @@ async function run(options) {
8714
8895
  opencodeConnected: false,
8715
8896
  opencodeVersion: null,
8716
8897
  opencodeProcess: null,
8898
+ litestreamProcess: null,
8717
8899
  connection: null,
8718
8900
  channelDriver: null,
8719
8901
  running: true,
@@ -8978,6 +9160,40 @@ async function run(options) {
8978
9160
  ocSpinner?.fail(error2.message);
8979
9161
  throw error2;
8980
9162
  }
9163
+ if (options.litestreamConfig) {
9164
+ const litestreamProcess = startSessionDbReplication(options.litestreamConfig);
9165
+ state.litestreamProcess = litestreamProcess;
9166
+ let failureHandled = false;
9167
+ const failRunForReplication = (message) => {
9168
+ if (failureHandled || state.shuttingDown || !state.running) return;
9169
+ failureHandled = true;
9170
+ state.shuttingDown = true;
9171
+ logActivity(state, { type: "error", error: message });
9172
+ if (state.interactive) displayStatus(state);
9173
+ void (async () => {
9174
+ try {
9175
+ await cleanup(state);
9176
+ await shutdownTelemetry();
9177
+ } catch (error2) {
9178
+ console.error(
9179
+ `[run] replication failure cleanup failed: ${error2 instanceof Error ? error2.message : String(error2)}`
9180
+ );
9181
+ }
9182
+ process.exit(1);
9183
+ })();
9184
+ };
9185
+ litestreamProcess.on("exit", (code, signal) => {
9186
+ failRunForReplication(
9187
+ `Litestream replication stopped unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`
9188
+ );
9189
+ });
9190
+ litestreamProcess.on("error", (error2) => {
9191
+ failRunForReplication(
9192
+ `Litestream replication failed: ${error2 instanceof Error ? error2.message : String(error2)}`
9193
+ );
9194
+ });
9195
+ log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
9196
+ }
8981
9197
  const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
8982
9198
  const channelDriver = new ChannelDriver({
8983
9199
  agentId: state.agentId,
@@ -9238,6 +9454,9 @@ program.command("run").description("Connect to Evident and process messages").op
9238
9454
  ).option(
9239
9455
  "--tunnel-ready-file <path>",
9240
9456
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
9457
+ ).option(
9458
+ "--litestream-config <path>",
9459
+ "Replicate the OpenCode session database with `litestream replicate` using this config file (written by the runner image). Omit to disable replication."
9241
9460
  ).action(
9242
9461
  (options) => {
9243
9462
  run({
@@ -9269,7 +9488,8 @@ program.command("run").description("Connect to Evident and process messages").op
9269
9488
  // Raw values — expansion/validation is single-sourced in run.ts's
9270
9489
  // resolveFileSyncDirectories.
9271
9490
  enableFileSyncTo: options.enableFileSyncTo,
9272
- tunnelReadyFile: options.tunnelReadyFile
9491
+ tunnelReadyFile: options.tunnelReadyFile,
9492
+ litestreamConfig: options.litestreamConfig
9273
9493
  });
9274
9494
  }
9275
9495
  );