@evident-ai/cli 3.4.0 → 3.4.1-dev.1549524

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
@@ -2196,7 +2504,7 @@ function isPreamblePinnedRunning(messages, userMessageId) {
2196
2504
  return completedOf(reply) != null && finishOf(reply) === "tool-calls";
2197
2505
  }
2198
2506
  function isB2AbandonmentConfirmed(params) {
2199
- return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
2507
+ return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
2200
2508
  }
2201
2509
  function isAmbiguousTerminalFinish(m) {
2202
2510
  if (completedOf(m) == null) return false;
@@ -2209,7 +2517,7 @@ function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
2209
2517
  return isAmbiguousTerminalFinish(reply);
2210
2518
  }
2211
2519
  function isAmbiguousFinishResolved(params) {
2212
- return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
2520
+ return params.sessionOngoing === false || params.sessionOngoing !== true && params.pinnedForMs >= params.maxPinnedMs;
2213
2521
  }
2214
2522
  function messageError(messages, userMessageId) {
2215
2523
  const reply = findLastAssistantReplyFor(messages, userMessageId);
@@ -2419,10 +2727,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
2419
2727
 
2420
2728
  // src/lib/opencode/session-db-size.ts
2421
2729
  import { statSync as statSync2 } from "fs";
2422
- import { join as join2 } from "path";
2730
+ import { join as join3 } from "path";
2423
2731
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
2424
2732
  function statSessionDbBytes(homeDir) {
2425
- const dbPath = join2(homeDir, ".local", "share", "opencode", "opencode.db");
2733
+ const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
2426
2734
  try {
2427
2735
  return statSync2(dbPath).size;
2428
2736
  } catch (err) {
@@ -2991,6 +3299,193 @@ function writeTunnelReadyMarker(path, agentId) {
2991
3299
  }
2992
3300
  }
2993
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
+
3318
+ // src/lib/openai-usage.ts
3319
+ import { readFileSync as readFileSync3 } from "fs";
3320
+ import { homedir as homedir2 } from "os";
3321
+ import { join as join4 } from "path";
3322
+ var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3323
+ var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
3324
+ var OpenAiUsageError = class extends Error {
3325
+ constructor(message, reason) {
3326
+ super(message);
3327
+ this.reason = reason;
3328
+ }
3329
+ };
3330
+ function isLocalCredentialProblem2(err) {
3331
+ return err instanceof OpenAiUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
3332
+ }
3333
+ function readOpenCodeChatGptCredentials() {
3334
+ try {
3335
+ const raw = readFileSync3(join4(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3336
+ let parsed;
3337
+ try {
3338
+ parsed = JSON.parse(raw);
3339
+ } catch {
3340
+ return null;
3341
+ }
3342
+ const entry = parsed.openai;
3343
+ if (entry?.type !== "oauth" || typeof entry.access !== "string" || !entry.access || typeof entry.expires !== "number") {
3344
+ return null;
3345
+ }
3346
+ return { accessToken: entry.access, expiresAt: entry.expires };
3347
+ } catch (err) {
3348
+ const code = err.code;
3349
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
3350
+ console.warn(
3351
+ `readOpenCodeChatGptCredentials: reading auth.json failed (${code ?? "unknown"})`
3352
+ );
3353
+ }
3354
+ return null;
3355
+ }
3356
+ }
3357
+ function toWindow2(headers, name) {
3358
+ const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
3359
+ const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
3360
+ if (utilizationHeader == null || utilizationHeader === "" || windowMinutesHeader == null) {
3361
+ return null;
3362
+ }
3363
+ const utilization = Number(utilizationHeader);
3364
+ const windowMinutes = Number(windowMinutesHeader);
3365
+ if (!Number.isFinite(utilization) || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
3366
+ return null;
3367
+ }
3368
+ const resetAtHeader = headers.get(`x-codex-${name}-reset-at`);
3369
+ const resetSeconds = resetAtHeader == null || resetAtHeader === "" ? NaN : Number(resetAtHeader);
3370
+ const resetsAt = Number.isFinite(resetSeconds) ? new Date(resetSeconds * 1e3).toISOString() : null;
3371
+ return { utilization: Math.min(100, Math.max(0, utilization)), windowMinutes, resetsAt };
3372
+ }
3373
+ function parseCodexUsageHeaders(headers) {
3374
+ return {
3375
+ primary: toWindow2(headers, "primary"),
3376
+ secondary: toWindow2(headers, "secondary"),
3377
+ hasCredits: headers.has("x-codex-credits-has-credits") ? headers.get("x-codex-credits-has-credits")?.toLowerCase() === "true" : null,
3378
+ creditsUnlimited: headers.has("x-codex-credits-unlimited") ? headers.get("x-codex-credits-unlimited")?.toLowerCase() === "true" : null
3379
+ };
3380
+ }
3381
+ function normalizeProbeModel(model) {
3382
+ return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
3383
+ }
3384
+ async function resolveProbeModels(port) {
3385
+ try {
3386
+ const res = await withRequestTimeout(
3387
+ fetch,
3388
+ REQUEST_TIMEOUT_MS
3389
+ )(`${opencodeBase(port)}/config/providers`);
3390
+ if (!res.ok) {
3391
+ console.error(
3392
+ `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
3393
+ );
3394
+ return [];
3395
+ }
3396
+ const body = await res.json();
3397
+ const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
3398
+ if (!provider || !provider.models || typeof provider.models !== "object") return [];
3399
+ const candidates = [
3400
+ ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
3401
+ ...Object.keys(provider.models)
3402
+ ].map(normalizeProbeModel);
3403
+ return [...new Set(candidates)].slice(0, 4);
3404
+ } catch (err) {
3405
+ console.error(
3406
+ `[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3407
+ );
3408
+ return [];
3409
+ }
3410
+ }
3411
+ function hasPrimaryHeaders(headers) {
3412
+ return [
3413
+ "x-codex-primary-used-percent",
3414
+ "x-codex-primary-window-minutes",
3415
+ "x-codex-primary-reset-at"
3416
+ ].some((name) => headers.has(name));
3417
+ }
3418
+ async function getOpenAiUsage(port) {
3419
+ const credentials2 = readOpenCodeChatGptCredentials();
3420
+ if (!credentials2) {
3421
+ throw new OpenAiUsageError(
3422
+ "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3423
+ "no_credentials"
3424
+ );
3425
+ }
3426
+ if (credentials2.expiresAt < Date.now()) {
3427
+ throw new OpenAiUsageError(
3428
+ "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3429
+ "credentials_expired"
3430
+ );
3431
+ }
3432
+ const models = await resolveProbeModels(port);
3433
+ if (models.length === 0) {
3434
+ throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
3435
+ }
3436
+ let lastStatus;
3437
+ for (const model of models) {
3438
+ let res;
3439
+ try {
3440
+ res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
3441
+ method: "POST",
3442
+ headers: {
3443
+ Authorization: `Bearer ${credentials2.accessToken}`,
3444
+ "Content-Type": "application/json"
3445
+ },
3446
+ body: JSON.stringify({ model, store: false, stream: true })
3447
+ });
3448
+ } catch (err) {
3449
+ throw new OpenAiUsageError(
3450
+ `OpenAI usage probe request failed: ${err instanceof Error ? err.message : String(err)}.`,
3451
+ "request_failed"
3452
+ );
3453
+ }
3454
+ try {
3455
+ lastStatus = res.status;
3456
+ if (hasPrimaryHeaders(res.headers)) {
3457
+ const usage = parseCodexUsageHeaders(res.headers);
3458
+ if (!usage.primary && !usage.secondary) {
3459
+ throw new OpenAiUsageError(
3460
+ `OpenAI usage probe returned no usable window (HTTP ${res.status}).`,
3461
+ "no_usable_window"
3462
+ );
3463
+ }
3464
+ return usage;
3465
+ }
3466
+ if (res.status === 401) {
3467
+ throw new OpenAiUsageError(
3468
+ "ChatGPT credentials have expired (HTTP 401).",
3469
+ "credentials_expired"
3470
+ );
3471
+ }
3472
+ if (res.status === 403 || res.status === 429) {
3473
+ throw new OpenAiUsageError(
3474
+ `OpenAI usage probe was blocked (HTTP ${res.status}).`,
3475
+ "probe_blocked"
3476
+ );
3477
+ }
3478
+ } finally {
3479
+ await res.body?.cancel().catch(() => {
3480
+ });
3481
+ }
3482
+ }
3483
+ throw new OpenAiUsageError(
3484
+ `OpenAI usage probe failed: HTTP ${lastStatus ?? "unknown"}.`,
3485
+ "request_failed"
3486
+ );
3487
+ }
3488
+
2994
3489
  // src/lib/reporting-schedule.ts
2995
3490
  function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2996
3491
  const jitterRangeMs = baseMs * jitterFraction;
@@ -2999,6 +3494,31 @@ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2999
3494
  function firstReportDelayMs(random = Math.random) {
3000
3495
  return 5e3 + random() * 1e4;
3001
3496
  }
3497
+ var VALID_USAGE_REPORTING_MODES = ["auto", "on", "off"];
3498
+ function resolveUsageReportingMode(flagValue, env, names) {
3499
+ const raw = flagValue ?? env[names.envVar];
3500
+ if (raw === void 0 || raw === "") return { mode: "auto", warnings: [] };
3501
+ const normalized = raw.trim().toLowerCase();
3502
+ if (VALID_USAGE_REPORTING_MODES.includes(normalized)) {
3503
+ return { mode: normalized, warnings: [] };
3504
+ }
3505
+ const source = flagValue !== void 0 ? names.flagName : names.envVar;
3506
+ return {
3507
+ mode: "auto",
3508
+ warnings: [
3509
+ `Ignoring invalid ${source} "${raw}": expected one of ${VALID_USAGE_REPORTING_MODES.join(", ")}; using auto`
3510
+ ]
3511
+ };
3512
+ }
3513
+ var BASE_USAGE_REPORT_DELAY_MS = 10 * 6e4;
3514
+ var USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
3515
+ function usageReportDelayMs(random = Math.random) {
3516
+ return jitteredDelayMs(BASE_USAGE_REPORT_DELAY_MS, USAGE_REPORT_DELAY_JITTER_FRACTION, random);
3517
+ }
3518
+ var USAGE_REPORT_FAILURE_REESCALATION_TICKS = 6;
3519
+ function usageReportFailureLogLevel(consecutiveFailures) {
3520
+ return reportFailureLogLevel(consecutiveFailures, USAGE_REPORT_FAILURE_REESCALATION_TICKS);
3521
+ }
3002
3522
  function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
3003
3523
  return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
3004
3524
  }
@@ -3007,33 +3527,26 @@ function failureStreakSuffix(consecutiveFailures) {
3007
3527
  }
3008
3528
 
3009
3529
  // src/lib/claude-usage-reporting.ts
3010
- var VALID_MODES = ["auto", "on", "off"];
3011
3530
  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
- };
3531
+ return resolveUsageReportingMode(flagValue, env, {
3532
+ flagName: "--claude-usage-reporting",
3533
+ envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
3534
+ });
3027
3535
  }
3028
- var BASE_REPORT_DELAY_MS = 10 * 6e4;
3029
- var REPORT_DELAY_JITTER_FRACTION = 0.2;
3030
3536
  function nextReportDelayMs(random = Math.random) {
3031
- return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);
3537
+ return usageReportDelayMs(random);
3032
3538
  }
3033
3539
  var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
3034
- var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
3035
3540
  function claudeUsageFailureLogLevel(consecutiveFailures) {
3036
- return reportFailureLogLevel(consecutiveFailures, CLAUDE_USAGE_FAILURE_REESCALATION_TICKS);
3541
+ return usageReportFailureLogLevel(consecutiveFailures);
3542
+ }
3543
+
3544
+ // src/lib/openai-usage-reporting.ts
3545
+ function resolveOpenAiUsageReportingMode(flagValue, env) {
3546
+ return resolveUsageReportingMode(flagValue, env, {
3547
+ flagName: "--openai-usage-reporting",
3548
+ envVar: "EVIDENT_OPENAI_USAGE_REPORTING"
3549
+ });
3037
3550
  }
3038
3551
 
3039
3552
  // src/lib/resource-usage-reporting.ts
@@ -3192,15 +3705,15 @@ function createResourceUsageCollector(homeDir) {
3192
3705
  }
3193
3706
 
3194
3707
  // src/lib/channels/driver.ts
3195
- import { homedir as homedir2 } from "os";
3708
+ import { homedir as homedir3 } from "os";
3196
3709
 
3197
3710
  // src/lib/runner-file-sync.ts
3198
- import { join as join4 } from "path";
3711
+ import { join as join6 } from "path";
3199
3712
 
3200
3713
  // src/lib/file-push.ts
3201
3714
  import { randomUUID } from "crypto";
3202
3715
  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";
3716
+ import { basename, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
3204
3717
  var FILE_MODE = 384;
3205
3718
  var DIRECTORY_MODE = 448;
3206
3719
  async function writePushedFile(request) {
@@ -3233,7 +3746,7 @@ async function writePushedFile(request) {
3233
3746
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
3234
3747
  dirname3(candidate)
3235
3748
  );
3236
- const realTarget = join3(existingAncestor, ...missingSegments, basename(candidate));
3749
+ const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
3237
3750
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
3238
3751
  if (allowedDirectory === null) {
3239
3752
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -3269,7 +3782,7 @@ function expandAndValidate(requestedPath, homeDir) {
3269
3782
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
3270
3783
  return null;
3271
3784
  }
3272
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join3(homeDir, requestedPath.slice(2)) : requestedPath;
3785
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
3273
3786
  if (expanded.split(/[/\\]/).includes("..")) {
3274
3787
  return null;
3275
3788
  }
@@ -3342,13 +3855,13 @@ function contains(realDirectory, realTarget) {
3342
3855
  async function createMissingDirectories(existingAncestor, missingSegments) {
3343
3856
  let current = existingAncestor;
3344
3857
  for (const segment of missingSegments) {
3345
- current = join3(current, segment);
3858
+ current = join5(current, segment);
3346
3859
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
3347
3860
  await chmod(current, DIRECTORY_MODE);
3348
3861
  }
3349
3862
  }
3350
3863
  async function writeAtomically(realTarget, content) {
3351
- const temporaryPath = join3(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3864
+ const temporaryPath = join5(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3352
3865
  let handle;
3353
3866
  try {
3354
3867
  handle = await open2(temporaryPath, "wx", FILE_MODE);
@@ -3390,20 +3903,28 @@ async function syncPendingRunnerFiles(options) {
3390
3903
  for (const id of options.ackFailures.keys()) {
3391
3904
  if (!pendingIds.has(id)) options.ackFailures.delete(id);
3392
3905
  }
3393
- if (pending.length === 0) return { applied: 0, claudeCredentialApplied: false };
3906
+ if (pending.length === 0) {
3907
+ return { applied: 0, claudeCredentialApplied: false, opencodeAuthApplied: false };
3908
+ }
3394
3909
  options.log({
3395
3910
  level: "info",
3396
3911
  message: `Runner file sync: ${pending.length} file(s) queued for this runner`
3397
3912
  });
3398
3913
  let applied = 0;
3399
3914
  let claudeCredentialApplied = false;
3915
+ let opencodeAuthApplied = false;
3400
3916
  for (const file of pending) {
3401
3917
  if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
3402
3918
  const outcome = await applyOne(options, file);
3403
3919
  if (outcome.applied) applied += 1;
3404
3920
  if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
3921
+ if (outcome.opencodeAuthApplied) opencodeAuthApplied = true;
3405
3922
  }
3406
- return { applied, claudeCredentialApplied };
3923
+ return {
3924
+ applied,
3925
+ claudeCredentialApplied,
3926
+ opencodeAuthApplied
3927
+ };
3407
3928
  }
3408
3929
  async function listPendingFiles(options) {
3409
3930
  let res;
@@ -3464,10 +3985,18 @@ function asPendingFile(entry) {
3464
3985
  if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
3465
3986
  return { id, path, size };
3466
3987
  }
3467
- var NOT_APPLIED = { applied: false, claudeCredentialApplied: false };
3988
+ var NOT_APPLIED = {
3989
+ applied: false,
3990
+ claudeCredentialApplied: false,
3991
+ opencodeAuthApplied: false
3992
+ };
3468
3993
  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);
3994
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
3995
+ return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
3996
+ }
3997
+ function isOpenCodeAuthPath(requestedPath, homeDir) {
3998
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
3999
+ return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
3471
4000
  }
3472
4001
  async function applyOne(options, file) {
3473
4002
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -3523,7 +4052,8 @@ async function applyOne(options, file) {
3523
4052
  await ack(options, file, "applied");
3524
4053
  return {
3525
4054
  applied: true,
3526
- claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
4055
+ claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),
4056
+ opencodeAuthApplied: isOpenCodeAuthPath(file.path, options.homeDir)
3527
4057
  };
3528
4058
  }
3529
4059
  function durableDownloadCode(status2) {
@@ -3977,6 +4507,7 @@ var ChannelDriver = class _ChannelDriver {
3977
4507
  * that way rather than "fixing" it into a count.
3978
4508
  */
3979
4509
  claudeCredentialApplyCount = 0;
4510
+ opencodeAuthApplyCount = 0;
3980
4511
  /**
3981
4512
  * The currently-executing `drainPending()` promise, or null when idle. Lets a
3982
4513
  * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
@@ -4011,7 +4542,7 @@ var ChannelDriver = class _ChannelDriver {
4011
4542
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
4012
4543
  this.now = config.now ?? (() => Date.now());
4013
4544
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
4014
- this.homeDir = config.homeDir ?? homedir2();
4545
+ this.homeDir = config.homeDir ?? homedir3();
4015
4546
  this.maxActiveSessions = config.maxActiveSessions;
4016
4547
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
4017
4548
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -4081,6 +4612,7 @@ var ChannelDriver = class _ChannelDriver {
4081
4612
  });
4082
4613
  this.appliedFileCount += result.applied;
4083
4614
  if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
4615
+ if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
4084
4616
  return result.applied;
4085
4617
  } catch (err) {
4086
4618
  this.log({
@@ -4188,7 +4720,8 @@ var ChannelDriver = class _ChannelDriver {
4188
4720
  return {
4189
4721
  appliedFiles: this.appliedFileCount,
4190
4722
  inFlight: this.syncingFiles,
4191
- claudeCredentialApplies: this.claudeCredentialApplyCount
4723
+ claudeCredentialApplies: this.claudeCredentialApplyCount,
4724
+ opencodeAuthApplyCount: this.opencodeAuthApplyCount
4192
4725
  };
4193
4726
  }
4194
4727
  /**
@@ -5276,6 +5809,7 @@ var ChannelDriver = class _ChannelDriver {
5276
5809
  deliveryDeadlineAnchored: false,
5277
5810
  b2PinnedSinceMs: 0,
5278
5811
  b2LastDescendantCheckMs: 0,
5812
+ b2RootOngoingHeldLogged: false,
5279
5813
  b2AbandonedSignalled: false,
5280
5814
  ambiguousPinnedSinceMs: 0,
5281
5815
  ambiguousResolved: false
@@ -5370,6 +5904,7 @@ var ChannelDriver = class _ChannelDriver {
5370
5904
  deliveryDeadlineAnchored: false,
5371
5905
  b2PinnedSinceMs: 0,
5372
5906
  b2LastDescendantCheckMs: 0,
5907
+ b2RootOngoingHeldLogged: false,
5373
5908
  b2AbandonedSignalled: false,
5374
5909
  ambiguousPinnedSinceMs: 0,
5375
5910
  ambiguousResolved: false
@@ -5723,6 +6258,7 @@ var ChannelDriver = class _ChannelDriver {
5723
6258
  if (snapshotReadable) {
5724
6259
  inFlight.b2PinnedSinceMs = 0;
5725
6260
  inFlight.b2LastDescendantCheckMs = 0;
6261
+ inFlight.b2RootOngoingHeldLogged = false;
5726
6262
  inFlight.b2AbandonedSignalled = false;
5727
6263
  }
5728
6264
  } else {
@@ -5734,11 +6270,15 @@ var ChannelDriver = class _ChannelDriver {
5734
6270
  const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
5735
6271
  if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
5736
6272
  inFlight.b2LastDescendantCheckMs = this.now();
5737
- const descendantOngoing = await this.isAnyDescendantSessionOngoing(sessionId);
6273
+ const [descendantOngoing, rootOngoing] = await Promise.all([
6274
+ this.isAnyDescendantSessionOngoing(sessionId),
6275
+ isSessionOngoing(this.port, sessionId)
6276
+ ]);
5738
6277
  if (isB2AbandonmentConfirmed({
5739
6278
  pinnedForMs,
5740
6279
  minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
5741
- descendantOngoing
6280
+ descendantOngoing,
6281
+ rootOngoing
5742
6282
  })) {
5743
6283
  inFlight.b2AbandonedSignalled = true;
5744
6284
  this.log({
@@ -5747,12 +6287,26 @@ var ChannelDriver = class _ChannelDriver {
5747
6287
  conversation_id: conv.id,
5748
6288
  message_id: id
5749
6289
  });
6290
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
5750
6291
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
5751
- 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
5752
6297
  });
5753
6298
  await this.settleMessageDone(sessionId, watcher, inFlight, messages);
5754
6299
  return;
5755
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
+ }
5756
6310
  }
5757
6311
  }
5758
6312
  const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
@@ -7495,6 +8049,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
7495
8049
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
7496
8050
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
7497
8051
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
8052
+ var CHILD_STOP_TIMEOUT_MS = 1e4;
7498
8053
  function resolveLogLevel(options) {
7499
8054
  const accepted = Object.keys(LOG_LEVELS);
7500
8055
  const validate = (value, source) => {
@@ -7525,7 +8080,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
7525
8080
  if (trimmed === "") {
7526
8081
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
7527
8082
  }
7528
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join5(homeDir, trimmed.slice(2)) : trimmed;
8083
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join7(homeDir, trimmed.slice(2)) : trimmed;
7529
8084
  if (!isAbsolute2(expanded)) {
7530
8085
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
7531
8086
  }
@@ -7623,7 +8178,7 @@ function logActivity(state, entry) {
7623
8178
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
7624
8179
  if (!meetsThreshold(state, level)) return;
7625
8180
  forwardRunnerActivity(
7626
- { level, message: entry.message, error: entry.error },
8181
+ { level, message: entry.message, error: entry.error, metadata: entry.metadata },
7627
8182
  { agentId: state.agentId, authHeader: state.authHeader }
7628
8183
  );
7629
8184
  const fullEntry = {
@@ -7643,6 +8198,26 @@ function logActivity(state, entry) {
7643
8198
  }
7644
8199
  }
7645
8200
  }
8201
+ function reportSessionDbRecovery(state) {
8202
+ try {
8203
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir4(), env: process.env });
8204
+ for (const record of report.records) {
8205
+ const activity = buildSessionDbRecoveryActivity(record);
8206
+ if (!activity) throw new Error("could not map session-DB recovery record");
8207
+ logActivity(state, {
8208
+ type: activity.level === "error" ? "error" : "info",
8209
+ level: activity.level,
8210
+ ...activity.level === "error" ? { error: activity.message } : { message: activity.message },
8211
+ metadata: activity.metadata
8212
+ });
8213
+ }
8214
+ acknowledgeSessionDbRecoveryReport(report.path);
8215
+ } catch (error2) {
8216
+ console.error(
8217
+ `[run] could not report session-DB recovery activity: ${error2 instanceof Error ? error2.message : String(error2)}`
8218
+ );
8219
+ }
8220
+ }
7646
8221
  function displayStatus(state) {
7647
8222
  if (!state.interactive) return;
7648
8223
  const attempt = state.connection?.reconnectAttempt ?? 0;
@@ -7733,6 +8308,7 @@ async function driveChannels(state, driver) {
7733
8308
  let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7734
8309
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
7735
8310
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
8311
+ let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
7736
8312
  while (state.running) {
7737
8313
  const cycleStartedAtMs = performance.now();
7738
8314
  let idleThisCycle = false;
@@ -7765,6 +8341,10 @@ async function driveChannels(state, driver) {
7765
8341
  const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
7766
8342
  lastSeenClaudeApplies = claudeCredentialApplies;
7767
8343
  if (claudeCredentialApplied) state.claudeUsageRearm?.();
8344
+ const opencodeAuthApplies = fileActivitySnapshot.opencodeAuthApplyCount;
8345
+ const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
8346
+ lastSeenOpencodeAuthApplies = opencodeAuthApplies;
8347
+ if (opencodeAuthApplied) state.openaiUsageRearm?.();
7768
8348
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
7769
8349
  idlePolls = 0;
7770
8350
  idleMs = 0;
@@ -7833,7 +8413,7 @@ async function driveChannels(state, driver) {
7833
8413
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
7834
8414
  var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
7835
8415
  function sessionDbPath() {
7836
- return join5(homedir3(), ".local", "share", "opencode", "opencode.db");
8416
+ return join7(homedir4(), ".local", "share", "opencode", "opencode.db");
7837
8417
  }
7838
8418
  async function runSweep(state, driver, config) {
7839
8419
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
@@ -7916,7 +8496,7 @@ function scheduleSessionCleanup(state, driver, options) {
7916
8496
  for (const warning2 of config.warnings) {
7917
8497
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
7918
8498
  }
7919
- const dbBytes = statSessionDbBytes(homedir3());
8499
+ const dbBytes = statSessionDbBytes(homedir4());
7920
8500
  void (async () => {
7921
8501
  const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
7922
8502
  const sizeWarning = buildSessionStoreSizeWarning({
@@ -7944,23 +8524,20 @@ function scheduleSessionCleanup(state, driver, options) {
7944
8524
  );
7945
8525
  state.sessionCleanupTimers.push(interval, firstSweep);
7946
8526
  }
7947
- function scheduleClaudeUsageReporting(state, options) {
7948
- const { mode, warnings } = resolveClaudeUsageReportingMode(
7949
- options.claudeUsageReporting,
7950
- process.env
7951
- );
8527
+ function scheduleUsageReporting(state, params) {
8528
+ const { mode, warnings } = params.resolved;
7952
8529
  for (const warning2 of warnings) {
7953
8530
  logActivity(state, {
7954
8531
  type: "info",
7955
8532
  level: "warn",
7956
- message: `Claude usage reporting: ${warning2}`
8533
+ message: `${params.label} usage reporting: ${warning2}`
7957
8534
  });
7958
8535
  }
7959
8536
  if (mode === "off") {
7960
8537
  logActivity(state, {
7961
8538
  type: "info",
7962
8539
  level: "debug",
7963
- message: "Claude usage reporting is off (--claude-usage-reporting off)"
8540
+ message: `${params.label} usage reporting is off (${params.offFlagHint})`
7964
8541
  });
7965
8542
  return null;
7966
8543
  }
@@ -7969,7 +8546,7 @@ function scheduleClaudeUsageReporting(state, options) {
7969
8546
  let rearmRequested = false;
7970
8547
  const armProbe = () => {
7971
8548
  phase = "probe-pending";
7972
- state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
8549
+ params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
7973
8550
  };
7974
8551
  const scheduleNextTick = () => {
7975
8552
  if (rearmRequested) {
@@ -7978,7 +8555,7 @@ function scheduleClaudeUsageReporting(state, options) {
7978
8555
  return;
7979
8556
  }
7980
8557
  phase = "steady-pending";
7981
- state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
8558
+ params.setTimer(setTimeout(() => void tick(false), params.nextDelayMs()));
7982
8559
  };
7983
8560
  const rearm = () => {
7984
8561
  switch (phase) {
@@ -7988,9 +8565,9 @@ function scheduleClaudeUsageReporting(state, options) {
7988
8565
  case "probe-pending":
7989
8566
  return;
7990
8567
  case "steady-pending":
7991
- if (state.claudeUsageTimer) {
7992
- clearTimeout(state.claudeUsageTimer);
7993
- state.claudeUsageTimer = null;
8568
+ if (params.getTimer()) {
8569
+ clearTimeout(params.getTimer());
8570
+ params.setTimer(null);
7994
8571
  }
7995
8572
  rearmRequested = false;
7996
8573
  armProbe();
@@ -8004,45 +8581,45 @@ function scheduleClaudeUsageReporting(state, options) {
8004
8581
  const tick = async (isProbe) => {
8005
8582
  phase = "tick-in-flight";
8006
8583
  try {
8007
- const usage = await getClaudeUsage();
8008
- const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
8584
+ const usage = await params.fetchUsage();
8585
+ const result = await params.report(usage);
8009
8586
  if (result.ok) {
8010
8587
  if (consecutiveFailures > 0) {
8011
8588
  logActivity(state, {
8012
8589
  type: "info",
8013
8590
  level: "info",
8014
- message: "Claude usage reporting recovered"
8591
+ message: `${params.label} usage reporting recovered`
8015
8592
  });
8016
8593
  }
8017
8594
  consecutiveFailures = 0;
8018
8595
  logActivity(state, {
8019
8596
  type: "info",
8020
8597
  level: "debug",
8021
- message: "Reported Claude usage to Evident"
8598
+ message: `Reported ${params.label} usage to Evident`
8022
8599
  });
8023
8600
  } else {
8024
8601
  consecutiveFailures++;
8025
8602
  logActivity(state, {
8026
8603
  type: "info",
8027
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8028
- message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8604
+ level: params.failureLogLevel(consecutiveFailures),
8605
+ message: `Failed to report ${params.label} usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8029
8606
  });
8030
8607
  }
8031
8608
  scheduleNextTick();
8032
8609
  } catch (error2) {
8033
- if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
8610
+ if (params.isLocalCredentialProblem(error2)) {
8034
8611
  if (mode === "on") {
8035
8612
  logActivity(state, {
8036
8613
  type: "info",
8037
8614
  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"
8615
+ message: `${params.label} usage reporting is forced on but no usable login was found \u2014 ${params.forcedOnHint}; reporting will keep retrying`
8039
8616
  });
8040
8617
  scheduleNextTick();
8041
8618
  } else if (isProbe) {
8042
8619
  logActivity(state, {
8043
8620
  type: "info",
8044
8621
  level: "debug",
8045
- message: `Claude usage reporting: ${error2.message}`
8622
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8046
8623
  });
8047
8624
  phase = "dormant";
8048
8625
  if (rearmRequested) rearm();
@@ -8050,7 +8627,7 @@ function scheduleClaudeUsageReporting(state, options) {
8050
8627
  logActivity(state, {
8051
8628
  type: "info",
8052
8629
  level: "debug",
8053
- message: `Claude usage reporting: ${error2.message}`
8630
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8054
8631
  });
8055
8632
  scheduleNextTick();
8056
8633
  }
@@ -8059,8 +8636,8 @@ function scheduleClaudeUsageReporting(state, options) {
8059
8636
  const message = error2 instanceof Error ? error2.message : String(error2);
8060
8637
  logActivity(state, {
8061
8638
  type: "info",
8062
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8063
- message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8639
+ level: params.failureLogLevel(consecutiveFailures),
8640
+ message: `${params.label} usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8064
8641
  });
8065
8642
  scheduleNextTick();
8066
8643
  }
@@ -8069,6 +8646,34 @@ function scheduleClaudeUsageReporting(state, options) {
8069
8646
  armProbe();
8070
8647
  return rearm;
8071
8648
  }
8649
+ function scheduleClaudeUsageReporting(state, options) {
8650
+ return scheduleUsageReporting(state, {
8651
+ label: "Claude",
8652
+ resolved: resolveClaudeUsageReportingMode(options.claudeUsageReporting, process.env),
8653
+ offFlagHint: "--claude-usage-reporting off",
8654
+ getTimer: () => state.claudeUsageTimer,
8655
+ setTimer: (timer) => {
8656
+ state.claudeUsageTimer = timer;
8657
+ },
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
+ },
8669
+ report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
8670
+ isLocalCredentialProblem,
8671
+ forcedOnHint: "run `claude` to sign in",
8672
+ firstDelayMs: () => FIRST_REPORT_DELAY_MS,
8673
+ nextDelayMs: nextReportDelayMs,
8674
+ failureLogLevel: claudeUsageFailureLogLevel
8675
+ });
8676
+ }
8072
8677
  var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
8073
8678
  var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
8074
8679
  var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
@@ -8092,7 +8697,7 @@ function scheduleResourceUsageReporting(state, options) {
8092
8697
  });
8093
8698
  return;
8094
8699
  }
8095
- const collect = createResourceUsageCollector(homedir3());
8700
+ const collect = createResourceUsageCollector(homedir4());
8096
8701
  let consecutiveFailures = 0;
8097
8702
  const tick = async () => {
8098
8703
  try {
@@ -8193,6 +8798,11 @@ async function cleanup(state, opts = {}) {
8193
8798
  state.claudeUsageTimer = null;
8194
8799
  }
8195
8800
  state.claudeUsageRearm = null;
8801
+ if (state.openaiUsageTimer) {
8802
+ clearTimeout(state.openaiUsageTimer);
8803
+ state.openaiUsageTimer = null;
8804
+ }
8805
+ state.openaiUsageRearm = null;
8196
8806
  if (state.resourceUsageTimer) {
8197
8807
  clearTimeout(state.resourceUsageTimer);
8198
8808
  state.resourceUsageTimer = null;
@@ -8227,15 +8837,31 @@ async function cleanup(state, opts = {}) {
8227
8837
  }
8228
8838
  if (state.opencodeProcess) {
8229
8839
  const opencodeProcess = state.opencodeProcess;
8230
- 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
+ );
8231
8846
  if (state.interactive) {
8232
- logActivity(state, { type: "info", message: "Stopped OpenCode process" });
8847
+ logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
8233
8848
  displayStatus(state);
8234
8849
  } else {
8235
- log2(state, "Stopped OpenCode process");
8850
+ log2(state, `Stopped OpenCode process (${result.outcome})`);
8236
8851
  }
8237
8852
  state.opencodeProcess = null;
8238
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
+ }
8239
8865
  return durations;
8240
8866
  }
8241
8867
  async function run(options) {
@@ -8244,7 +8870,7 @@ async function run(options) {
8244
8870
  let fileSyncDirectories;
8245
8871
  try {
8246
8872
  logLevel = resolveLogLevel(options);
8247
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir3());
8873
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir4());
8248
8874
  } catch (error2) {
8249
8875
  const message = error2 instanceof Error ? error2.message : String(error2);
8250
8876
  if (options.json) {
@@ -8269,6 +8895,7 @@ async function run(options) {
8269
8895
  opencodeConnected: false,
8270
8896
  opencodeVersion: null,
8271
8897
  opencodeProcess: null,
8898
+ litestreamProcess: null,
8272
8899
  connection: null,
8273
8900
  channelDriver: null,
8274
8901
  running: true,
@@ -8279,6 +8906,8 @@ async function run(options) {
8279
8906
  sessionCleanupTimers: [],
8280
8907
  claudeUsageTimer: null,
8281
8908
  claudeUsageRearm: null,
8909
+ openaiUsageTimer: null,
8910
+ openaiUsageRearm: null,
8282
8911
  resourceUsageTimer: null,
8283
8912
  authHeader: ""
8284
8913
  };
@@ -8474,6 +9103,7 @@ async function run(options) {
8474
9103
  } else {
8475
9104
  log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
8476
9105
  }
9106
+ reportSessionDbRecovery(state);
8477
9107
  const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } = resolveOpenCodeStartTimeoutMs(options, process.env);
8478
9108
  for (const warning2 of opencodeStartTimeoutWarnings) {
8479
9109
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -8530,6 +9160,40 @@ async function run(options) {
8530
9160
  ocSpinner?.fail(error2.message);
8531
9161
  throw error2;
8532
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
+ }
8533
9197
  const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
8534
9198
  const channelDriver = new ChannelDriver({
8535
9199
  agentId: state.agentId,
@@ -8541,7 +9205,7 @@ async function run(options) {
8541
9205
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
8542
9206
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
8543
9207
  fileSyncDirectories,
8544
- homeDir: homedir3(),
9208
+ homeDir: homedir4(),
8545
9209
  maxActiveSessions,
8546
9210
  log: (entry) => (
8547
9211
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -8679,6 +9343,22 @@ async function run(options) {
8679
9343
  }
8680
9344
  scheduleSessionCleanup(state, channelDriver, options);
8681
9345
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
9346
+ state.openaiUsageRearm = scheduleUsageReporting(state, {
9347
+ label: "OpenAI",
9348
+ resolved: resolveOpenAiUsageReportingMode(options.openaiUsageReporting, process.env),
9349
+ offFlagHint: "--openai-usage-reporting off",
9350
+ getTimer: () => state.openaiUsageTimer,
9351
+ setTimer: (timer) => {
9352
+ state.openaiUsageTimer = timer;
9353
+ },
9354
+ fetchUsage: () => getOpenAiUsage(state.port),
9355
+ report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
9356
+ isLocalCredentialProblem: isLocalCredentialProblem2,
9357
+ forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
9358
+ firstDelayMs: firstReportDelayMs,
9359
+ nextDelayMs: usageReportDelayMs,
9360
+ failureLogLevel: usageReportFailureLogLevel
9361
+ });
8682
9362
  scheduleResourceUsageReporting(state, options);
8683
9363
  if (!interactive || state.json) {
8684
9364
  log2(state, "Driving channel messages...");
@@ -8760,6 +9440,9 @@ program.command("run").description("Connect to Evident and process messages").op
8760
9440
  ).option(
8761
9441
  "--claude-usage-reporting <mode>",
8762
9442
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
9443
+ ).option(
9444
+ "--openai-usage-reporting <mode>",
9445
+ "Report OpenAI plan usage to Evident: auto | on | off (default: auto). Env: EVIDENT_OPENAI_USAGE_REPORTING"
8763
9446
  ).option(
8764
9447
  "--no-resource-usage-reporting",
8765
9448
  "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 +9454,9 @@ program.command("run").description("Connect to Evident and process messages").op
8771
9454
  ).option(
8772
9455
  "--tunnel-ready-file <path>",
8773
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."
8774
9460
  ).action(
8775
9461
  (options) => {
8776
9462
  run({
@@ -8795,13 +9481,15 @@ program.command("run").description("Connect to Evident and process messages").op
8795
9481
  // Raw string — the resolver in run.ts single-sources parsing
8796
9482
  // (resolveClaudeUsageReportingMode).
8797
9483
  claudeUsageReporting: options.claudeUsageReporting,
9484
+ openaiUsageReporting: options.openaiUsageReporting,
8798
9485
  // Raw value — resolution is single-sourced in run.ts's
8799
9486
  // resolveResourceUsageReportingEnabled.
8800
9487
  resourceUsageReporting: options.resourceUsageReporting,
8801
9488
  // Raw values — expansion/validation is single-sourced in run.ts's
8802
9489
  // resolveFileSyncDirectories.
8803
9490
  enableFileSyncTo: options.enableFileSyncTo,
8804
- tunnelReadyFile: options.tunnelReadyFile
9491
+ tunnelReadyFile: options.tunnelReadyFile,
9492
+ litestreamConfig: options.litestreamConfig
8805
9493
  });
8806
9494
  }
8807
9495
  );