@evident-ai/cli 3.4.0 → 3.4.1-dev.11f0c53

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
@@ -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,42 @@ 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)
743
+ }),
744
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
745
+ });
746
+ if (!response.ok) {
747
+ const serverMessage = await readErrorMessage(response);
748
+ return {
749
+ ok: false,
750
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
751
+ };
752
+ }
753
+ return { ok: true };
754
+ } catch (error2) {
755
+ return { ok: false, error: describeBestEffortError(error2) };
756
+ }
757
+ }
758
+ function toReportedOpenAiWindow(window) {
759
+ if (!window) return null;
760
+ return {
761
+ utilization: window.utilization,
762
+ window_minutes: window.windowMinutes,
763
+ resets_at: window.resetsAt
764
+ };
765
+ }
766
+ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
767
+ try {
768
+ const apiUrl = getApiUrlConfig();
769
+ const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
770
+ method: "POST",
771
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
772
+ body: JSON.stringify({
773
+ primary: toReportedOpenAiWindow(snapshot.primary),
774
+ secondary: toReportedOpenAiWindow(snapshot.secondary),
775
+ has_credits: snapshot.hasCredits,
776
+ credits_unlimited: snapshot.creditsUnlimited
734
777
  }),
735
778
  signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
736
779
  });
@@ -962,7 +1005,10 @@ import { readFileSync } from "fs";
962
1005
  import { homedir } from "os";
963
1006
  import { join } from "path";
964
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;
965
1010
  var KEYCHAIN_SERVICE = "Claude Code-credentials";
1011
+ var cachedOwner = null;
966
1012
  var CLAUDE_CREDENTIALS_SEGMENTS = [".claude", ".credentials.json"];
967
1013
  function parseClaudeCliCredentials(raw) {
968
1014
  let parsed;
@@ -1036,6 +1082,47 @@ function toWindow(value) {
1036
1082
  }
1037
1083
  return { utilization: window.utilization, resetsAt };
1038
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
+ }
1039
1126
  async function getClaudeUsage() {
1040
1127
  const credentials2 = readClaudeCliCredentials();
1041
1128
  if (!credentials2) {
@@ -1055,15 +1142,19 @@ async function getClaudeUsage() {
1055
1142
  Authorization: `Bearer ${credentials2.accessToken}`,
1056
1143
  "Content-Type": "application/json",
1057
1144
  "anthropic-version": "2023-06-01"
1058
- }
1145
+ },
1146
+ signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
1059
1147
  });
1060
1148
  if (!res.ok) {
1061
1149
  throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
1062
1150
  }
1063
1151
  const body = await res.json();
1152
+ const { owner, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
1064
1153
  return {
1065
1154
  fiveHour: toWindow(body.five_hour),
1066
- sevenDay: toWindow(body.seven_day)
1155
+ sevenDay: toWindow(body.seven_day),
1156
+ owner,
1157
+ ownerLookupError
1067
1158
  };
1068
1159
  }
1069
1160
 
@@ -1092,8 +1183,8 @@ async function claudeUsage() {
1092
1183
  }
1093
1184
 
1094
1185
  // src/commands/run.ts
1095
- import { homedir as homedir3 } from "os";
1096
- import { isAbsolute as isAbsolute2, join as join5, parse, resolve as resolvePath } from "path";
1186
+ import { homedir as homedir4 } from "os";
1187
+ import { isAbsolute as isAbsolute2, join as join7, parse, resolve as resolvePath } from "path";
1097
1188
  import chalk6 from "chalk";
1098
1189
 
1099
1190
  // ../../packages/types/src/agents/index.ts
@@ -1326,6 +1417,8 @@ var SEVERITY_BY_LEVEL = {
1326
1417
  error: "error"
1327
1418
  };
1328
1419
  var MAX_MESSAGE_LENGTH = 500;
1420
+ var MAX_METADATA_VALUE_LENGTH = 200;
1421
+ var MAX_METADATA_ENTRIES = 20;
1329
1422
  var TRUNCATION_MARKER = "\u2026";
1330
1423
  function redact(message) {
1331
1424
  return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/https?:\/\/\S+/g, "<url>");
@@ -1334,6 +1427,24 @@ function truncate(message) {
1334
1427
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
1335
1428
  return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
1336
1429
  }
1430
+ function sanitiseMetadata(metadata) {
1431
+ if (!metadata) return {};
1432
+ const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
1433
+ if (Object.keys(metadata).length > entries.length) {
1434
+ console.error(
1435
+ `[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
1436
+ );
1437
+ }
1438
+ const sanitised = [];
1439
+ for (const [key, value] of entries) {
1440
+ if (typeof value === "string") {
1441
+ sanitised.push([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
1442
+ } else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
1443
+ sanitised.push([key, value]);
1444
+ }
1445
+ }
1446
+ return Object.fromEntries(sanitised);
1447
+ }
1337
1448
  var RATE_LIMIT_WINDOW_MS = 6e4;
1338
1449
  var RATE_LIMIT_MAX_EVENTS = 30;
1339
1450
  var windowStartedAt = 0;
@@ -1372,7 +1483,7 @@ function forwardRunnerActivity(entry, context) {
1372
1483
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1373
1484
  severity: SEVERITY_BY_LEVEL[entry.level],
1374
1485
  message,
1375
- metadata: { source: "cli.run" },
1486
+ metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
1376
1487
  agentId: context.agentId
1377
1488
  });
1378
1489
  } catch (err) {
@@ -1382,6 +1493,150 @@ function forwardRunnerActivity(entry, context) {
1382
1493
  }
1383
1494
  }
1384
1495
 
1496
+ // src/lib/opencode/session-db-recovery-report.ts
1497
+ import { readFileSync as readFileSync2, unlinkSync } from "fs";
1498
+ import { join as join2 } from "path";
1499
+ function sessionDbRecoveryReportPath(homeDir, env) {
1500
+ const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1501
+ return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
1502
+ }
1503
+ function drainSessionDbRecoveryReport({
1504
+ homeDir,
1505
+ env
1506
+ }) {
1507
+ const path = sessionDbRecoveryReportPath(homeDir, env);
1508
+ let content;
1509
+ try {
1510
+ content = readFileSync2(path, "utf8");
1511
+ } catch (error2) {
1512
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
1513
+ return { path, records: [], skippedLines: 0, readError: null };
1514
+ const readError = error2 instanceof Error ? error2.message : String(error2);
1515
+ console.error(`[session-db-recovery-report] could not read ${path}: ${readError}`);
1516
+ return { path, records: [], skippedLines: 0, readError };
1517
+ }
1518
+ let skippedLines = 0;
1519
+ const records = content.split("\n").flatMap((line) => {
1520
+ if (!line.trim()) return [];
1521
+ try {
1522
+ const value = JSON.parse(line);
1523
+ if (!isSessionDbRecoveryRecord(value)) {
1524
+ skippedLines++;
1525
+ return [];
1526
+ }
1527
+ return [value];
1528
+ } catch (error2) {
1529
+ skippedLines++;
1530
+ console.error(
1531
+ `[session-db-recovery-report] skipped malformed record in ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
1532
+ );
1533
+ return [];
1534
+ }
1535
+ });
1536
+ return { path, records, skippedLines, readError: null };
1537
+ }
1538
+ function acknowledgeSessionDbRecoveryReport(path) {
1539
+ try {
1540
+ unlinkSync(path);
1541
+ } catch (error2) {
1542
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
1543
+ console.error(
1544
+ `[session-db-recovery-report] could not remove ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
1545
+ );
1546
+ }
1547
+ }
1548
+ function buildSessionDbRecoveryActivity(record) {
1549
+ const level = record.severity === "warning" ? "warn" : record.severity === "error" ? "error" : null;
1550
+ if (!level) return null;
1551
+ const noVerifiedPoint = record.verified_restore_point ? ` The verified restore point is ${record.verified_restore_point}.` : " No verified restore point is known.";
1552
+ switch (record.outcome) {
1553
+ case "fresh_session_db":
1554
+ return {
1555
+ level,
1556
+ metadata: withoutContractFields(record),
1557
+ message: `This runner started with a fresh session database. Earlier sessions are unavailable, and queued conversation or session references from before this start may no longer resolve.${noVerifiedPoint} Review the runner's backup configuration before relying on restored session history.`
1558
+ };
1559
+ case "restore_retried":
1560
+ return {
1561
+ level,
1562
+ metadata: withoutContractFields(record),
1563
+ message: "The initial session database restore failed; the runner will retry it. Monitor the runner backup for another restore failure."
1564
+ };
1565
+ case "replica_recovered":
1566
+ if (record.reason === "quarantine")
1567
+ return {
1568
+ level,
1569
+ metadata: withoutContractFields(record),
1570
+ message: `Session database recovery quarantined ${record.quarantined_objects ?? "unknown"} objects (${record.quarantined_bytes ?? "unknown"} bytes); ${record.quarantine_failed_objects ?? "unknown"} moves failed. Review the preserved backup at ${record.quarantine_destination ?? "an unknown destination"} before deleting it.`
1571
+ };
1572
+ if (record.reason === "prune")
1573
+ return {
1574
+ level,
1575
+ metadata: withoutContractFields(record),
1576
+ message: "Session database recovery discarded a damaged newest backup and retried. Sessions recorded after the previous backup point may be unavailable. Review the runner backup for another restore failure."
1577
+ };
1578
+ if (record.reason === "clear")
1579
+ return {
1580
+ level,
1581
+ metadata: withoutContractFields(record),
1582
+ message: "Session database recovery deleted the damaged backup and prior session history is unavailable. Review the runner backup configuration before relying on restored session history."
1583
+ };
1584
+ return null;
1585
+ case "history_rolled_back":
1586
+ return {
1587
+ level,
1588
+ metadata: withoutContractFields(record),
1589
+ message: `Session history was rolled back to verified restore point ${record.verified_restore_point ?? "unknown"}; everything after it is unavailable. Review the runner backup for another restore failure.`
1590
+ };
1591
+ case "restore_misconfigured":
1592
+ return {
1593
+ level,
1594
+ metadata: withoutContractFields(record),
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."
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
+ };
1603
+ default:
1604
+ return null;
1605
+ }
1606
+ }
1607
+ function withoutContractFields(record) {
1608
+ const { v: _v, event: _event, ...metadata } = record;
1609
+ return metadata;
1610
+ }
1611
+ var OUTCOMES = /* @__PURE__ */ new Set([
1612
+ "replica_recovered",
1613
+ "restore_retried",
1614
+ "fresh_session_db",
1615
+ "history_rolled_back",
1616
+ "restore_misconfigured",
1617
+ "session_db_boot_refused"
1618
+ ]);
1619
+ var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
1620
+ var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
1621
+ var NUMBER_FIELDS = [
1622
+ "litestream_exit_code",
1623
+ "attempt",
1624
+ "replica_objects",
1625
+ "replica_bytes",
1626
+ "quarantined_objects",
1627
+ "quarantine_failed_objects",
1628
+ "quarantined_bytes",
1629
+ "restore_points_tried"
1630
+ ];
1631
+ var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"];
1632
+ function isSessionDbRecoveryRecord(value) {
1633
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1634
+ const record = value;
1635
+ 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" && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
1636
+ (field) => record[field] === null || typeof record[field] === "string"
1637
+ );
1638
+ }
1639
+
1385
1640
  // src/lib/opencode/health.ts
1386
1641
  async function checkOpenCodeHealth(port) {
1387
1642
  try {
@@ -1426,6 +1681,62 @@ function buildOpenCodeVersionWarning(version2) {
1426
1681
 
1427
1682
  // src/lib/opencode/process.ts
1428
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
1429
1740
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
1430
1741
  function getProcessCwd(pid) {
1431
1742
  const platform = process.platform;
@@ -1597,23 +1908,20 @@ async function startOpenCode(port) {
1597
1908
  });
1598
1909
  return child;
1599
1910
  }
1600
- function stopOpenCode(opencodeProcess) {
1601
- if (!opencodeProcess || !opencodeProcess.pid) {
1602
- return;
1603
- }
1604
- try {
1911
+ function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
1912
+ const sendSignal = (signal) => {
1605
1913
  if (process.platform === "win32") {
1606
- opencodeProcess.kill("SIGTERM");
1914
+ opencodeProcess.kill(signal);
1607
1915
  } else {
1608
- process.kill(-opencodeProcess.pid, "SIGTERM");
1916
+ process.kill(-opencodeProcess.pid, signal);
1609
1917
  }
1610
- } catch (err) {
1611
- if (err.code !== "ESRCH") {
1612
- console.warn(
1613
- `stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`
1614
- );
1615
- }
1616
- }
1918
+ };
1919
+ return stopProcessAndWait(
1920
+ opencodeProcess,
1921
+ timeoutMs,
1922
+ () => sendSignal("SIGTERM"),
1923
+ () => sendSignal("SIGKILL")
1924
+ );
1617
1925
  }
1618
1926
 
1619
1927
  // src/lib/opencode/install.ts
@@ -1900,6 +2208,7 @@ async function createOpenCodeSession(port, directory) {
1900
2208
  return data.id;
1901
2209
  }
1902
2210
  async function getModelAttachmentCapability(port, model) {
2211
+ const { model: baseModel } = splitModelVariant(model);
1903
2212
  try {
1904
2213
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
1905
2214
  if (!res.ok) {
@@ -1916,9 +2225,9 @@ async function getModelAttachmentCapability(port, model) {
1916
2225
  );
1917
2226
  return null;
1918
2227
  }
1919
- const slash = model ? model.indexOf("/") : -1;
1920
- const providerId = slash > 0 ? model.slice(0, slash) : void 0;
1921
- 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;
1922
2231
  const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
1923
2232
  let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
1924
2233
  if (!provider && !providerId) {
@@ -1998,6 +2307,29 @@ async function buildFileParts(attachments, capable) {
1998
2307
  }
1999
2308
  return { parts, outcomes, capabilityUnknown };
2000
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
+ }
2001
2333
  function messageText(m) {
2002
2334
  if (!m || !Array.isArray(m.parts)) return "";
2003
2335
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
@@ -2022,18 +2354,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
2022
2354
  const body = {
2023
2355
  parts
2024
2356
  };
2025
- if (options?.agent) {
2026
- body.agent = options.agent;
2027
- }
2028
- if (options?.model) {
2029
- const slashIndex = options.model.indexOf("/");
2030
- if (slashIndex !== -1) {
2031
- body.model = {
2032
- providerID: options.model.substring(0, slashIndex),
2033
- modelID: options.model.substring(slashIndex + 1)
2034
- };
2035
- }
2036
- }
2357
+ applyModelOptions(body, options);
2037
2358
  const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
2038
2359
  method: "POST",
2039
2360
  headers: { "Content-Type": "application/json" },
@@ -2041,7 +2362,10 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
2041
2362
  });
2042
2363
  if (res.status < 200 || res.status >= 300) {
2043
2364
  const text = await res.text().catch(() => "");
2044
- 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
+ );
2045
2369
  }
2046
2370
  const READ_BACK_ATTEMPTS = 5;
2047
2371
  const READ_BACK_DELAY_MS = 150;
@@ -2196,7 +2520,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
2196
2520
  return completedOf(reply) != null && finishOf(reply) === "tool-calls";
2197
2521
  }
2198
2522
  function isB2AbandonmentConfirmed(params) {
2199
- return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
2523
+ return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
2200
2524
  }
2201
2525
  function isAmbiguousTerminalFinish(m) {
2202
2526
  if (completedOf(m) == null) return false;
@@ -2209,7 +2533,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
2209
2533
  return isAmbiguousTerminalFinish(reply);
2210
2534
  }
2211
2535
  function isAmbiguousFinishResolved(params) {
2212
- return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
2536
+ return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
2213
2537
  }
2214
2538
  function messageError(messages, userMessageId) {
2215
2539
  const reply = findLastAssistantReplyFor(messages, userMessageId);
@@ -2419,10 +2743,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
2419
2743
 
2420
2744
  // src/lib/opencode/session-db-size.ts
2421
2745
  import { statSync as statSync2 } from "fs";
2422
- import { join as join2 } from "path";
2746
+ import { join as join3 } from "path";
2423
2747
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
2424
2748
  function statSessionDbBytes(homeDir) {
2425
- const dbPath = join2(homeDir, ".local", "share", "opencode", "opencode.db");
2749
+ const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
2426
2750
  try {
2427
2751
  return statSync2(dbPath).size;
2428
2752
  } catch (err) {
@@ -2991,6 +3315,193 @@ function writeTunnelReadyMarker(path, agentId) {
2991
3315
  }
2992
3316
  }
2993
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
+
3334
+ // src/lib/openai-usage.ts
3335
+ import { readFileSync as readFileSync3 } from "fs";
3336
+ import { homedir as homedir2 } from "os";
3337
+ import { join as join4 } from "path";
3338
+ var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3339
+ var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
3340
+ var OpenAiUsageError = class extends Error {
3341
+ constructor(message, reason) {
3342
+ super(message);
3343
+ this.reason = reason;
3344
+ }
3345
+ };
3346
+ function isLocalCredentialProblem2(err) {
3347
+ return err instanceof OpenAiUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
3348
+ }
3349
+ function readOpenCodeChatGptCredentials() {
3350
+ try {
3351
+ const raw = readFileSync3(join4(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3352
+ let parsed;
3353
+ try {
3354
+ parsed = JSON.parse(raw);
3355
+ } catch {
3356
+ return null;
3357
+ }
3358
+ const entry = parsed.openai;
3359
+ if (entry?.type !== "oauth" || typeof entry.access !== "string" || !entry.access || typeof entry.expires !== "number") {
3360
+ return null;
3361
+ }
3362
+ return { accessToken: entry.access, expiresAt: entry.expires };
3363
+ } catch (err) {
3364
+ const code = err.code;
3365
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
3366
+ console.warn(
3367
+ `readOpenCodeChatGptCredentials: reading auth.json failed (${code ?? "unknown"})`
3368
+ );
3369
+ }
3370
+ return null;
3371
+ }
3372
+ }
3373
+ function toWindow2(headers, name) {
3374
+ const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
3375
+ const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
3376
+ if (utilizationHeader == null || utilizationHeader === "" || windowMinutesHeader == null) {
3377
+ return null;
3378
+ }
3379
+ const utilization = Number(utilizationHeader);
3380
+ const windowMinutes = Number(windowMinutesHeader);
3381
+ if (!Number.isFinite(utilization) || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
3382
+ return null;
3383
+ }
3384
+ const resetAtHeader = headers.get(`x-codex-${name}-reset-at`);
3385
+ const resetSeconds = resetAtHeader == null || resetAtHeader === "" ? NaN : Number(resetAtHeader);
3386
+ const resetsAt = Number.isFinite(resetSeconds) ? new Date(resetSeconds * 1e3).toISOString() : null;
3387
+ return { utilization: Math.min(100, Math.max(0, utilization)), windowMinutes, resetsAt };
3388
+ }
3389
+ function parseCodexUsageHeaders(headers) {
3390
+ return {
3391
+ primary: toWindow2(headers, "primary"),
3392
+ secondary: toWindow2(headers, "secondary"),
3393
+ hasCredits: headers.has("x-codex-credits-has-credits") ? headers.get("x-codex-credits-has-credits")?.toLowerCase() === "true" : null,
3394
+ creditsUnlimited: headers.has("x-codex-credits-unlimited") ? headers.get("x-codex-credits-unlimited")?.toLowerCase() === "true" : null
3395
+ };
3396
+ }
3397
+ function normalizeProbeModel(model) {
3398
+ return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
3399
+ }
3400
+ async function resolveProbeModels(port) {
3401
+ try {
3402
+ const res = await withRequestTimeout(
3403
+ fetch,
3404
+ REQUEST_TIMEOUT_MS
3405
+ )(`${opencodeBase(port)}/config/providers`);
3406
+ if (!res.ok) {
3407
+ console.error(
3408
+ `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
3409
+ );
3410
+ return [];
3411
+ }
3412
+ const body = await res.json();
3413
+ const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
3414
+ if (!provider || !provider.models || typeof provider.models !== "object") return [];
3415
+ const candidates = [
3416
+ ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
3417
+ ...Object.keys(provider.models)
3418
+ ].map(normalizeProbeModel);
3419
+ return [...new Set(candidates)].slice(0, 4);
3420
+ } catch (err) {
3421
+ console.error(
3422
+ `[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3423
+ );
3424
+ return [];
3425
+ }
3426
+ }
3427
+ function hasPrimaryHeaders(headers) {
3428
+ return [
3429
+ "x-codex-primary-used-percent",
3430
+ "x-codex-primary-window-minutes",
3431
+ "x-codex-primary-reset-at"
3432
+ ].some((name) => headers.has(name));
3433
+ }
3434
+ async function getOpenAiUsage(port) {
3435
+ const credentials2 = readOpenCodeChatGptCredentials();
3436
+ if (!credentials2) {
3437
+ throw new OpenAiUsageError(
3438
+ "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3439
+ "no_credentials"
3440
+ );
3441
+ }
3442
+ if (credentials2.expiresAt < Date.now()) {
3443
+ throw new OpenAiUsageError(
3444
+ "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3445
+ "credentials_expired"
3446
+ );
3447
+ }
3448
+ const models = await resolveProbeModels(port);
3449
+ if (models.length === 0) {
3450
+ throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
3451
+ }
3452
+ let lastStatus;
3453
+ for (const model of models) {
3454
+ let res;
3455
+ try {
3456
+ res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
3457
+ method: "POST",
3458
+ headers: {
3459
+ Authorization: `Bearer ${credentials2.accessToken}`,
3460
+ "Content-Type": "application/json"
3461
+ },
3462
+ body: JSON.stringify({ model, store: false, stream: true })
3463
+ });
3464
+ } catch (err) {
3465
+ throw new OpenAiUsageError(
3466
+ `OpenAI usage probe request failed: ${err instanceof Error ? err.message : String(err)}.`,
3467
+ "request_failed"
3468
+ );
3469
+ }
3470
+ try {
3471
+ lastStatus = res.status;
3472
+ if (hasPrimaryHeaders(res.headers)) {
3473
+ const usage = parseCodexUsageHeaders(res.headers);
3474
+ if (!usage.primary && !usage.secondary) {
3475
+ throw new OpenAiUsageError(
3476
+ `OpenAI usage probe returned no usable window (HTTP ${res.status}).`,
3477
+ "no_usable_window"
3478
+ );
3479
+ }
3480
+ return usage;
3481
+ }
3482
+ if (res.status === 401) {
3483
+ throw new OpenAiUsageError(
3484
+ "ChatGPT credentials have expired (HTTP 401).",
3485
+ "credentials_expired"
3486
+ );
3487
+ }
3488
+ if (res.status === 403 || res.status === 429) {
3489
+ throw new OpenAiUsageError(
3490
+ `OpenAI usage probe was blocked (HTTP ${res.status}).`,
3491
+ "probe_blocked"
3492
+ );
3493
+ }
3494
+ } finally {
3495
+ await res.body?.cancel().catch(() => {
3496
+ });
3497
+ }
3498
+ }
3499
+ throw new OpenAiUsageError(
3500
+ `OpenAI usage probe failed: HTTP ${lastStatus ?? "unknown"}.`,
3501
+ "request_failed"
3502
+ );
3503
+ }
3504
+
2994
3505
  // src/lib/reporting-schedule.ts
2995
3506
  function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2996
3507
  const jitterRangeMs = baseMs * jitterFraction;
@@ -2999,6 +3510,31 @@ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2999
3510
  function firstReportDelayMs(random = Math.random) {
3000
3511
  return 5e3 + random() * 1e4;
3001
3512
  }
3513
+ var VALID_USAGE_REPORTING_MODES = ["auto", "on", "off"];
3514
+ function resolveUsageReportingMode(flagValue, env, names) {
3515
+ const raw = flagValue ?? env[names.envVar];
3516
+ if (raw === void 0 || raw === "") return { mode: "auto", warnings: [] };
3517
+ const normalized = raw.trim().toLowerCase();
3518
+ if (VALID_USAGE_REPORTING_MODES.includes(normalized)) {
3519
+ return { mode: normalized, warnings: [] };
3520
+ }
3521
+ const source = flagValue !== void 0 ? names.flagName : names.envVar;
3522
+ return {
3523
+ mode: "auto",
3524
+ warnings: [
3525
+ `Ignoring invalid ${source} "${raw}": expected one of ${VALID_USAGE_REPORTING_MODES.join(", ")}; using auto`
3526
+ ]
3527
+ };
3528
+ }
3529
+ var BASE_USAGE_REPORT_DELAY_MS = 10 * 6e4;
3530
+ var USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
3531
+ function usageReportDelayMs(random = Math.random) {
3532
+ return jitteredDelayMs(BASE_USAGE_REPORT_DELAY_MS, USAGE_REPORT_DELAY_JITTER_FRACTION, random);
3533
+ }
3534
+ var USAGE_REPORT_FAILURE_REESCALATION_TICKS = 6;
3535
+ function usageReportFailureLogLevel(consecutiveFailures) {
3536
+ return reportFailureLogLevel(consecutiveFailures, USAGE_REPORT_FAILURE_REESCALATION_TICKS);
3537
+ }
3002
3538
  function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
3003
3539
  return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
3004
3540
  }
@@ -3007,33 +3543,26 @@ function failureStreakSuffix(consecutiveFailures) {
3007
3543
  }
3008
3544
 
3009
3545
  // src/lib/claude-usage-reporting.ts
3010
- var VALID_MODES = ["auto", "on", "off"];
3011
3546
  function resolveClaudeUsageReportingMode(flagValue, env) {
3012
- const raw = flagValue ?? env.EVIDENT_CLAUDE_USAGE_REPORTING;
3013
- if (raw === void 0 || raw === "") {
3014
- return { mode: "auto", warnings: [] };
3015
- }
3016
- const normalized = raw.trim().toLowerCase();
3017
- if (VALID_MODES.includes(normalized)) {
3018
- return { mode: normalized, warnings: [] };
3019
- }
3020
- const source = flagValue !== void 0 ? "--claude-usage-reporting" : "EVIDENT_CLAUDE_USAGE_REPORTING";
3021
- return {
3022
- mode: "auto",
3023
- warnings: [
3024
- `Ignoring invalid ${source} "${raw}": expected one of ${VALID_MODES.join(", ")}; using auto`
3025
- ]
3026
- };
3547
+ return resolveUsageReportingMode(flagValue, env, {
3548
+ flagName: "--claude-usage-reporting",
3549
+ envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
3550
+ });
3027
3551
  }
3028
- var BASE_REPORT_DELAY_MS = 10 * 6e4;
3029
- var REPORT_DELAY_JITTER_FRACTION = 0.2;
3030
3552
  function nextReportDelayMs(random = Math.random) {
3031
- return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);
3553
+ return usageReportDelayMs(random);
3032
3554
  }
3033
3555
  var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
3034
- var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
3035
3556
  function claudeUsageFailureLogLevel(consecutiveFailures) {
3036
- return reportFailureLogLevel(consecutiveFailures, CLAUDE_USAGE_FAILURE_REESCALATION_TICKS);
3557
+ return usageReportFailureLogLevel(consecutiveFailures);
3558
+ }
3559
+
3560
+ // src/lib/openai-usage-reporting.ts
3561
+ function resolveOpenAiUsageReportingMode(flagValue, env) {
3562
+ return resolveUsageReportingMode(flagValue, env, {
3563
+ flagName: "--openai-usage-reporting",
3564
+ envVar: "EVIDENT_OPENAI_USAGE_REPORTING"
3565
+ });
3037
3566
  }
3038
3567
 
3039
3568
  // src/lib/resource-usage-reporting.ts
@@ -3192,15 +3721,15 @@ function createResourceUsageCollector(homeDir) {
3192
3721
  }
3193
3722
 
3194
3723
  // src/lib/channels/driver.ts
3195
- import { homedir as homedir2 } from "os";
3724
+ import { homedir as homedir3 } from "os";
3196
3725
 
3197
3726
  // src/lib/runner-file-sync.ts
3198
- import { join as join4 } from "path";
3727
+ import { join as join6 } from "path";
3199
3728
 
3200
3729
  // src/lib/file-push.ts
3201
3730
  import { randomUUID } from "crypto";
3202
3731
  import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
3203
- import { basename, dirname as dirname3, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
3732
+ import { basename, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
3204
3733
  var FILE_MODE = 384;
3205
3734
  var DIRECTORY_MODE = 448;
3206
3735
  async function writePushedFile(request) {
@@ -3233,7 +3762,7 @@ async function writePushedFile(request) {
3233
3762
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
3234
3763
  dirname3(candidate)
3235
3764
  );
3236
- const realTarget = join3(existingAncestor, ...missingSegments, basename(candidate));
3765
+ const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
3237
3766
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
3238
3767
  if (allowedDirectory === null) {
3239
3768
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -3269,7 +3798,7 @@ function expandAndValidate(requestedPath, homeDir) {
3269
3798
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
3270
3799
  return null;
3271
3800
  }
3272
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join3(homeDir, requestedPath.slice(2)) : requestedPath;
3801
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
3273
3802
  if (expanded.split(/[/\\]/).includes("..")) {
3274
3803
  return null;
3275
3804
  }
@@ -3342,13 +3871,13 @@ function contains(realDirectory, realTarget) {
3342
3871
  async function createMissingDirectories(existingAncestor, missingSegments) {
3343
3872
  let current = existingAncestor;
3344
3873
  for (const segment of missingSegments) {
3345
- current = join3(current, segment);
3874
+ current = join5(current, segment);
3346
3875
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
3347
3876
  await chmod(current, DIRECTORY_MODE);
3348
3877
  }
3349
3878
  }
3350
3879
  async function writeAtomically(realTarget, content) {
3351
- const temporaryPath = join3(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3880
+ const temporaryPath = join5(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3352
3881
  let handle;
3353
3882
  try {
3354
3883
  handle = await open2(temporaryPath, "wx", FILE_MODE);
@@ -3390,20 +3919,28 @@ async function syncPendingRunnerFiles(options) {
3390
3919
  for (const id of options.ackFailures.keys()) {
3391
3920
  if (!pendingIds.has(id)) options.ackFailures.delete(id);
3392
3921
  }
3393
- if (pending.length === 0) return { applied: 0, claudeCredentialApplied: false };
3922
+ if (pending.length === 0) {
3923
+ return { applied: 0, claudeCredentialApplied: false, opencodeAuthApplied: false };
3924
+ }
3394
3925
  options.log({
3395
3926
  level: "info",
3396
3927
  message: `Runner file sync: ${pending.length} file(s) queued for this runner`
3397
3928
  });
3398
3929
  let applied = 0;
3399
3930
  let claudeCredentialApplied = false;
3931
+ let opencodeAuthApplied = false;
3400
3932
  for (const file of pending) {
3401
3933
  if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
3402
3934
  const outcome = await applyOne(options, file);
3403
3935
  if (outcome.applied) applied += 1;
3404
3936
  if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
3937
+ if (outcome.opencodeAuthApplied) opencodeAuthApplied = true;
3405
3938
  }
3406
- return { applied, claudeCredentialApplied };
3939
+ return {
3940
+ applied,
3941
+ claudeCredentialApplied,
3942
+ opencodeAuthApplied
3943
+ };
3407
3944
  }
3408
3945
  async function listPendingFiles(options) {
3409
3946
  let res;
@@ -3464,10 +4001,18 @@ function asPendingFile(entry) {
3464
4001
  if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
3465
4002
  return { id, path, size };
3466
4003
  }
3467
- var NOT_APPLIED = { applied: false, claudeCredentialApplied: false };
4004
+ var NOT_APPLIED = {
4005
+ applied: false,
4006
+ claudeCredentialApplied: false,
4007
+ opencodeAuthApplied: false
4008
+ };
3468
4009
  function isClaudeCredentialPath(requestedPath, homeDir) {
3469
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join4(homeDir, requestedPath.slice(2)) : requestedPath;
3470
- return expanded === join4(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4010
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
4011
+ return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
4012
+ }
4013
+ function isOpenCodeAuthPath(requestedPath, homeDir) {
4014
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
4015
+ return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
3471
4016
  }
3472
4017
  async function applyOne(options, file) {
3473
4018
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -3523,7 +4068,8 @@ async function applyOne(options, file) {
3523
4068
  await ack(options, file, "applied");
3524
4069
  return {
3525
4070
  applied: true,
3526
- claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
4071
+ claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),
4072
+ opencodeAuthApplied: isOpenCodeAuthPath(file.path, options.homeDir)
3527
4073
  };
3528
4074
  }
3529
4075
  function durableDownloadCode(status2) {
@@ -3977,6 +4523,7 @@ var ChannelDriver = class _ChannelDriver {
3977
4523
  * that way rather than "fixing" it into a count.
3978
4524
  */
3979
4525
  claudeCredentialApplyCount = 0;
4526
+ opencodeAuthApplyCount = 0;
3980
4527
  /**
3981
4528
  * The currently-executing `drainPending()` promise, or null when idle. Lets a
3982
4529
  * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
@@ -4011,7 +4558,7 @@ var ChannelDriver = class _ChannelDriver {
4011
4558
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
4012
4559
  this.now = config.now ?? (() => Date.now());
4013
4560
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
4014
- this.homeDir = config.homeDir ?? homedir2();
4561
+ this.homeDir = config.homeDir ?? homedir3();
4015
4562
  this.maxActiveSessions = config.maxActiveSessions;
4016
4563
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
4017
4564
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -4081,6 +4628,7 @@ var ChannelDriver = class _ChannelDriver {
4081
4628
  });
4082
4629
  this.appliedFileCount += result.applied;
4083
4630
  if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
4631
+ if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
4084
4632
  return result.applied;
4085
4633
  } catch (err) {
4086
4634
  this.log({
@@ -4188,7 +4736,8 @@ var ChannelDriver = class _ChannelDriver {
4188
4736
  return {
4189
4737
  appliedFiles: this.appliedFileCount,
4190
4738
  inFlight: this.syncingFiles,
4191
- claudeCredentialApplies: this.claudeCredentialApplyCount
4739
+ claudeCredentialApplies: this.claudeCredentialApplyCount,
4740
+ opencodeAuthApplyCount: this.opencodeAuthApplyCount
4192
4741
  };
4193
4742
  }
4194
4743
  /**
@@ -5276,6 +5825,7 @@ var ChannelDriver = class _ChannelDriver {
5276
5825
  deliveryDeadlineAnchored: false,
5277
5826
  b2PinnedSinceMs: 0,
5278
5827
  b2LastDescendantCheckMs: 0,
5828
+ b2RootOngoingHeldLogged: false,
5279
5829
  b2AbandonedSignalled: false,
5280
5830
  ambiguousPinnedSinceMs: 0,
5281
5831
  ambiguousResolved: false
@@ -5370,6 +5920,7 @@ var ChannelDriver = class _ChannelDriver {
5370
5920
  deliveryDeadlineAnchored: false,
5371
5921
  b2PinnedSinceMs: 0,
5372
5922
  b2LastDescendantCheckMs: 0,
5923
+ b2RootOngoingHeldLogged: false,
5373
5924
  b2AbandonedSignalled: false,
5374
5925
  ambiguousPinnedSinceMs: 0,
5375
5926
  ambiguousResolved: false
@@ -5723,6 +6274,7 @@ var ChannelDriver = class _ChannelDriver {
5723
6274
  if (snapshotReadable) {
5724
6275
  inFlight.b2PinnedSinceMs = 0;
5725
6276
  inFlight.b2LastDescendantCheckMs = 0;
6277
+ inFlight.b2RootOngoingHeldLogged = false;
5726
6278
  inFlight.b2AbandonedSignalled = false;
5727
6279
  }
5728
6280
  } else {
@@ -5734,11 +6286,15 @@ var ChannelDriver = class _ChannelDriver {
5734
6286
  const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
5735
6287
  if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
5736
6288
  inFlight.b2LastDescendantCheckMs = this.now();
5737
- const descendantOngoing = await this.isAnyDescendantSessionOngoing(sessionId);
6289
+ const [descendantOngoing, rootOngoing] = await Promise.all([
6290
+ this.isAnyDescendantSessionOngoing(sessionId),
6291
+ isSessionOngoing(this.port, sessionId)
6292
+ ]);
5738
6293
  if (isB2AbandonmentConfirmed({
5739
6294
  pinnedForMs,
5740
6295
  minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
5741
- descendantOngoing
6296
+ descendantOngoing,
6297
+ rootOngoing
5742
6298
  })) {
5743
6299
  inFlight.b2AbandonedSignalled = true;
5744
6300
  this.log({
@@ -5747,12 +6303,26 @@ var ChannelDriver = class _ChannelDriver {
5747
6303
  conversation_id: conv.id,
5748
6304
  message_id: id
5749
6305
  });
6306
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
5750
6307
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
5751
- 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
5752
6313
  });
5753
6314
  await this.settleMessageDone(sessionId, watcher, inFlight, messages);
5754
6315
  return;
5755
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
+ }
5756
6326
  }
5757
6327
  }
5758
6328
  const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
@@ -7495,6 +8065,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
7495
8065
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
7496
8066
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
7497
8067
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
8068
+ var CHILD_STOP_TIMEOUT_MS = 1e4;
7498
8069
  function resolveLogLevel(options) {
7499
8070
  const accepted = Object.keys(LOG_LEVELS);
7500
8071
  const validate = (value, source) => {
@@ -7525,7 +8096,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
7525
8096
  if (trimmed === "") {
7526
8097
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
7527
8098
  }
7528
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join5(homeDir, trimmed.slice(2)) : trimmed;
8099
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join7(homeDir, trimmed.slice(2)) : trimmed;
7529
8100
  if (!isAbsolute2(expanded)) {
7530
8101
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
7531
8102
  }
@@ -7623,7 +8194,7 @@ function logActivity(state, entry) {
7623
8194
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
7624
8195
  if (!meetsThreshold(state, level)) return;
7625
8196
  forwardRunnerActivity(
7626
- { level, message: entry.message, error: entry.error },
8197
+ { level, message: entry.message, error: entry.error, metadata: entry.metadata },
7627
8198
  { agentId: state.agentId, authHeader: state.authHeader }
7628
8199
  );
7629
8200
  const fullEntry = {
@@ -7643,6 +8214,26 @@ function logActivity(state, entry) {
7643
8214
  }
7644
8215
  }
7645
8216
  }
8217
+ function reportSessionDbRecovery(state) {
8218
+ try {
8219
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir4(), env: process.env });
8220
+ for (const record of report.records) {
8221
+ const activity = buildSessionDbRecoveryActivity(record);
8222
+ if (!activity) throw new Error("could not map session-DB recovery record");
8223
+ logActivity(state, {
8224
+ type: activity.level === "error" ? "error" : "info",
8225
+ level: activity.level,
8226
+ ...activity.level === "error" ? { error: activity.message } : { message: activity.message },
8227
+ metadata: activity.metadata
8228
+ });
8229
+ }
8230
+ acknowledgeSessionDbRecoveryReport(report.path);
8231
+ } catch (error2) {
8232
+ console.error(
8233
+ `[run] could not report session-DB recovery activity: ${error2 instanceof Error ? error2.message : String(error2)}`
8234
+ );
8235
+ }
8236
+ }
7646
8237
  function displayStatus(state) {
7647
8238
  if (!state.interactive) return;
7648
8239
  const attempt = state.connection?.reconnectAttempt ?? 0;
@@ -7733,6 +8324,7 @@ async function driveChannels(state, driver) {
7733
8324
  let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7734
8325
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
7735
8326
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
8327
+ let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
7736
8328
  while (state.running) {
7737
8329
  const cycleStartedAtMs = performance.now();
7738
8330
  let idleThisCycle = false;
@@ -7765,6 +8357,10 @@ async function driveChannels(state, driver) {
7765
8357
  const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
7766
8358
  lastSeenClaudeApplies = claudeCredentialApplies;
7767
8359
  if (claudeCredentialApplied) state.claudeUsageRearm?.();
8360
+ const opencodeAuthApplies = fileActivitySnapshot.opencodeAuthApplyCount;
8361
+ const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
8362
+ lastSeenOpencodeAuthApplies = opencodeAuthApplies;
8363
+ if (opencodeAuthApplied) state.openaiUsageRearm?.();
7768
8364
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
7769
8365
  idlePolls = 0;
7770
8366
  idleMs = 0;
@@ -7833,7 +8429,7 @@ async function driveChannels(state, driver) {
7833
8429
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
7834
8430
  var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
7835
8431
  function sessionDbPath() {
7836
- return join5(homedir3(), ".local", "share", "opencode", "opencode.db");
8432
+ return join7(homedir4(), ".local", "share", "opencode", "opencode.db");
7837
8433
  }
7838
8434
  async function runSweep(state, driver, config) {
7839
8435
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
@@ -7916,7 +8512,7 @@ function scheduleSessionCleanup(state, driver, options) {
7916
8512
  for (const warning2 of config.warnings) {
7917
8513
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
7918
8514
  }
7919
- const dbBytes = statSessionDbBytes(homedir3());
8515
+ const dbBytes = statSessionDbBytes(homedir4());
7920
8516
  void (async () => {
7921
8517
  const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
7922
8518
  const sizeWarning = buildSessionStoreSizeWarning({
@@ -7944,23 +8540,20 @@ function scheduleSessionCleanup(state, driver, options) {
7944
8540
  );
7945
8541
  state.sessionCleanupTimers.push(interval, firstSweep);
7946
8542
  }
7947
- function scheduleClaudeUsageReporting(state, options) {
7948
- const { mode, warnings } = resolveClaudeUsageReportingMode(
7949
- options.claudeUsageReporting,
7950
- process.env
7951
- );
8543
+ function scheduleUsageReporting(state, params) {
8544
+ const { mode, warnings } = params.resolved;
7952
8545
  for (const warning2 of warnings) {
7953
8546
  logActivity(state, {
7954
8547
  type: "info",
7955
8548
  level: "warn",
7956
- message: `Claude usage reporting: ${warning2}`
8549
+ message: `${params.label} usage reporting: ${warning2}`
7957
8550
  });
7958
8551
  }
7959
8552
  if (mode === "off") {
7960
8553
  logActivity(state, {
7961
8554
  type: "info",
7962
8555
  level: "debug",
7963
- message: "Claude usage reporting is off (--claude-usage-reporting off)"
8556
+ message: `${params.label} usage reporting is off (${params.offFlagHint})`
7964
8557
  });
7965
8558
  return null;
7966
8559
  }
@@ -7969,7 +8562,7 @@ function scheduleClaudeUsageReporting(state, options) {
7969
8562
  let rearmRequested = false;
7970
8563
  const armProbe = () => {
7971
8564
  phase = "probe-pending";
7972
- state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
8565
+ params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
7973
8566
  };
7974
8567
  const scheduleNextTick = () => {
7975
8568
  if (rearmRequested) {
@@ -7978,7 +8571,7 @@ function scheduleClaudeUsageReporting(state, options) {
7978
8571
  return;
7979
8572
  }
7980
8573
  phase = "steady-pending";
7981
- state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
8574
+ params.setTimer(setTimeout(() => void tick(false), params.nextDelayMs()));
7982
8575
  };
7983
8576
  const rearm = () => {
7984
8577
  switch (phase) {
@@ -7988,9 +8581,9 @@ function scheduleClaudeUsageReporting(state, options) {
7988
8581
  case "probe-pending":
7989
8582
  return;
7990
8583
  case "steady-pending":
7991
- if (state.claudeUsageTimer) {
7992
- clearTimeout(state.claudeUsageTimer);
7993
- state.claudeUsageTimer = null;
8584
+ if (params.getTimer()) {
8585
+ clearTimeout(params.getTimer());
8586
+ params.setTimer(null);
7994
8587
  }
7995
8588
  rearmRequested = false;
7996
8589
  armProbe();
@@ -8004,45 +8597,45 @@ function scheduleClaudeUsageReporting(state, options) {
8004
8597
  const tick = async (isProbe) => {
8005
8598
  phase = "tick-in-flight";
8006
8599
  try {
8007
- const usage = await getClaudeUsage();
8008
- const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
8600
+ const usage = await params.fetchUsage();
8601
+ const result = await params.report(usage);
8009
8602
  if (result.ok) {
8010
8603
  if (consecutiveFailures > 0) {
8011
8604
  logActivity(state, {
8012
8605
  type: "info",
8013
8606
  level: "info",
8014
- message: "Claude usage reporting recovered"
8607
+ message: `${params.label} usage reporting recovered`
8015
8608
  });
8016
8609
  }
8017
8610
  consecutiveFailures = 0;
8018
8611
  logActivity(state, {
8019
8612
  type: "info",
8020
8613
  level: "debug",
8021
- message: "Reported Claude usage to Evident"
8614
+ message: `Reported ${params.label} usage to Evident`
8022
8615
  });
8023
8616
  } else {
8024
8617
  consecutiveFailures++;
8025
8618
  logActivity(state, {
8026
8619
  type: "info",
8027
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8028
- message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8620
+ level: params.failureLogLevel(consecutiveFailures),
8621
+ message: `Failed to report ${params.label} usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8029
8622
  });
8030
8623
  }
8031
8624
  scheduleNextTick();
8032
8625
  } catch (error2) {
8033
- if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
8626
+ if (params.isLocalCredentialProblem(error2)) {
8034
8627
  if (mode === "on") {
8035
8628
  logActivity(state, {
8036
8629
  type: "info",
8037
8630
  level: "warn",
8038
- message: "Claude usage reporting is forced on but no usable Claude Code login was found \u2014 run `claude` to sign in; reporting will keep retrying"
8631
+ message: `${params.label} usage reporting is forced on but no usable login was found \u2014 ${params.forcedOnHint}; reporting will keep retrying`
8039
8632
  });
8040
8633
  scheduleNextTick();
8041
8634
  } else if (isProbe) {
8042
8635
  logActivity(state, {
8043
8636
  type: "info",
8044
8637
  level: "debug",
8045
- message: `Claude usage reporting: ${error2.message}`
8638
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8046
8639
  });
8047
8640
  phase = "dormant";
8048
8641
  if (rearmRequested) rearm();
@@ -8050,7 +8643,7 @@ function scheduleClaudeUsageReporting(state, options) {
8050
8643
  logActivity(state, {
8051
8644
  type: "info",
8052
8645
  level: "debug",
8053
- message: `Claude usage reporting: ${error2.message}`
8646
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8054
8647
  });
8055
8648
  scheduleNextTick();
8056
8649
  }
@@ -8059,8 +8652,8 @@ function scheduleClaudeUsageReporting(state, options) {
8059
8652
  const message = error2 instanceof Error ? error2.message : String(error2);
8060
8653
  logActivity(state, {
8061
8654
  type: "info",
8062
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8063
- message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8655
+ level: params.failureLogLevel(consecutiveFailures),
8656
+ message: `${params.label} usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8064
8657
  });
8065
8658
  scheduleNextTick();
8066
8659
  }
@@ -8069,6 +8662,34 @@ function scheduleClaudeUsageReporting(state, options) {
8069
8662
  armProbe();
8070
8663
  return rearm;
8071
8664
  }
8665
+ function scheduleClaudeUsageReporting(state, options) {
8666
+ return scheduleUsageReporting(state, {
8667
+ label: "Claude",
8668
+ resolved: resolveClaudeUsageReportingMode(options.claudeUsageReporting, process.env),
8669
+ offFlagHint: "--claude-usage-reporting off",
8670
+ getTimer: () => state.claudeUsageTimer,
8671
+ setTimer: (timer) => {
8672
+ state.claudeUsageTimer = timer;
8673
+ },
8674
+ fetchUsage: async () => {
8675
+ const usage = await getClaudeUsage();
8676
+ if (usage.ownerLookupError) {
8677
+ logActivity(state, {
8678
+ type: "info",
8679
+ level: "debug",
8680
+ message: `Claude usage owner lookup failed: ${usage.ownerLookupError}`
8681
+ });
8682
+ }
8683
+ return usage;
8684
+ },
8685
+ report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
8686
+ isLocalCredentialProblem,
8687
+ forcedOnHint: "run `claude` to sign in",
8688
+ firstDelayMs: () => FIRST_REPORT_DELAY_MS,
8689
+ nextDelayMs: nextReportDelayMs,
8690
+ failureLogLevel: claudeUsageFailureLogLevel
8691
+ });
8692
+ }
8072
8693
  var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
8073
8694
  var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
8074
8695
  var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
@@ -8092,7 +8713,7 @@ function scheduleResourceUsageReporting(state, options) {
8092
8713
  });
8093
8714
  return;
8094
8715
  }
8095
- const collect = createResourceUsageCollector(homedir3());
8716
+ const collect = createResourceUsageCollector(homedir4());
8096
8717
  let consecutiveFailures = 0;
8097
8718
  const tick = async () => {
8098
8719
  try {
@@ -8193,6 +8814,11 @@ async function cleanup(state, opts = {}) {
8193
8814
  state.claudeUsageTimer = null;
8194
8815
  }
8195
8816
  state.claudeUsageRearm = null;
8817
+ if (state.openaiUsageTimer) {
8818
+ clearTimeout(state.openaiUsageTimer);
8819
+ state.openaiUsageTimer = null;
8820
+ }
8821
+ state.openaiUsageRearm = null;
8196
8822
  if (state.resourceUsageTimer) {
8197
8823
  clearTimeout(state.resourceUsageTimer);
8198
8824
  state.resourceUsageTimer = null;
@@ -8227,15 +8853,31 @@ async function cleanup(state, opts = {}) {
8227
8853
  }
8228
8854
  if (state.opencodeProcess) {
8229
8855
  const opencodeProcess = state.opencodeProcess;
8230
- 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
+ );
8231
8862
  if (state.interactive) {
8232
- logActivity(state, { type: "info", message: "Stopped OpenCode process" });
8863
+ logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
8233
8864
  displayStatus(state);
8234
8865
  } else {
8235
- log2(state, "Stopped OpenCode process");
8866
+ log2(state, `Stopped OpenCode process (${result.outcome})`);
8236
8867
  }
8237
8868
  state.opencodeProcess = null;
8238
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
+ }
8239
8881
  return durations;
8240
8882
  }
8241
8883
  async function run(options) {
@@ -8244,7 +8886,7 @@ async function run(options) {
8244
8886
  let fileSyncDirectories;
8245
8887
  try {
8246
8888
  logLevel = resolveLogLevel(options);
8247
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir3());
8889
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir4());
8248
8890
  } catch (error2) {
8249
8891
  const message = error2 instanceof Error ? error2.message : String(error2);
8250
8892
  if (options.json) {
@@ -8269,6 +8911,7 @@ async function run(options) {
8269
8911
  opencodeConnected: false,
8270
8912
  opencodeVersion: null,
8271
8913
  opencodeProcess: null,
8914
+ litestreamProcess: null,
8272
8915
  connection: null,
8273
8916
  channelDriver: null,
8274
8917
  running: true,
@@ -8279,6 +8922,8 @@ async function run(options) {
8279
8922
  sessionCleanupTimers: [],
8280
8923
  claudeUsageTimer: null,
8281
8924
  claudeUsageRearm: null,
8925
+ openaiUsageTimer: null,
8926
+ openaiUsageRearm: null,
8282
8927
  resourceUsageTimer: null,
8283
8928
  authHeader: ""
8284
8929
  };
@@ -8474,6 +9119,7 @@ async function run(options) {
8474
9119
  } else {
8475
9120
  log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
8476
9121
  }
9122
+ reportSessionDbRecovery(state);
8477
9123
  const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } = resolveOpenCodeStartTimeoutMs(options, process.env);
8478
9124
  for (const warning2 of opencodeStartTimeoutWarnings) {
8479
9125
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -8530,6 +9176,40 @@ async function run(options) {
8530
9176
  ocSpinner?.fail(error2.message);
8531
9177
  throw error2;
8532
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
+ }
8533
9213
  const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
8534
9214
  const channelDriver = new ChannelDriver({
8535
9215
  agentId: state.agentId,
@@ -8541,7 +9221,7 @@ async function run(options) {
8541
9221
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
8542
9222
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
8543
9223
  fileSyncDirectories,
8544
- homeDir: homedir3(),
9224
+ homeDir: homedir4(),
8545
9225
  maxActiveSessions,
8546
9226
  log: (entry) => (
8547
9227
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -8679,6 +9359,22 @@ async function run(options) {
8679
9359
  }
8680
9360
  scheduleSessionCleanup(state, channelDriver, options);
8681
9361
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
9362
+ state.openaiUsageRearm = scheduleUsageReporting(state, {
9363
+ label: "OpenAI",
9364
+ resolved: resolveOpenAiUsageReportingMode(options.openaiUsageReporting, process.env),
9365
+ offFlagHint: "--openai-usage-reporting off",
9366
+ getTimer: () => state.openaiUsageTimer,
9367
+ setTimer: (timer) => {
9368
+ state.openaiUsageTimer = timer;
9369
+ },
9370
+ fetchUsage: () => getOpenAiUsage(state.port),
9371
+ report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
9372
+ isLocalCredentialProblem: isLocalCredentialProblem2,
9373
+ forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
9374
+ firstDelayMs: firstReportDelayMs,
9375
+ nextDelayMs: usageReportDelayMs,
9376
+ failureLogLevel: usageReportFailureLogLevel
9377
+ });
8682
9378
  scheduleResourceUsageReporting(state, options);
8683
9379
  if (!interactive || state.json) {
8684
9380
  log2(state, "Driving channel messages...");
@@ -8760,6 +9456,9 @@ program.command("run").description("Connect to Evident and process messages").op
8760
9456
  ).option(
8761
9457
  "--claude-usage-reporting <mode>",
8762
9458
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
9459
+ ).option(
9460
+ "--openai-usage-reporting <mode>",
9461
+ "Report OpenAI plan usage to Evident: auto | on | off (default: auto). Env: EVIDENT_OPENAI_USAGE_REPORTING"
8763
9462
  ).option(
8764
9463
  "--no-resource-usage-reporting",
8765
9464
  "Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off"
@@ -8771,6 +9470,9 @@ program.command("run").description("Connect to Evident and process messages").op
8771
9470
  ).option(
8772
9471
  "--tunnel-ready-file <path>",
8773
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."
8774
9476
  ).action(
8775
9477
  (options) => {
8776
9478
  run({
@@ -8795,13 +9497,15 @@ program.command("run").description("Connect to Evident and process messages").op
8795
9497
  // Raw string — the resolver in run.ts single-sources parsing
8796
9498
  // (resolveClaudeUsageReportingMode).
8797
9499
  claudeUsageReporting: options.claudeUsageReporting,
9500
+ openaiUsageReporting: options.openaiUsageReporting,
8798
9501
  // Raw value — resolution is single-sourced in run.ts's
8799
9502
  // resolveResourceUsageReportingEnabled.
8800
9503
  resourceUsageReporting: options.resourceUsageReporting,
8801
9504
  // Raw values — expansion/validation is single-sourced in run.ts's
8802
9505
  // resolveFileSyncDirectories.
8803
9506
  enableFileSyncTo: options.enableFileSyncTo,
8804
- tunnelReadyFile: options.tunnelReadyFile
9507
+ tunnelReadyFile: options.tunnelReadyFile,
9508
+ litestreamConfig: options.litestreamConfig
8805
9509
  });
8806
9510
  }
8807
9511
  );