@evident-ai/cli 3.4.1-dev.8fa4d29 → 3.4.1-dev.aa71e9a

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
@@ -746,6 +746,40 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
746
746
  return { ok: false, error: describeBestEffortError(error2) };
747
747
  }
748
748
  }
749
+ function toReportedOpenAiWindow(window) {
750
+ if (!window) return null;
751
+ return {
752
+ utilization: window.utilization,
753
+ window_minutes: window.windowMinutes,
754
+ resets_at: window.resetsAt
755
+ };
756
+ }
757
+ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
758
+ try {
759
+ const apiUrl = getApiUrlConfig();
760
+ const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
761
+ method: "POST",
762
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
763
+ body: JSON.stringify({
764
+ primary: toReportedOpenAiWindow(snapshot.primary),
765
+ secondary: toReportedOpenAiWindow(snapshot.secondary),
766
+ has_credits: snapshot.hasCredits,
767
+ credits_unlimited: snapshot.creditsUnlimited
768
+ }),
769
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
770
+ });
771
+ if (!response.ok) {
772
+ const serverMessage = await readErrorMessage(response);
773
+ return {
774
+ ok: false,
775
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
776
+ };
777
+ }
778
+ return { ok: true };
779
+ } catch (error2) {
780
+ return { ok: false, error: describeBestEffortError(error2) };
781
+ }
782
+ }
749
783
  async function reportResourceUsage(agentId, authHeader, usage) {
750
784
  try {
751
785
  const apiUrl = getApiUrlConfig();
@@ -1092,8 +1126,8 @@ async function claudeUsage() {
1092
1126
  }
1093
1127
 
1094
1128
  // 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";
1129
+ import { homedir as homedir4 } from "os";
1130
+ import { isAbsolute as isAbsolute2, join as join7, parse, resolve as resolvePath } from "path";
1097
1131
  import chalk6 from "chalk";
1098
1132
 
1099
1133
  // ../../packages/types/src/agents/index.ts
@@ -1326,6 +1360,8 @@ var SEVERITY_BY_LEVEL = {
1326
1360
  error: "error"
1327
1361
  };
1328
1362
  var MAX_MESSAGE_LENGTH = 500;
1363
+ var MAX_METADATA_VALUE_LENGTH = 200;
1364
+ var MAX_METADATA_ENTRIES = 20;
1329
1365
  var TRUNCATION_MARKER = "\u2026";
1330
1366
  function redact(message) {
1331
1367
  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 +1370,24 @@ function truncate(message) {
1334
1370
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
1335
1371
  return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
1336
1372
  }
1373
+ function sanitiseMetadata(metadata) {
1374
+ if (!metadata) return {};
1375
+ const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
1376
+ if (Object.keys(metadata).length > entries.length) {
1377
+ console.error(
1378
+ `[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
1379
+ );
1380
+ }
1381
+ const sanitised = [];
1382
+ for (const [key, value] of entries) {
1383
+ if (typeof value === "string") {
1384
+ sanitised.push([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
1385
+ } else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
1386
+ sanitised.push([key, value]);
1387
+ }
1388
+ }
1389
+ return Object.fromEntries(sanitised);
1390
+ }
1337
1391
  var RATE_LIMIT_WINDOW_MS = 6e4;
1338
1392
  var RATE_LIMIT_MAX_EVENTS = 30;
1339
1393
  var windowStartedAt = 0;
@@ -1372,7 +1426,7 @@ function forwardRunnerActivity(entry, context) {
1372
1426
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1373
1427
  severity: SEVERITY_BY_LEVEL[entry.level],
1374
1428
  message,
1375
- metadata: { source: "cli.run" },
1429
+ metadata: { ...sanitiseMetadata(entry.metadata), source: "cli.run" },
1376
1430
  agentId: context.agentId
1377
1431
  });
1378
1432
  } catch (err) {
@@ -1382,6 +1436,143 @@ function forwardRunnerActivity(entry, context) {
1382
1436
  }
1383
1437
  }
1384
1438
 
1439
+ // src/lib/opencode/session-db-recovery-report.ts
1440
+ import { readFileSync as readFileSync2, unlinkSync } from "fs";
1441
+ import { join as join2 } from "path";
1442
+ function sessionDbRecoveryReportPath(homeDir, env) {
1443
+ const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1444
+ return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
1445
+ }
1446
+ function drainSessionDbRecoveryReport({
1447
+ homeDir,
1448
+ env
1449
+ }) {
1450
+ const path = sessionDbRecoveryReportPath(homeDir, env);
1451
+ let content;
1452
+ try {
1453
+ content = readFileSync2(path, "utf8");
1454
+ } catch (error2) {
1455
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
1456
+ return { path, records: [], skippedLines: 0, readError: null };
1457
+ const readError = error2 instanceof Error ? error2.message : String(error2);
1458
+ console.error(`[session-db-recovery-report] could not read ${path}: ${readError}`);
1459
+ return { path, records: [], skippedLines: 0, readError };
1460
+ }
1461
+ let skippedLines = 0;
1462
+ const records = content.split("\n").flatMap((line) => {
1463
+ if (!line.trim()) return [];
1464
+ try {
1465
+ const value = JSON.parse(line);
1466
+ if (!isSessionDbRecoveryRecord(value)) {
1467
+ skippedLines++;
1468
+ return [];
1469
+ }
1470
+ return [value];
1471
+ } catch (error2) {
1472
+ skippedLines++;
1473
+ console.error(
1474
+ `[session-db-recovery-report] skipped malformed record in ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
1475
+ );
1476
+ return [];
1477
+ }
1478
+ });
1479
+ return { path, records, skippedLines, readError: null };
1480
+ }
1481
+ function acknowledgeSessionDbRecoveryReport(path) {
1482
+ try {
1483
+ unlinkSync(path);
1484
+ } catch (error2) {
1485
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
1486
+ console.error(
1487
+ `[session-db-recovery-report] could not remove ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
1488
+ );
1489
+ }
1490
+ }
1491
+ function buildSessionDbRecoveryActivity(record) {
1492
+ const level = record.severity === "warning" ? "warn" : record.severity === "error" ? "error" : null;
1493
+ if (!level) return null;
1494
+ const noVerifiedPoint = record.verified_restore_point ? ` The verified restore point is ${record.verified_restore_point}.` : " No verified restore point is known.";
1495
+ switch (record.outcome) {
1496
+ case "fresh_session_db":
1497
+ return {
1498
+ level,
1499
+ metadata: withoutContractFields(record),
1500
+ 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.`
1501
+ };
1502
+ case "restore_retried":
1503
+ return {
1504
+ level,
1505
+ metadata: withoutContractFields(record),
1506
+ message: "The initial session database restore failed; the runner will retry it. Monitor the runner backup for another restore failure."
1507
+ };
1508
+ case "replica_recovered":
1509
+ if (record.reason === "quarantine")
1510
+ return {
1511
+ level,
1512
+ metadata: withoutContractFields(record),
1513
+ 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.`
1514
+ };
1515
+ if (record.reason === "prune")
1516
+ return {
1517
+ level,
1518
+ metadata: withoutContractFields(record),
1519
+ 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."
1520
+ };
1521
+ if (record.reason === "clear")
1522
+ return {
1523
+ level,
1524
+ metadata: withoutContractFields(record),
1525
+ 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."
1526
+ };
1527
+ return null;
1528
+ case "history_rolled_back":
1529
+ return {
1530
+ level,
1531
+ metadata: withoutContractFields(record),
1532
+ 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.`
1533
+ };
1534
+ case "restore_misconfigured":
1535
+ return {
1536
+ level,
1537
+ metadata: withoutContractFields(record),
1538
+ message: "This runner started with a fresh session database because it could not verify where its session history is backed up. Nothing in the backup was changed; check the runner backup configuration and permissions."
1539
+ };
1540
+ default:
1541
+ return null;
1542
+ }
1543
+ }
1544
+ function withoutContractFields(record) {
1545
+ const { v: _v, event: _event, ...metadata } = record;
1546
+ return metadata;
1547
+ }
1548
+ var OUTCOMES = /* @__PURE__ */ new Set([
1549
+ "replica_recovered",
1550
+ "restore_retried",
1551
+ "fresh_session_db",
1552
+ "history_rolled_back",
1553
+ "restore_misconfigured"
1554
+ ]);
1555
+ var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
1556
+ var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
1557
+ var NUMBER_FIELDS = [
1558
+ "litestream_exit_code",
1559
+ "attempt",
1560
+ "replica_objects",
1561
+ "replica_bytes",
1562
+ "quarantined_objects",
1563
+ "quarantine_failed_objects",
1564
+ "quarantined_bytes",
1565
+ "restore_points_tried"
1566
+ ];
1567
+ var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"];
1568
+ function isSessionDbRecoveryRecord(value) {
1569
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1570
+ const record = value;
1571
+ 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(
1572
+ (field) => record[field] === null || typeof record[field] === "string"
1573
+ );
1574
+ }
1575
+
1385
1576
  // src/lib/opencode/health.ts
1386
1577
  async function checkOpenCodeHealth(port) {
1387
1578
  try {
@@ -2419,10 +2610,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
2419
2610
 
2420
2611
  // src/lib/opencode/session-db-size.ts
2421
2612
  import { statSync as statSync2 } from "fs";
2422
- import { join as join2 } from "path";
2613
+ import { join as join3 } from "path";
2423
2614
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
2424
2615
  function statSessionDbBytes(homeDir) {
2425
- const dbPath = join2(homeDir, ".local", "share", "opencode", "opencode.db");
2616
+ const dbPath = join3(homeDir, ".local", "share", "opencode", "opencode.db");
2426
2617
  try {
2427
2618
  return statSync2(dbPath).size;
2428
2619
  } catch (err) {
@@ -2991,6 +3182,177 @@ function writeTunnelReadyMarker(path, agentId) {
2991
3182
  }
2992
3183
  }
2993
3184
 
3185
+ // src/lib/openai-usage.ts
3186
+ import { readFileSync as readFileSync3 } from "fs";
3187
+ import { homedir as homedir2 } from "os";
3188
+ import { join as join4 } from "path";
3189
+ var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3190
+ var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
3191
+ var OpenAiUsageError = class extends Error {
3192
+ constructor(message, reason) {
3193
+ super(message);
3194
+ this.reason = reason;
3195
+ }
3196
+ };
3197
+ function isLocalCredentialProblem2(err) {
3198
+ return err instanceof OpenAiUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
3199
+ }
3200
+ function readOpenCodeChatGptCredentials() {
3201
+ try {
3202
+ const raw = readFileSync3(join4(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3203
+ let parsed;
3204
+ try {
3205
+ parsed = JSON.parse(raw);
3206
+ } catch {
3207
+ return null;
3208
+ }
3209
+ const entry = parsed.openai;
3210
+ if (entry?.type !== "oauth" || typeof entry.access !== "string" || !entry.access || typeof entry.expires !== "number") {
3211
+ return null;
3212
+ }
3213
+ return { accessToken: entry.access, expiresAt: entry.expires };
3214
+ } catch (err) {
3215
+ const code = err.code;
3216
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
3217
+ console.warn(
3218
+ `readOpenCodeChatGptCredentials: reading auth.json failed (${code ?? "unknown"})`
3219
+ );
3220
+ }
3221
+ return null;
3222
+ }
3223
+ }
3224
+ function toWindow2(headers, name) {
3225
+ const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
3226
+ const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
3227
+ if (utilizationHeader == null || utilizationHeader === "" || windowMinutesHeader == null) {
3228
+ return null;
3229
+ }
3230
+ const utilization = Number(utilizationHeader);
3231
+ const windowMinutes = Number(windowMinutesHeader);
3232
+ if (!Number.isFinite(utilization) || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
3233
+ return null;
3234
+ }
3235
+ const resetAtHeader = headers.get(`x-codex-${name}-reset-at`);
3236
+ const resetSeconds = resetAtHeader == null || resetAtHeader === "" ? NaN : Number(resetAtHeader);
3237
+ const resetsAt = Number.isFinite(resetSeconds) ? new Date(resetSeconds * 1e3).toISOString() : null;
3238
+ return { utilization: Math.min(100, Math.max(0, utilization)), windowMinutes, resetsAt };
3239
+ }
3240
+ function parseCodexUsageHeaders(headers) {
3241
+ return {
3242
+ primary: toWindow2(headers, "primary"),
3243
+ secondary: toWindow2(headers, "secondary"),
3244
+ hasCredits: headers.has("x-codex-credits-has-credits") ? headers.get("x-codex-credits-has-credits")?.toLowerCase() === "true" : null,
3245
+ creditsUnlimited: headers.has("x-codex-credits-unlimited") ? headers.get("x-codex-credits-unlimited")?.toLowerCase() === "true" : null
3246
+ };
3247
+ }
3248
+ function normalizeProbeModel(model) {
3249
+ return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
3250
+ }
3251
+ async function resolveProbeModels(port) {
3252
+ try {
3253
+ const res = await withRequestTimeout(
3254
+ fetch,
3255
+ REQUEST_TIMEOUT_MS
3256
+ )(`${opencodeBase(port)}/config/providers`);
3257
+ if (!res.ok) {
3258
+ console.error(
3259
+ `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
3260
+ );
3261
+ return [];
3262
+ }
3263
+ const body = await res.json();
3264
+ const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
3265
+ if (!provider || !provider.models || typeof provider.models !== "object") return [];
3266
+ const candidates = [
3267
+ ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
3268
+ ...Object.keys(provider.models)
3269
+ ].map(normalizeProbeModel);
3270
+ return [...new Set(candidates)].slice(0, 4);
3271
+ } catch (err) {
3272
+ console.error(
3273
+ `[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3274
+ );
3275
+ return [];
3276
+ }
3277
+ }
3278
+ function hasPrimaryHeaders(headers) {
3279
+ return [
3280
+ "x-codex-primary-used-percent",
3281
+ "x-codex-primary-window-minutes",
3282
+ "x-codex-primary-reset-at"
3283
+ ].some((name) => headers.has(name));
3284
+ }
3285
+ async function getOpenAiUsage(port) {
3286
+ const credentials2 = readOpenCodeChatGptCredentials();
3287
+ if (!credentials2) {
3288
+ throw new OpenAiUsageError(
3289
+ "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3290
+ "no_credentials"
3291
+ );
3292
+ }
3293
+ if (credentials2.expiresAt < Date.now()) {
3294
+ throw new OpenAiUsageError(
3295
+ "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3296
+ "credentials_expired"
3297
+ );
3298
+ }
3299
+ const models = await resolveProbeModels(port);
3300
+ if (models.length === 0) {
3301
+ throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
3302
+ }
3303
+ let lastStatus;
3304
+ for (const model of models) {
3305
+ let res;
3306
+ try {
3307
+ res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
3308
+ method: "POST",
3309
+ headers: {
3310
+ Authorization: `Bearer ${credentials2.accessToken}`,
3311
+ "Content-Type": "application/json"
3312
+ },
3313
+ body: JSON.stringify({ model, store: false, stream: true })
3314
+ });
3315
+ } catch (err) {
3316
+ throw new OpenAiUsageError(
3317
+ `OpenAI usage probe request failed: ${err instanceof Error ? err.message : String(err)}.`,
3318
+ "request_failed"
3319
+ );
3320
+ }
3321
+ try {
3322
+ lastStatus = res.status;
3323
+ if (hasPrimaryHeaders(res.headers)) {
3324
+ const usage = parseCodexUsageHeaders(res.headers);
3325
+ if (!usage.primary && !usage.secondary) {
3326
+ throw new OpenAiUsageError(
3327
+ `OpenAI usage probe returned no usable window (HTTP ${res.status}).`,
3328
+ "no_usable_window"
3329
+ );
3330
+ }
3331
+ return usage;
3332
+ }
3333
+ if (res.status === 401) {
3334
+ throw new OpenAiUsageError(
3335
+ "ChatGPT credentials have expired (HTTP 401).",
3336
+ "credentials_expired"
3337
+ );
3338
+ }
3339
+ if (res.status === 403 || res.status === 429) {
3340
+ throw new OpenAiUsageError(
3341
+ `OpenAI usage probe was blocked (HTTP ${res.status}).`,
3342
+ "probe_blocked"
3343
+ );
3344
+ }
3345
+ } finally {
3346
+ await res.body?.cancel().catch(() => {
3347
+ });
3348
+ }
3349
+ }
3350
+ throw new OpenAiUsageError(
3351
+ `OpenAI usage probe failed: HTTP ${lastStatus ?? "unknown"}.`,
3352
+ "request_failed"
3353
+ );
3354
+ }
3355
+
2994
3356
  // src/lib/reporting-schedule.ts
2995
3357
  function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2996
3358
  const jitterRangeMs = baseMs * jitterFraction;
@@ -2999,6 +3361,31 @@ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2999
3361
  function firstReportDelayMs(random = Math.random) {
3000
3362
  return 5e3 + random() * 1e4;
3001
3363
  }
3364
+ var VALID_USAGE_REPORTING_MODES = ["auto", "on", "off"];
3365
+ function resolveUsageReportingMode(flagValue, env, names) {
3366
+ const raw = flagValue ?? env[names.envVar];
3367
+ if (raw === void 0 || raw === "") return { mode: "auto", warnings: [] };
3368
+ const normalized = raw.trim().toLowerCase();
3369
+ if (VALID_USAGE_REPORTING_MODES.includes(normalized)) {
3370
+ return { mode: normalized, warnings: [] };
3371
+ }
3372
+ const source = flagValue !== void 0 ? names.flagName : names.envVar;
3373
+ return {
3374
+ mode: "auto",
3375
+ warnings: [
3376
+ `Ignoring invalid ${source} "${raw}": expected one of ${VALID_USAGE_REPORTING_MODES.join(", ")}; using auto`
3377
+ ]
3378
+ };
3379
+ }
3380
+ var BASE_USAGE_REPORT_DELAY_MS = 10 * 6e4;
3381
+ var USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
3382
+ function usageReportDelayMs(random = Math.random) {
3383
+ return jitteredDelayMs(BASE_USAGE_REPORT_DELAY_MS, USAGE_REPORT_DELAY_JITTER_FRACTION, random);
3384
+ }
3385
+ var USAGE_REPORT_FAILURE_REESCALATION_TICKS = 6;
3386
+ function usageReportFailureLogLevel(consecutiveFailures) {
3387
+ return reportFailureLogLevel(consecutiveFailures, USAGE_REPORT_FAILURE_REESCALATION_TICKS);
3388
+ }
3002
3389
  function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
3003
3390
  return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
3004
3391
  }
@@ -3007,33 +3394,26 @@ function failureStreakSuffix(consecutiveFailures) {
3007
3394
  }
3008
3395
 
3009
3396
  // src/lib/claude-usage-reporting.ts
3010
- var VALID_MODES = ["auto", "on", "off"];
3011
3397
  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
- };
3398
+ return resolveUsageReportingMode(flagValue, env, {
3399
+ flagName: "--claude-usage-reporting",
3400
+ envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
3401
+ });
3027
3402
  }
3028
- var BASE_REPORT_DELAY_MS = 10 * 6e4;
3029
- var REPORT_DELAY_JITTER_FRACTION = 0.2;
3030
3403
  function nextReportDelayMs(random = Math.random) {
3031
- return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);
3404
+ return usageReportDelayMs(random);
3032
3405
  }
3033
3406
  var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
3034
- var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
3035
3407
  function claudeUsageFailureLogLevel(consecutiveFailures) {
3036
- return reportFailureLogLevel(consecutiveFailures, CLAUDE_USAGE_FAILURE_REESCALATION_TICKS);
3408
+ return usageReportFailureLogLevel(consecutiveFailures);
3409
+ }
3410
+
3411
+ // src/lib/openai-usage-reporting.ts
3412
+ function resolveOpenAiUsageReportingMode(flagValue, env) {
3413
+ return resolveUsageReportingMode(flagValue, env, {
3414
+ flagName: "--openai-usage-reporting",
3415
+ envVar: "EVIDENT_OPENAI_USAGE_REPORTING"
3416
+ });
3037
3417
  }
3038
3418
 
3039
3419
  // src/lib/resource-usage-reporting.ts
@@ -3192,15 +3572,15 @@ function createResourceUsageCollector(homeDir) {
3192
3572
  }
3193
3573
 
3194
3574
  // src/lib/channels/driver.ts
3195
- import { homedir as homedir2 } from "os";
3575
+ import { homedir as homedir3 } from "os";
3196
3576
 
3197
3577
  // src/lib/runner-file-sync.ts
3198
- import { join as join4 } from "path";
3578
+ import { join as join6 } from "path";
3199
3579
 
3200
3580
  // src/lib/file-push.ts
3201
3581
  import { randomUUID } from "crypto";
3202
3582
  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";
3583
+ import { basename, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
3204
3584
  var FILE_MODE = 384;
3205
3585
  var DIRECTORY_MODE = 448;
3206
3586
  async function writePushedFile(request) {
@@ -3233,7 +3613,7 @@ async function writePushedFile(request) {
3233
3613
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
3234
3614
  dirname3(candidate)
3235
3615
  );
3236
- const realTarget = join3(existingAncestor, ...missingSegments, basename(candidate));
3616
+ const realTarget = join5(existingAncestor, ...missingSegments, basename(candidate));
3237
3617
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
3238
3618
  if (allowedDirectory === null) {
3239
3619
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -3269,7 +3649,7 @@ function expandAndValidate(requestedPath, homeDir) {
3269
3649
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
3270
3650
  return null;
3271
3651
  }
3272
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join3(homeDir, requestedPath.slice(2)) : requestedPath;
3652
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
3273
3653
  if (expanded.split(/[/\\]/).includes("..")) {
3274
3654
  return null;
3275
3655
  }
@@ -3342,13 +3722,13 @@ function contains(realDirectory, realTarget) {
3342
3722
  async function createMissingDirectories(existingAncestor, missingSegments) {
3343
3723
  let current = existingAncestor;
3344
3724
  for (const segment of missingSegments) {
3345
- current = join3(current, segment);
3725
+ current = join5(current, segment);
3346
3726
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
3347
3727
  await chmod(current, DIRECTORY_MODE);
3348
3728
  }
3349
3729
  }
3350
3730
  async function writeAtomically(realTarget, content) {
3351
- const temporaryPath = join3(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3731
+ const temporaryPath = join5(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3352
3732
  let handle;
3353
3733
  try {
3354
3734
  handle = await open2(temporaryPath, "wx", FILE_MODE);
@@ -3390,20 +3770,28 @@ async function syncPendingRunnerFiles(options) {
3390
3770
  for (const id of options.ackFailures.keys()) {
3391
3771
  if (!pendingIds.has(id)) options.ackFailures.delete(id);
3392
3772
  }
3393
- if (pending.length === 0) return { applied: 0, claudeCredentialApplied: false };
3773
+ if (pending.length === 0) {
3774
+ return { applied: 0, claudeCredentialApplied: false, opencodeAuthApplied: false };
3775
+ }
3394
3776
  options.log({
3395
3777
  level: "info",
3396
3778
  message: `Runner file sync: ${pending.length} file(s) queued for this runner`
3397
3779
  });
3398
3780
  let applied = 0;
3399
3781
  let claudeCredentialApplied = false;
3782
+ let opencodeAuthApplied = false;
3400
3783
  for (const file of pending) {
3401
3784
  if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
3402
3785
  const outcome = await applyOne(options, file);
3403
3786
  if (outcome.applied) applied += 1;
3404
3787
  if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
3788
+ if (outcome.opencodeAuthApplied) opencodeAuthApplied = true;
3405
3789
  }
3406
- return { applied, claudeCredentialApplied };
3790
+ return {
3791
+ applied,
3792
+ claudeCredentialApplied,
3793
+ opencodeAuthApplied
3794
+ };
3407
3795
  }
3408
3796
  async function listPendingFiles(options) {
3409
3797
  let res;
@@ -3464,10 +3852,18 @@ function asPendingFile(entry) {
3464
3852
  if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
3465
3853
  return { id, path, size };
3466
3854
  }
3467
- var NOT_APPLIED = { applied: false, claudeCredentialApplied: false };
3855
+ var NOT_APPLIED = {
3856
+ applied: false,
3857
+ claudeCredentialApplied: false,
3858
+ opencodeAuthApplied: false
3859
+ };
3468
3860
  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);
3861
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
3862
+ return expanded === join6(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
3863
+ }
3864
+ function isOpenCodeAuthPath(requestedPath, homeDir) {
3865
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
3866
+ return expanded === join6(homeDir, ...OPENCODE_AUTH_SEGMENTS);
3471
3867
  }
3472
3868
  async function applyOne(options, file) {
3473
3869
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -3523,7 +3919,8 @@ async function applyOne(options, file) {
3523
3919
  await ack(options, file, "applied");
3524
3920
  return {
3525
3921
  applied: true,
3526
- claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
3922
+ claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),
3923
+ opencodeAuthApplied: isOpenCodeAuthPath(file.path, options.homeDir)
3527
3924
  };
3528
3925
  }
3529
3926
  function durableDownloadCode(status2) {
@@ -3977,6 +4374,7 @@ var ChannelDriver = class _ChannelDriver {
3977
4374
  * that way rather than "fixing" it into a count.
3978
4375
  */
3979
4376
  claudeCredentialApplyCount = 0;
4377
+ opencodeAuthApplyCount = 0;
3980
4378
  /**
3981
4379
  * The currently-executing `drainPending()` promise, or null when idle. Lets a
3982
4380
  * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
@@ -4011,7 +4409,7 @@ var ChannelDriver = class _ChannelDriver {
4011
4409
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
4012
4410
  this.now = config.now ?? (() => Date.now());
4013
4411
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
4014
- this.homeDir = config.homeDir ?? homedir2();
4412
+ this.homeDir = config.homeDir ?? homedir3();
4015
4413
  this.maxActiveSessions = config.maxActiveSessions;
4016
4414
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
4017
4415
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -4081,6 +4479,7 @@ var ChannelDriver = class _ChannelDriver {
4081
4479
  });
4082
4480
  this.appliedFileCount += result.applied;
4083
4481
  if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
4482
+ if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
4084
4483
  return result.applied;
4085
4484
  } catch (err) {
4086
4485
  this.log({
@@ -4188,7 +4587,8 @@ var ChannelDriver = class _ChannelDriver {
4188
4587
  return {
4189
4588
  appliedFiles: this.appliedFileCount,
4190
4589
  inFlight: this.syncingFiles,
4191
- claudeCredentialApplies: this.claudeCredentialApplyCount
4590
+ claudeCredentialApplies: this.claudeCredentialApplyCount,
4591
+ opencodeAuthApplyCount: this.opencodeAuthApplyCount
4192
4592
  };
4193
4593
  }
4194
4594
  /**
@@ -7525,7 +7925,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
7525
7925
  if (trimmed === "") {
7526
7926
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
7527
7927
  }
7528
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join5(homeDir, trimmed.slice(2)) : trimmed;
7928
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join7(homeDir, trimmed.slice(2)) : trimmed;
7529
7929
  if (!isAbsolute2(expanded)) {
7530
7930
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
7531
7931
  }
@@ -7623,7 +8023,7 @@ function logActivity(state, entry) {
7623
8023
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
7624
8024
  if (!meetsThreshold(state, level)) return;
7625
8025
  forwardRunnerActivity(
7626
- { level, message: entry.message, error: entry.error },
8026
+ { level, message: entry.message, error: entry.error, metadata: entry.metadata },
7627
8027
  { agentId: state.agentId, authHeader: state.authHeader }
7628
8028
  );
7629
8029
  const fullEntry = {
@@ -7643,6 +8043,26 @@ function logActivity(state, entry) {
7643
8043
  }
7644
8044
  }
7645
8045
  }
8046
+ function reportSessionDbRecovery(state) {
8047
+ try {
8048
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir4(), env: process.env });
8049
+ for (const record of report.records) {
8050
+ const activity = buildSessionDbRecoveryActivity(record);
8051
+ if (!activity) throw new Error("could not map session-DB recovery record");
8052
+ logActivity(state, {
8053
+ type: activity.level === "error" ? "error" : "info",
8054
+ level: activity.level,
8055
+ ...activity.level === "error" ? { error: activity.message } : { message: activity.message },
8056
+ metadata: activity.metadata
8057
+ });
8058
+ }
8059
+ acknowledgeSessionDbRecoveryReport(report.path);
8060
+ } catch (error2) {
8061
+ console.error(
8062
+ `[run] could not report session-DB recovery activity: ${error2 instanceof Error ? error2.message : String(error2)}`
8063
+ );
8064
+ }
8065
+ }
7646
8066
  function displayStatus(state) {
7647
8067
  if (!state.interactive) return;
7648
8068
  const attempt = state.connection?.reconnectAttempt ?? 0;
@@ -7733,6 +8153,7 @@ async function driveChannels(state, driver) {
7733
8153
  let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7734
8154
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
7735
8155
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
8156
+ let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
7736
8157
  while (state.running) {
7737
8158
  const cycleStartedAtMs = performance.now();
7738
8159
  let idleThisCycle = false;
@@ -7765,6 +8186,10 @@ async function driveChannels(state, driver) {
7765
8186
  const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
7766
8187
  lastSeenClaudeApplies = claudeCredentialApplies;
7767
8188
  if (claudeCredentialApplied) state.claudeUsageRearm?.();
8189
+ const opencodeAuthApplies = fileActivitySnapshot.opencodeAuthApplyCount;
8190
+ const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
8191
+ lastSeenOpencodeAuthApplies = opencodeAuthApplies;
8192
+ if (opencodeAuthApplied) state.openaiUsageRearm?.();
7768
8193
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
7769
8194
  idlePolls = 0;
7770
8195
  idleMs = 0;
@@ -7833,7 +8258,7 @@ async function driveChannels(state, driver) {
7833
8258
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
7834
8259
  var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
7835
8260
  function sessionDbPath() {
7836
- return join5(homedir3(), ".local", "share", "opencode", "opencode.db");
8261
+ return join7(homedir4(), ".local", "share", "opencode", "opencode.db");
7837
8262
  }
7838
8263
  async function runSweep(state, driver, config) {
7839
8264
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
@@ -7916,7 +8341,7 @@ function scheduleSessionCleanup(state, driver, options) {
7916
8341
  for (const warning2 of config.warnings) {
7917
8342
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
7918
8343
  }
7919
- const dbBytes = statSessionDbBytes(homedir3());
8344
+ const dbBytes = statSessionDbBytes(homedir4());
7920
8345
  void (async () => {
7921
8346
  const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
7922
8347
  const sizeWarning = buildSessionStoreSizeWarning({
@@ -7944,23 +8369,20 @@ function scheduleSessionCleanup(state, driver, options) {
7944
8369
  );
7945
8370
  state.sessionCleanupTimers.push(interval, firstSweep);
7946
8371
  }
7947
- function scheduleClaudeUsageReporting(state, options) {
7948
- const { mode, warnings } = resolveClaudeUsageReportingMode(
7949
- options.claudeUsageReporting,
7950
- process.env
7951
- );
8372
+ function scheduleUsageReporting(state, params) {
8373
+ const { mode, warnings } = params.resolved;
7952
8374
  for (const warning2 of warnings) {
7953
8375
  logActivity(state, {
7954
8376
  type: "info",
7955
8377
  level: "warn",
7956
- message: `Claude usage reporting: ${warning2}`
8378
+ message: `${params.label} usage reporting: ${warning2}`
7957
8379
  });
7958
8380
  }
7959
8381
  if (mode === "off") {
7960
8382
  logActivity(state, {
7961
8383
  type: "info",
7962
8384
  level: "debug",
7963
- message: "Claude usage reporting is off (--claude-usage-reporting off)"
8385
+ message: `${params.label} usage reporting is off (${params.offFlagHint})`
7964
8386
  });
7965
8387
  return null;
7966
8388
  }
@@ -7969,7 +8391,7 @@ function scheduleClaudeUsageReporting(state, options) {
7969
8391
  let rearmRequested = false;
7970
8392
  const armProbe = () => {
7971
8393
  phase = "probe-pending";
7972
- state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
8394
+ params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
7973
8395
  };
7974
8396
  const scheduleNextTick = () => {
7975
8397
  if (rearmRequested) {
@@ -7978,7 +8400,7 @@ function scheduleClaudeUsageReporting(state, options) {
7978
8400
  return;
7979
8401
  }
7980
8402
  phase = "steady-pending";
7981
- state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
8403
+ params.setTimer(setTimeout(() => void tick(false), params.nextDelayMs()));
7982
8404
  };
7983
8405
  const rearm = () => {
7984
8406
  switch (phase) {
@@ -7988,9 +8410,9 @@ function scheduleClaudeUsageReporting(state, options) {
7988
8410
  case "probe-pending":
7989
8411
  return;
7990
8412
  case "steady-pending":
7991
- if (state.claudeUsageTimer) {
7992
- clearTimeout(state.claudeUsageTimer);
7993
- state.claudeUsageTimer = null;
8413
+ if (params.getTimer()) {
8414
+ clearTimeout(params.getTimer());
8415
+ params.setTimer(null);
7994
8416
  }
7995
8417
  rearmRequested = false;
7996
8418
  armProbe();
@@ -8004,45 +8426,45 @@ function scheduleClaudeUsageReporting(state, options) {
8004
8426
  const tick = async (isProbe) => {
8005
8427
  phase = "tick-in-flight";
8006
8428
  try {
8007
- const usage = await getClaudeUsage();
8008
- const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
8429
+ const usage = await params.fetchUsage();
8430
+ const result = await params.report(usage);
8009
8431
  if (result.ok) {
8010
8432
  if (consecutiveFailures > 0) {
8011
8433
  logActivity(state, {
8012
8434
  type: "info",
8013
8435
  level: "info",
8014
- message: "Claude usage reporting recovered"
8436
+ message: `${params.label} usage reporting recovered`
8015
8437
  });
8016
8438
  }
8017
8439
  consecutiveFailures = 0;
8018
8440
  logActivity(state, {
8019
8441
  type: "info",
8020
8442
  level: "debug",
8021
- message: "Reported Claude usage to Evident"
8443
+ message: `Reported ${params.label} usage to Evident`
8022
8444
  });
8023
8445
  } else {
8024
8446
  consecutiveFailures++;
8025
8447
  logActivity(state, {
8026
8448
  type: "info",
8027
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8028
- message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8449
+ level: params.failureLogLevel(consecutiveFailures),
8450
+ message: `Failed to report ${params.label} usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8029
8451
  });
8030
8452
  }
8031
8453
  scheduleNextTick();
8032
8454
  } catch (error2) {
8033
- if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
8455
+ if (params.isLocalCredentialProblem(error2)) {
8034
8456
  if (mode === "on") {
8035
8457
  logActivity(state, {
8036
8458
  type: "info",
8037
8459
  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"
8460
+ message: `${params.label} usage reporting is forced on but no usable login was found \u2014 ${params.forcedOnHint}; reporting will keep retrying`
8039
8461
  });
8040
8462
  scheduleNextTick();
8041
8463
  } else if (isProbe) {
8042
8464
  logActivity(state, {
8043
8465
  type: "info",
8044
8466
  level: "debug",
8045
- message: `Claude usage reporting: ${error2.message}`
8467
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8046
8468
  });
8047
8469
  phase = "dormant";
8048
8470
  if (rearmRequested) rearm();
@@ -8050,7 +8472,7 @@ function scheduleClaudeUsageReporting(state, options) {
8050
8472
  logActivity(state, {
8051
8473
  type: "info",
8052
8474
  level: "debug",
8053
- message: `Claude usage reporting: ${error2.message}`
8475
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8054
8476
  });
8055
8477
  scheduleNextTick();
8056
8478
  }
@@ -8059,8 +8481,8 @@ function scheduleClaudeUsageReporting(state, options) {
8059
8481
  const message = error2 instanceof Error ? error2.message : String(error2);
8060
8482
  logActivity(state, {
8061
8483
  type: "info",
8062
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8063
- message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8484
+ level: params.failureLogLevel(consecutiveFailures),
8485
+ message: `${params.label} usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8064
8486
  });
8065
8487
  scheduleNextTick();
8066
8488
  }
@@ -8069,6 +8491,24 @@ function scheduleClaudeUsageReporting(state, options) {
8069
8491
  armProbe();
8070
8492
  return rearm;
8071
8493
  }
8494
+ function scheduleClaudeUsageReporting(state, options) {
8495
+ return scheduleUsageReporting(state, {
8496
+ label: "Claude",
8497
+ resolved: resolveClaudeUsageReportingMode(options.claudeUsageReporting, process.env),
8498
+ offFlagHint: "--claude-usage-reporting off",
8499
+ getTimer: () => state.claudeUsageTimer,
8500
+ setTimer: (timer) => {
8501
+ state.claudeUsageTimer = timer;
8502
+ },
8503
+ fetchUsage: getClaudeUsage,
8504
+ report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
8505
+ isLocalCredentialProblem,
8506
+ forcedOnHint: "run `claude` to sign in",
8507
+ firstDelayMs: () => FIRST_REPORT_DELAY_MS,
8508
+ nextDelayMs: nextReportDelayMs,
8509
+ failureLogLevel: claudeUsageFailureLogLevel
8510
+ });
8511
+ }
8072
8512
  var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
8073
8513
  var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
8074
8514
  var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
@@ -8092,7 +8532,7 @@ function scheduleResourceUsageReporting(state, options) {
8092
8532
  });
8093
8533
  return;
8094
8534
  }
8095
- const collect = createResourceUsageCollector(homedir3());
8535
+ const collect = createResourceUsageCollector(homedir4());
8096
8536
  let consecutiveFailures = 0;
8097
8537
  const tick = async () => {
8098
8538
  try {
@@ -8193,6 +8633,11 @@ async function cleanup(state, opts = {}) {
8193
8633
  state.claudeUsageTimer = null;
8194
8634
  }
8195
8635
  state.claudeUsageRearm = null;
8636
+ if (state.openaiUsageTimer) {
8637
+ clearTimeout(state.openaiUsageTimer);
8638
+ state.openaiUsageTimer = null;
8639
+ }
8640
+ state.openaiUsageRearm = null;
8196
8641
  if (state.resourceUsageTimer) {
8197
8642
  clearTimeout(state.resourceUsageTimer);
8198
8643
  state.resourceUsageTimer = null;
@@ -8244,7 +8689,7 @@ async function run(options) {
8244
8689
  let fileSyncDirectories;
8245
8690
  try {
8246
8691
  logLevel = resolveLogLevel(options);
8247
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir3());
8692
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir4());
8248
8693
  } catch (error2) {
8249
8694
  const message = error2 instanceof Error ? error2.message : String(error2);
8250
8695
  if (options.json) {
@@ -8279,6 +8724,8 @@ async function run(options) {
8279
8724
  sessionCleanupTimers: [],
8280
8725
  claudeUsageTimer: null,
8281
8726
  claudeUsageRearm: null,
8727
+ openaiUsageTimer: null,
8728
+ openaiUsageRearm: null,
8282
8729
  resourceUsageTimer: null,
8283
8730
  authHeader: ""
8284
8731
  };
@@ -8474,6 +8921,7 @@ async function run(options) {
8474
8921
  } else {
8475
8922
  log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
8476
8923
  }
8924
+ reportSessionDbRecovery(state);
8477
8925
  const { timeoutMs: opencodeStartTimeoutMs, warnings: opencodeStartTimeoutWarnings } = resolveOpenCodeStartTimeoutMs(options, process.env);
8478
8926
  for (const warning2 of opencodeStartTimeoutWarnings) {
8479
8927
  logActivity(state, { type: "info", level: "warn", message: warning2 });
@@ -8541,7 +8989,7 @@ async function run(options) {
8541
8989
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
8542
8990
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
8543
8991
  fileSyncDirectories,
8544
- homeDir: homedir3(),
8992
+ homeDir: homedir4(),
8545
8993
  maxActiveSessions,
8546
8994
  log: (entry) => (
8547
8995
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -8679,6 +9127,22 @@ async function run(options) {
8679
9127
  }
8680
9128
  scheduleSessionCleanup(state, channelDriver, options);
8681
9129
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
9130
+ state.openaiUsageRearm = scheduleUsageReporting(state, {
9131
+ label: "OpenAI",
9132
+ resolved: resolveOpenAiUsageReportingMode(options.openaiUsageReporting, process.env),
9133
+ offFlagHint: "--openai-usage-reporting off",
9134
+ getTimer: () => state.openaiUsageTimer,
9135
+ setTimer: (timer) => {
9136
+ state.openaiUsageTimer = timer;
9137
+ },
9138
+ fetchUsage: () => getOpenAiUsage(state.port),
9139
+ report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
9140
+ isLocalCredentialProblem: isLocalCredentialProblem2,
9141
+ forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
9142
+ firstDelayMs: firstReportDelayMs,
9143
+ nextDelayMs: usageReportDelayMs,
9144
+ failureLogLevel: usageReportFailureLogLevel
9145
+ });
8682
9146
  scheduleResourceUsageReporting(state, options);
8683
9147
  if (!interactive || state.json) {
8684
9148
  log2(state, "Driving channel messages...");
@@ -8760,6 +9224,9 @@ program.command("run").description("Connect to Evident and process messages").op
8760
9224
  ).option(
8761
9225
  "--claude-usage-reporting <mode>",
8762
9226
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
9227
+ ).option(
9228
+ "--openai-usage-reporting <mode>",
9229
+ "Report OpenAI plan usage to Evident: auto | on | off (default: auto). Env: EVIDENT_OPENAI_USAGE_REPORTING"
8763
9230
  ).option(
8764
9231
  "--no-resource-usage-reporting",
8765
9232
  "Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off"
@@ -8795,6 +9262,7 @@ program.command("run").description("Connect to Evident and process messages").op
8795
9262
  // Raw string — the resolver in run.ts single-sources parsing
8796
9263
  // (resolveClaudeUsageReportingMode).
8797
9264
  claudeUsageReporting: options.claudeUsageReporting,
9265
+ openaiUsageReporting: options.openaiUsageReporting,
8798
9266
  // Raw value — resolution is single-sourced in run.ts's
8799
9267
  // resolveResourceUsageReportingEnabled.
8800
9268
  resourceUsageReporting: options.resourceUsageReporting,