@evident-ai/cli 3.4.1-dev.c6e7cf0 → 3.4.1-dev.f505f12

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
@@ -1681,6 +1681,62 @@ function buildOpenCodeVersionWarning(version2) {
1681
1681
 
1682
1682
  // src/lib/opencode/process.ts
1683
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
1684
1740
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
1685
1741
  function getProcessCwd(pid) {
1686
1742
  const platform = process.platform;
@@ -1852,23 +1908,20 @@ async function startOpenCode(port) {
1852
1908
  });
1853
1909
  return child;
1854
1910
  }
1855
- function stopOpenCode(opencodeProcess) {
1856
- if (!opencodeProcess || !opencodeProcess.pid) {
1857
- return;
1858
- }
1859
- try {
1911
+ function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
1912
+ const sendSignal = (signal) => {
1860
1913
  if (process.platform === "win32") {
1861
- opencodeProcess.kill("SIGTERM");
1914
+ opencodeProcess.kill(signal);
1862
1915
  } else {
1863
- process.kill(-opencodeProcess.pid, "SIGTERM");
1916
+ process.kill(-opencodeProcess.pid, signal);
1864
1917
  }
1865
- } catch (err) {
1866
- if (err.code !== "ESRCH") {
1867
- console.warn(
1868
- `stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`
1869
- );
1870
- }
1871
- }
1918
+ };
1919
+ return stopProcessAndWait(
1920
+ opencodeProcess,
1921
+ timeoutMs,
1922
+ () => sendSignal("SIGTERM"),
1923
+ () => sendSignal("SIGKILL")
1924
+ );
1872
1925
  }
1873
1926
 
1874
1927
  // src/lib/opencode/install.ts
@@ -2155,6 +2208,7 @@ async function createOpenCodeSession(port, directory) {
2155
2208
  return data.id;
2156
2209
  }
2157
2210
  async function getModelAttachmentCapability(port, model) {
2211
+ const { model: baseModel } = splitModelVariant(model);
2158
2212
  try {
2159
2213
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
2160
2214
  if (!res.ok) {
@@ -2171,9 +2225,9 @@ async function getModelAttachmentCapability(port, model) {
2171
2225
  );
2172
2226
  return null;
2173
2227
  }
2174
- const slash = model ? model.indexOf("/") : -1;
2175
- const providerId = slash > 0 ? model.slice(0, slash) : void 0;
2176
- let modelId = slash > 0 ? model.slice(slash + 1) : void 0;
2228
+ const slash = baseModel ? baseModel.indexOf("/") : -1;
2229
+ const providerId = slash > 0 ? baseModel.slice(0, slash) : void 0;
2230
+ let modelId = slash > 0 ? baseModel.slice(slash + 1) : void 0;
2177
2231
  const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
2178
2232
  let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
2179
2233
  if (!provider && !providerId) {
@@ -2253,6 +2307,29 @@ async function buildFileParts(attachments, capable) {
2253
2307
  }
2254
2308
  return { parts, outcomes, capabilityUnknown };
2255
2309
  }
2310
+ function splitModelVariant(raw) {
2311
+ const value = raw?.trim();
2312
+ if (!value) return {};
2313
+ const hashIndex = value.indexOf("#");
2314
+ if (hashIndex === -1) return { model: value };
2315
+ const model = value.slice(0, hashIndex).trim() || void 0;
2316
+ const variant = value.slice(hashIndex + 1).trim() || void 0;
2317
+ return { model, variant };
2318
+ }
2319
+ function applyModelOptions(body, options) {
2320
+ if (options?.agent) body.agent = options.agent;
2321
+ const { model, variant } = splitModelVariant(options?.model);
2322
+ if (model) {
2323
+ const slashIndex = model.indexOf("/");
2324
+ if (slashIndex !== -1) {
2325
+ body.model = {
2326
+ providerID: model.substring(0, slashIndex),
2327
+ modelID: model.substring(slashIndex + 1)
2328
+ };
2329
+ }
2330
+ }
2331
+ if (variant) body.variant = variant;
2332
+ }
2256
2333
  function messageText(m) {
2257
2334
  if (!m || !Array.isArray(m.parts)) return "";
2258
2335
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
@@ -2277,18 +2354,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
2277
2354
  const body = {
2278
2355
  parts
2279
2356
  };
2280
- if (options?.agent) {
2281
- body.agent = options.agent;
2282
- }
2283
- if (options?.model) {
2284
- const slashIndex = options.model.indexOf("/");
2285
- if (slashIndex !== -1) {
2286
- body.model = {
2287
- providerID: options.model.substring(0, slashIndex),
2288
- modelID: options.model.substring(slashIndex + 1)
2289
- };
2290
- }
2291
- }
2357
+ applyModelOptions(body, options);
2292
2358
  const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
2293
2359
  method: "POST",
2294
2360
  headers: { "Content-Type": "application/json" },
@@ -2296,7 +2362,10 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
2296
2362
  });
2297
2363
  if (res.status < 200 || res.status >= 300) {
2298
2364
  const text = await res.text().catch(() => "");
2299
- throw new Error(`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}`);
2365
+ const { variant } = splitModelVariant(options?.model);
2366
+ throw new Error(
2367
+ `OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}${variant ? ` (variant: ${variant})` : ""}`
2368
+ );
2300
2369
  }
2301
2370
  const READ_BACK_ATTEMPTS = 5;
2302
2371
  const READ_BACK_DELAY_MS = 150;
@@ -2451,7 +2520,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
2451
2520
  return completedOf(reply) != null && finishOf(reply) === "tool-calls";
2452
2521
  }
2453
2522
  function isB2AbandonmentConfirmed(params) {
2454
- return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
2523
+ return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
2455
2524
  }
2456
2525
  function isAmbiguousTerminalFinish(m) {
2457
2526
  if (completedOf(m) == null) return false;
@@ -2464,7 +2533,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
2464
2533
  return isAmbiguousTerminalFinish(reply);
2465
2534
  }
2466
2535
  function isAmbiguousFinishResolved(params) {
2467
- return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
2536
+ return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
2468
2537
  }
2469
2538
  function messageError(messages, userMessageId) {
2470
2539
  const reply = findLastAssistantReplyFor(messages, userMessageId);
@@ -3246,6 +3315,22 @@ function writeTunnelReadyMarker(path, agentId) {
3246
3315
  }
3247
3316
  }
3248
3317
 
3318
+ // src/lib/replication.ts
3319
+ import { spawn as spawn2 } from "child_process";
3320
+ function startSessionDbReplication(configPath) {
3321
+ return spawn2("litestream", ["replicate", "-config", configPath], {
3322
+ stdio: "inherit"
3323
+ });
3324
+ }
3325
+ async function stopSessionDbReplication(child, timeoutMs) {
3326
+ return stopProcessAndWait(
3327
+ child,
3328
+ timeoutMs,
3329
+ () => child.kill("SIGTERM"),
3330
+ () => child.kill("SIGKILL")
3331
+ );
3332
+ }
3333
+
3249
3334
  // src/lib/openai-usage.ts
3250
3335
  import { readFileSync as readFileSync3 } from "fs";
3251
3336
  import { homedir as homedir2 } from "os";
@@ -5740,6 +5825,7 @@ var ChannelDriver = class _ChannelDriver {
5740
5825
  deliveryDeadlineAnchored: false,
5741
5826
  b2PinnedSinceMs: 0,
5742
5827
  b2LastDescendantCheckMs: 0,
5828
+ b2RootOngoingHeldLogged: false,
5743
5829
  b2AbandonedSignalled: false,
5744
5830
  ambiguousPinnedSinceMs: 0,
5745
5831
  ambiguousResolved: false
@@ -5834,6 +5920,7 @@ var ChannelDriver = class _ChannelDriver {
5834
5920
  deliveryDeadlineAnchored: false,
5835
5921
  b2PinnedSinceMs: 0,
5836
5922
  b2LastDescendantCheckMs: 0,
5923
+ b2RootOngoingHeldLogged: false,
5837
5924
  b2AbandonedSignalled: false,
5838
5925
  ambiguousPinnedSinceMs: 0,
5839
5926
  ambiguousResolved: false
@@ -6187,6 +6274,7 @@ var ChannelDriver = class _ChannelDriver {
6187
6274
  if (snapshotReadable) {
6188
6275
  inFlight.b2PinnedSinceMs = 0;
6189
6276
  inFlight.b2LastDescendantCheckMs = 0;
6277
+ inFlight.b2RootOngoingHeldLogged = false;
6190
6278
  inFlight.b2AbandonedSignalled = false;
6191
6279
  }
6192
6280
  } else {
@@ -6198,11 +6286,15 @@ var ChannelDriver = class _ChannelDriver {
6198
6286
  const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
6199
6287
  if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
6200
6288
  inFlight.b2LastDescendantCheckMs = this.now();
6201
- const descendantOngoing = await this.isAnyDescendantSessionOngoing(sessionId);
6289
+ const [descendantOngoing, rootOngoing] = await Promise.all([
6290
+ this.isAnyDescendantSessionOngoing(sessionId),
6291
+ isSessionOngoing(this.port, sessionId)
6292
+ ]);
6202
6293
  if (isB2AbandonmentConfirmed({
6203
6294
  pinnedForMs,
6204
6295
  minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
6205
- descendantOngoing
6296
+ descendantOngoing,
6297
+ rootOngoing
6206
6298
  })) {
6207
6299
  inFlight.b2AbandonedSignalled = true;
6208
6300
  this.log({
@@ -6211,12 +6303,26 @@ var ChannelDriver = class _ChannelDriver {
6211
6303
  conversation_id: conv.id,
6212
6304
  message_id: id
6213
6305
  });
6306
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
6214
6307
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
6215
- watched_for_ms: pinnedForMs
6308
+ watched_for_ms: pinnedForMs,
6309
+ finish: reply?.info?.finish ?? reply?.finish,
6310
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
6311
+ ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
6312
+ opencode_message_id: inFlight.opencodeMessageId
6216
6313
  });
6217
6314
  await this.settleMessageDone(sessionId, watcher, inFlight, messages);
6218
6315
  return;
6219
6316
  }
6317
+ if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
6318
+ inFlight.b2RootOngoingHeldLogged = true;
6319
+ this.log({
6320
+ level: "warn",
6321
+ 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`,
6322
+ conversation_id: conv.id,
6323
+ message_id: id
6324
+ });
6325
+ }
6220
6326
  }
6221
6327
  }
6222
6328
  const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
@@ -7959,6 +8065,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
7959
8065
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
7960
8066
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
7961
8067
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
8068
+ var CHILD_STOP_TIMEOUT_MS = 1e4;
7962
8069
  function resolveLogLevel(options) {
7963
8070
  const accepted = Object.keys(LOG_LEVELS);
7964
8071
  const validate = (value, source) => {
@@ -8746,15 +8853,31 @@ async function cleanup(state, opts = {}) {
8746
8853
  }
8747
8854
  if (state.opencodeProcess) {
8748
8855
  const opencodeProcess = state.opencodeProcess;
8749
- await timeShutdownPhase(state, durations, "opencode_stop", () => stopOpenCode(opencodeProcess));
8856
+ const result = await timeShutdownPhase(
8857
+ state,
8858
+ durations,
8859
+ "opencode_stop",
8860
+ () => stopOpenCodeAndWait(opencodeProcess, CHILD_STOP_TIMEOUT_MS)
8861
+ );
8750
8862
  if (state.interactive) {
8751
- logActivity(state, { type: "info", message: "Stopped OpenCode process" });
8863
+ logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
8752
8864
  displayStatus(state);
8753
8865
  } else {
8754
- log2(state, "Stopped OpenCode process");
8866
+ log2(state, `Stopped OpenCode process (${result.outcome})`);
8755
8867
  }
8756
8868
  state.opencodeProcess = null;
8757
8869
  }
8870
+ if (state.litestreamProcess) {
8871
+ const litestreamProcess = state.litestreamProcess;
8872
+ const result = await timeShutdownPhase(
8873
+ state,
8874
+ durations,
8875
+ "litestream_stop",
8876
+ () => stopSessionDbReplication(litestreamProcess, CHILD_STOP_TIMEOUT_MS)
8877
+ );
8878
+ log2(state, `Stopped litestream replication (${result.outcome})`);
8879
+ state.litestreamProcess = null;
8880
+ }
8758
8881
  return durations;
8759
8882
  }
8760
8883
  async function run(options) {
@@ -8788,6 +8911,7 @@ async function run(options) {
8788
8911
  opencodeConnected: false,
8789
8912
  opencodeVersion: null,
8790
8913
  opencodeProcess: null,
8914
+ litestreamProcess: null,
8791
8915
  connection: null,
8792
8916
  channelDriver: null,
8793
8917
  running: true,
@@ -9052,6 +9176,40 @@ async function run(options) {
9052
9176
  ocSpinner?.fail(error2.message);
9053
9177
  throw error2;
9054
9178
  }
9179
+ if (options.litestreamConfig) {
9180
+ const litestreamProcess = startSessionDbReplication(options.litestreamConfig);
9181
+ state.litestreamProcess = litestreamProcess;
9182
+ let failureHandled = false;
9183
+ const failRunForReplication = (message) => {
9184
+ if (failureHandled || state.shuttingDown || !state.running) return;
9185
+ failureHandled = true;
9186
+ state.shuttingDown = true;
9187
+ logActivity(state, { type: "error", error: message });
9188
+ if (state.interactive) displayStatus(state);
9189
+ void (async () => {
9190
+ try {
9191
+ await cleanup(state);
9192
+ await shutdownTelemetry();
9193
+ } catch (error2) {
9194
+ console.error(
9195
+ `[run] replication failure cleanup failed: ${error2 instanceof Error ? error2.message : String(error2)}`
9196
+ );
9197
+ }
9198
+ process.exit(1);
9199
+ })();
9200
+ };
9201
+ litestreamProcess.on("exit", (code, signal) => {
9202
+ failRunForReplication(
9203
+ `Litestream replication stopped unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`
9204
+ );
9205
+ });
9206
+ litestreamProcess.on("error", (error2) => {
9207
+ failRunForReplication(
9208
+ `Litestream replication failed: ${error2 instanceof Error ? error2.message : String(error2)}`
9209
+ );
9210
+ });
9211
+ log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
9212
+ }
9055
9213
  const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
9056
9214
  const channelDriver = new ChannelDriver({
9057
9215
  agentId: state.agentId,
@@ -9312,6 +9470,9 @@ program.command("run").description("Connect to Evident and process messages").op
9312
9470
  ).option(
9313
9471
  "--tunnel-ready-file <path>",
9314
9472
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
9473
+ ).option(
9474
+ "--litestream-config <path>",
9475
+ "Replicate the OpenCode session database with `litestream replicate` using this config file (written by the runner image). Omit to disable replication."
9315
9476
  ).action(
9316
9477
  (options) => {
9317
9478
  run({
@@ -9343,7 +9504,8 @@ program.command("run").description("Connect to Evident and process messages").op
9343
9504
  // Raw values — expansion/validation is single-sourced in run.ts's
9344
9505
  // resolveFileSyncDirectories.
9345
9506
  enableFileSyncTo: options.enableFileSyncTo,
9346
- tunnelReadyFile: options.tunnelReadyFile
9507
+ tunnelReadyFile: options.tunnelReadyFile,
9508
+ litestreamConfig: options.litestreamConfig
9347
9509
  });
9348
9510
  }
9349
9511
  );