@evident-ai/cli 3.3.1-dev.a4bb60b → 3.3.1-dev.b5645b1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,6 +105,11 @@ Options:
105
105
  keeps retrying) if no usable login is found; `off` disables it entirely — no
106
106
  Claude credential is ever read. An unrecognized value falls back to `auto` with
107
107
  a warning. Env: `EVIDENT_CLAUDE_USAGE_REPORTING`.
108
+ - `--no-resource-usage-reporting` — Don't report this machine's CPU
109
+ utilization, total and available memory, and core count to Evident, so it
110
+ shows on the runner page. On by default; nothing else about the machine
111
+ leaves it. Env: `EVIDENT_RESOURCE_USAGE_REPORTING=off` (the flag wins if
112
+ both are set).
108
113
  - `--json` — Output in JSON format (forces non-interactive mode).
109
114
  - `--session-cleanup-max-age <duration>` — Delete OpenCode sessions idle longer
110
115
  than this window (format `<number><unit>`, unit one of `s, m, h, d` — e.g.
@@ -171,6 +176,9 @@ targets the **production** Evident platform by default.
171
176
  - `EVIDENT_TUNNEL_URL` — Override the tunnel relay URL (equivalent to `--tunnel`).
172
177
  - `EVIDENT_CLAUDE_USAGE_REPORTING` — Equivalent to `--claude-usage-reporting`; the
173
178
  flag wins if both are set.
179
+ - `EVIDENT_RESOURCE_USAGE_REPORTING` — Equivalent to
180
+ `--no-resource-usage-reporting`; set it to `off` to disable. The flag wins
181
+ if both are set.
174
182
  - `EVIDENT_LOG_LEVEL` — Equivalent to `--log-level`; the flag wins if both are
175
183
  set, and `-v`/`--verbose` also outranks this env var.
176
184
  - `EVIDENT_OPENCODE_START_TIMEOUT` — Equivalent to `--opencode-start-timeout`
package/dist/index.js CHANGED
@@ -746,6 +746,32 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
746
746
  return { ok: false, error: describeBestEffortError(error2) };
747
747
  }
748
748
  }
749
+ async function reportResourceUsage(agentId, authHeader, usage) {
750
+ try {
751
+ const apiUrl = getApiUrlConfig();
752
+ const response = await fetch(`${apiUrl}/runners/${agentId}/resource-usage`, {
753
+ method: "POST",
754
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
755
+ body: JSON.stringify({
756
+ cpu_percent: usage.cpuPercent,
757
+ cpu_count: usage.cpuCount,
758
+ memory_total_bytes: usage.memoryTotalBytes,
759
+ memory_available_bytes: usage.memoryAvailableBytes
760
+ }),
761
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
762
+ });
763
+ if (!response.ok) {
764
+ const serverMessage = await readErrorMessage(response);
765
+ return {
766
+ ok: false,
767
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
768
+ };
769
+ }
770
+ return { ok: true };
771
+ } catch (error2) {
772
+ return { ok: false, error: describeBestEffortError(error2) };
773
+ }
774
+ }
749
775
  async function getAgentInfo(agentId, authHeader) {
750
776
  const apiUrl = getApiUrlConfig();
751
777
  try {
@@ -934,6 +960,7 @@ import { homedir } from "os";
934
960
  import { join } from "path";
935
961
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
936
962
  var KEYCHAIN_SERVICE = "Claude Code-credentials";
963
+ var CLAUDE_CREDENTIALS_SEGMENTS = [".claude", ".credentials.json"];
937
964
  function parseClaudeCliCredentials(raw) {
938
965
  let parsed;
939
966
  try {
@@ -967,7 +994,7 @@ function readClaudeCliCredentials() {
967
994
  }
968
995
  }
969
996
  try {
970
- const raw = readFileSync(join(homedir(), ".claude", ".credentials.json"), "utf-8");
997
+ const raw = readFileSync(join(homedir(), ...CLAUDE_CREDENTIALS_SEGMENTS), "utf-8");
971
998
  return parseClaudeCliCredentials(raw);
972
999
  } catch (err) {
973
1000
  const code = err.code;
@@ -1063,7 +1090,7 @@ async function claudeUsage() {
1063
1090
 
1064
1091
  // src/commands/run.ts
1065
1092
  import { homedir as homedir3 } from "os";
1066
- import { isAbsolute as isAbsolute2, join as join4, parse, resolve as resolvePath } from "path";
1093
+ import { isAbsolute as isAbsolute2, join as join5, parse, resolve as resolvePath } from "path";
1067
1094
  import chalk6 from "chalk";
1068
1095
 
1069
1096
  // ../../packages/types/src/agents/index.ts
@@ -2961,6 +2988,21 @@ function writeTunnelReadyMarker(path, agentId) {
2961
2988
  }
2962
2989
  }
2963
2990
 
2991
+ // src/lib/reporting-schedule.ts
2992
+ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2993
+ const jitterRangeMs = baseMs * jitterFraction;
2994
+ return baseMs - jitterRangeMs + random() * (2 * jitterRangeMs);
2995
+ }
2996
+ function firstReportDelayMs(random = Math.random) {
2997
+ return 5e3 + random() * 1e4;
2998
+ }
2999
+ function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
3000
+ return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
3001
+ }
3002
+ function failureStreakSuffix(consecutiveFailures) {
3003
+ return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : "";
3004
+ }
3005
+
2964
3006
  // src/lib/claude-usage-reporting.ts
2965
3007
  var VALID_MODES = ["auto", "on", "off"];
2966
3008
  function resolveClaudeUsageReportingMode(flagValue, env) {
@@ -2983,18 +3025,79 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
2983
3025
  var BASE_REPORT_DELAY_MS = 10 * 6e4;
2984
3026
  var REPORT_DELAY_JITTER_FRACTION = 0.2;
2985
3027
  function nextReportDelayMs(random = Math.random) {
2986
- const jitterRangeMs = BASE_REPORT_DELAY_MS * REPORT_DELAY_JITTER_FRACTION;
2987
- return BASE_REPORT_DELAY_MS - jitterRangeMs + random() * (2 * jitterRangeMs);
3028
+ return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);
2988
3029
  }
2989
- var FIRST_REPORT_DELAY_MS = 5e3 + Math.random() * 1e4;
3030
+ var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
2990
3031
  var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
2991
3032
  function claudeUsageFailureLogLevel(consecutiveFailures) {
2992
- return consecutiveFailures === 1 || consecutiveFailures % CLAUDE_USAGE_FAILURE_REESCALATION_TICKS === 0 ? "warn" : "debug";
3033
+ return reportFailureLogLevel(consecutiveFailures, CLAUDE_USAGE_FAILURE_REESCALATION_TICKS);
3034
+ }
3035
+
3036
+ // src/lib/resource-usage-reporting.ts
3037
+ var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
3038
+ var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
3039
+ function resolveResourceUsageReportingEnabled(flagValue, env) {
3040
+ if (flagValue === false) {
3041
+ return { enabled: false, warnings: [] };
3042
+ }
3043
+ const raw = env.EVIDENT_RESOURCE_USAGE_REPORTING;
3044
+ if (raw === void 0 || raw === "") {
3045
+ return { enabled: true, warnings: [] };
3046
+ }
3047
+ const normalized = raw.trim().toLowerCase();
3048
+ if (DISABLED_VALUES.has(normalized)) {
3049
+ return { enabled: false, warnings: [] };
3050
+ }
3051
+ if (ENABLED_VALUES.has(normalized)) {
3052
+ return { enabled: true, warnings: [] };
3053
+ }
3054
+ return {
3055
+ enabled: true,
3056
+ warnings: [
3057
+ `Ignoring invalid EVIDENT_RESOURCE_USAGE_REPORTING "${raw}": expected on or off; leaving reporting on`
3058
+ ]
3059
+ };
3060
+ }
3061
+
3062
+ // src/lib/resource-usage.ts
3063
+ import { cpus, totalmem, freemem } from "os";
3064
+ function readCpuSample() {
3065
+ let busyMs = 0;
3066
+ let idleMs = 0;
3067
+ for (const cpu of cpus()) {
3068
+ busyMs += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq;
3069
+ idleMs += cpu.times.idle;
3070
+ }
3071
+ return { busyMs, idleMs };
3072
+ }
3073
+ function cpuPercentBetween(previous, current) {
3074
+ const deltaBusy = current.busyMs - previous.busyMs;
3075
+ const deltaIdle = current.idleMs - previous.idleMs;
3076
+ const total = deltaBusy + deltaIdle;
3077
+ if (total === 0) return null;
3078
+ return Math.round((deltaBusy / total * 100 + Number.EPSILON) * 100) / 100;
3079
+ }
3080
+ function createResourceUsageCollector() {
3081
+ let previous = readCpuSample();
3082
+ return () => {
3083
+ const current = readCpuSample();
3084
+ const cpuPercent = cpuPercentBetween(previous, current);
3085
+ previous = current;
3086
+ return {
3087
+ cpuPercent,
3088
+ cpuCount: cpus().length,
3089
+ memoryTotalBytes: totalmem(),
3090
+ memoryAvailableBytes: freemem()
3091
+ };
3092
+ };
2993
3093
  }
2994
3094
 
2995
3095
  // src/lib/channels/driver.ts
2996
3096
  import { homedir as homedir2 } from "os";
2997
3097
 
3098
+ // src/lib/runner-file-sync.ts
3099
+ import { join as join4 } from "path";
3100
+
2998
3101
  // src/lib/file-push.ts
2999
3102
  import { randomUUID } from "crypto";
3000
3103
  import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
@@ -3188,17 +3291,20 @@ async function syncPendingRunnerFiles(options) {
3188
3291
  for (const id of options.ackFailures.keys()) {
3189
3292
  if (!pendingIds.has(id)) options.ackFailures.delete(id);
3190
3293
  }
3191
- if (pending.length === 0) return 0;
3294
+ if (pending.length === 0) return { applied: 0, claudeCredentialApplied: false };
3192
3295
  options.log({
3193
3296
  level: "info",
3194
3297
  message: `Runner file sync: ${pending.length} file(s) queued for this runner`
3195
3298
  });
3196
3299
  let applied = 0;
3300
+ let claudeCredentialApplied = false;
3197
3301
  for (const file of pending) {
3198
3302
  if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
3199
- if (await applyOne(options, file)) applied += 1;
3303
+ const outcome = await applyOne(options, file);
3304
+ if (outcome.applied) applied += 1;
3305
+ if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
3200
3306
  }
3201
- return applied;
3307
+ return { applied, claudeCredentialApplied };
3202
3308
  }
3203
3309
  async function listPendingFiles(options) {
3204
3310
  let res;
@@ -3259,6 +3365,11 @@ function asPendingFile(entry) {
3259
3365
  if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
3260
3366
  return { id, path, size };
3261
3367
  }
3368
+ var NOT_APPLIED = { applied: false, claudeCredentialApplied: false };
3369
+ function isClaudeCredentialPath(requestedPath, homeDir) {
3370
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join4(homeDir, requestedPath.slice(2)) : requestedPath;
3371
+ return expanded === join4(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
3372
+ }
3262
3373
  async function applyOne(options, file) {
3263
3374
  const label = `${file.id.slice(0, 8)} (${file.path})`;
3264
3375
  if (options.allowedDirectories.length === 0) {
@@ -3267,7 +3378,7 @@ async function applyOne(options, file) {
3267
3378
  message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
3268
3379
  });
3269
3380
  await ack(options, file, "rejected", "file_sync_disabled");
3270
- return false;
3381
+ return NOT_APPLIED;
3271
3382
  }
3272
3383
  if (file.size > MAX_FILE_PUSH_BYTES) {
3273
3384
  options.log({
@@ -3275,12 +3386,12 @@ async function applyOne(options, file) {
3275
3386
  message: `Runner file ${label} rejected: declared ${file.size} bytes, the limit is ${MAX_FILE_PUSH_BYTES}`
3276
3387
  });
3277
3388
  await ack(options, file, "rejected", "file_too_large");
3278
- return false;
3389
+ return NOT_APPLIED;
3279
3390
  }
3280
3391
  const download = await downloadContent(options, file, label);
3281
3392
  if (!download.ok) {
3282
3393
  if (download.terminal) await ack(options, file, "rejected", download.code);
3283
- return false;
3394
+ return NOT_APPLIED;
3284
3395
  }
3285
3396
  let outcome;
3286
3397
  try {
@@ -3296,7 +3407,7 @@ async function applyOne(options, file) {
3296
3407
  message: `Runner file ${label} could not be written: ${describe(err)}`
3297
3408
  });
3298
3409
  await ack(options, file, "rejected", "write_failed");
3299
- return false;
3410
+ return NOT_APPLIED;
3300
3411
  }
3301
3412
  if (!outcome.ok) {
3302
3413
  options.log({
@@ -3304,14 +3415,17 @@ async function applyOne(options, file) {
3304
3415
  message: `Runner file ${label} rejected (${outcome.code}): ${outcome.message}`
3305
3416
  });
3306
3417
  await ack(options, file, "rejected", outcome.code);
3307
- return false;
3418
+ return NOT_APPLIED;
3308
3419
  }
3309
3420
  options.log({
3310
3421
  level: "info",
3311
3422
  message: `Runner file ${label} applied (${download.content.byteLength} bytes)`
3312
3423
  });
3313
3424
  await ack(options, file, "applied");
3314
- return true;
3425
+ return {
3426
+ applied: true,
3427
+ claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
3428
+ };
3315
3429
  }
3316
3430
  function durableDownloadCode(status2) {
3317
3431
  return status2 === 413 ? "file_too_large" : "write_failed";
@@ -3756,6 +3870,14 @@ var ChannelDriver = class _ChannelDriver {
3756
3870
  * same trick `lastProxiedActivityAt` uses.
3757
3871
  */
3758
3872
  appliedFileCount = 0;
3873
+ /**
3874
+ * Generation counter, NOT a tally (#1656): advances by exactly one per sync
3875
+ * batch that applied the Claude CLI credential file, not by how many
3876
+ * credential files were in that batch. `run.ts` only ever tests inequality
3877
+ * against the value it saw last cycle, so magnitude is meaningless — keep it
3878
+ * that way rather than "fixing" it into a count.
3879
+ */
3880
+ claudeCredentialApplyCount = 0;
3759
3881
  /**
3760
3882
  * The currently-executing `drainPending()` promise, or null when idle. Lets a
3761
3883
  * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
@@ -3848,7 +3970,7 @@ var ChannelDriver = class _ChannelDriver {
3848
3970
  if (this.syncingFiles) return 0;
3849
3971
  this.syncingFiles = true;
3850
3972
  try {
3851
- const applied = await syncPendingRunnerFiles({
3973
+ const result = await syncPendingRunnerFiles({
3852
3974
  agentId: this.agentId,
3853
3975
  apiUrl: this.apiUrl,
3854
3976
  getAuthHeader: this.getAuthHeader,
@@ -3858,8 +3980,9 @@ var ChannelDriver = class _ChannelDriver {
3858
3980
  ackFailures: this.fileAckFailures,
3859
3981
  log: this.log
3860
3982
  });
3861
- this.appliedFileCount += applied;
3862
- return applied;
3983
+ this.appliedFileCount += result.applied;
3984
+ if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
3985
+ return result.applied;
3863
3986
  } catch (err) {
3864
3987
  this.log({
3865
3988
  level: "error",
@@ -3950,12 +4073,24 @@ var ChannelDriver = class _ChannelDriver {
3950
4073
  * `appliedFiles` is monotonic so a pull that started AND finished between two
3951
4074
  * idle checks still shows up as an advance.
3952
4075
  *
4076
+ * A THIRD signal, `claudeCredentialApplies`, is a separate re-arm trigger
4077
+ * (#1656), not idle accounting: `run.ts` gates re-probing Claude usage
4078
+ * reporting on it advancing, so an unrelated file sync can never disturb a
4079
+ * healthy reporting cadence (#1627) — it never even reaches that trigger, let
4080
+ * alone gets declined by it. Keep this narrower signal OUT of `appliedFiles`,
4081
+ * whose consumer is idle-timeout suppression and must key on ANY file, not
4082
+ * just a Claude credential.
4083
+ *
3953
4084
  * CALLER CONTRACT: sample `inFlight` BEFORE calling `syncPendingFiles()` for
3954
4085
  * the cycle. `syncPendingFiles` sets the flag synchronously, so a caller that
3955
4086
  * samples afterwards reads `true` every single cycle and can never idle out.
3956
4087
  */
3957
4088
  fileSyncActivity() {
3958
- return { appliedFiles: this.appliedFileCount, inFlight: this.syncingFiles };
4089
+ return {
4090
+ appliedFiles: this.appliedFileCount,
4091
+ inFlight: this.syncingFiles,
4092
+ claudeCredentialApplies: this.claudeCredentialApplyCount
4093
+ };
3959
4094
  }
3960
4095
  /**
3961
4096
  * OpenCode session ids the session-cleanup sweep (issue #190) must NOT delete:
@@ -4815,6 +4950,17 @@ var ChannelDriver = class _ChannelDriver {
4815
4950
  message: `OpenCode session ${bound} for conversation ${conv.id.slice(0, 8)} no longer exists (deleted or DB reset) \u2014 creating a fresh session and rebinding.`,
4816
4951
  conversation_id: conv.id
4817
4952
  });
4953
+ const watcher = this.watchers.get(bound);
4954
+ if (watcher) {
4955
+ for (const [evidentMessageId, inFlight] of [...watcher.inFlight]) {
4956
+ this.recordReleasedOpencodeId(evidentMessageId, bound, inFlight.opencodeMessageId);
4957
+ this.removeInFlight(watcher, evidentMessageId);
4958
+ void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
4959
+ recovery: "session_gone_released"
4960
+ });
4961
+ }
4962
+ this.watchers.delete(bound);
4963
+ }
4818
4964
  this.sessions.delete(conv.id);
4819
4965
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
4820
4966
  }
@@ -7280,7 +7426,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
7280
7426
  if (trimmed === "") {
7281
7427
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
7282
7428
  }
7283
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join4(homeDir, trimmed.slice(2)) : trimmed;
7429
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join5(homeDir, trimmed.slice(2)) : trimmed;
7284
7430
  if (!isAbsolute2(expanded)) {
7285
7431
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
7286
7432
  }
@@ -7487,6 +7633,7 @@ async function driveChannels(state, driver) {
7487
7633
  let unreachableMs = 0;
7488
7634
  let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7489
7635
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
7636
+ let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
7490
7637
  while (state.running) {
7491
7638
  const cycleStartedAtMs = performance.now();
7492
7639
  let idleThisCycle = false;
@@ -7510,11 +7657,15 @@ async function driveChannels(state, driver) {
7510
7657
  state.messageCount += processed;
7511
7658
  const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
7512
7659
  lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7513
- const appliedFiles = driver.fileSyncActivity().appliedFiles;
7660
+ const fileActivitySnapshot = driver.fileSyncActivity();
7661
+ const appliedFiles = fileActivitySnapshot.appliedFiles;
7514
7662
  const filesApplied = appliedFiles !== lastSeenAppliedFiles;
7515
7663
  const fileActivity = carriedOverFileSync || filesApplied;
7516
7664
  lastSeenAppliedFiles = appliedFiles;
7517
- if (filesApplied) state.claudeUsageRearm?.();
7665
+ const claudeCredentialApplies = fileActivitySnapshot.claudeCredentialApplies;
7666
+ const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
7667
+ lastSeenClaudeApplies = claudeCredentialApplies;
7668
+ if (claudeCredentialApplied) state.claudeUsageRearm?.();
7518
7669
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
7519
7670
  idlePolls = 0;
7520
7671
  idleMs = 0;
@@ -7583,7 +7734,7 @@ async function driveChannels(state, driver) {
7583
7734
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
7584
7735
  var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
7585
7736
  function sessionDbPath() {
7586
- return join4(homedir3(), ".local", "share", "opencode", "opencode.db");
7737
+ return join5(homedir3(), ".local", "share", "opencode", "opencode.db");
7587
7738
  }
7588
7739
  async function runSweep(state, driver, config) {
7589
7740
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
@@ -7694,9 +7845,6 @@ function scheduleSessionCleanup(state, driver, options) {
7694
7845
  );
7695
7846
  state.sessionCleanupTimers.push(interval, firstSweep);
7696
7847
  }
7697
- function claudeUsageFailureStreakSuffix(consecutiveFailures) {
7698
- return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : "";
7699
- }
7700
7848
  function scheduleClaudeUsageReporting(state, options) {
7701
7849
  const { mode, warnings } = resolveClaudeUsageReportingMode(
7702
7850
  options.claudeUsageReporting,
@@ -7724,13 +7872,13 @@ function scheduleClaudeUsageReporting(state, options) {
7724
7872
  phase = "probe-pending";
7725
7873
  state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
7726
7874
  };
7727
- const scheduleNextTick = (reportedSuccessfully) => {
7875
+ const scheduleNextTick = () => {
7728
7876
  if (rearmRequested) {
7729
7877
  rearmRequested = false;
7730
7878
  armProbe();
7731
7879
  return;
7732
7880
  }
7733
- phase = reportedSuccessfully ? "steady-pending-healthy" : "steady-pending-retry";
7881
+ phase = "steady-pending";
7734
7882
  state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
7735
7883
  };
7736
7884
  const rearm = () => {
@@ -7740,9 +7888,7 @@ function scheduleClaudeUsageReporting(state, options) {
7740
7888
  return;
7741
7889
  case "probe-pending":
7742
7890
  return;
7743
- case "steady-pending-healthy":
7744
- return;
7745
- case "steady-pending-retry":
7891
+ case "steady-pending":
7746
7892
  if (state.claudeUsageTimer) {
7747
7893
  clearTimeout(state.claudeUsageTimer);
7748
7894
  state.claudeUsageTimer = null;
@@ -7780,10 +7926,10 @@ function scheduleClaudeUsageReporting(state, options) {
7780
7926
  logActivity(state, {
7781
7927
  type: "info",
7782
7928
  level: claudeUsageFailureLogLevel(consecutiveFailures),
7783
- message: `Failed to report Claude usage: ${result.error}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
7929
+ message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
7784
7930
  });
7785
7931
  }
7786
- scheduleNextTick(result.ok);
7932
+ scheduleNextTick();
7787
7933
  } catch (error2) {
7788
7934
  if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
7789
7935
  if (mode === "on") {
@@ -7792,7 +7938,7 @@ function scheduleClaudeUsageReporting(state, options) {
7792
7938
  level: "warn",
7793
7939
  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"
7794
7940
  });
7795
- scheduleNextTick(false);
7941
+ scheduleNextTick();
7796
7942
  } else if (isProbe) {
7797
7943
  logActivity(state, {
7798
7944
  type: "info",
@@ -7807,7 +7953,7 @@ function scheduleClaudeUsageReporting(state, options) {
7807
7953
  level: "debug",
7808
7954
  message: `Claude usage reporting: ${error2.message}`
7809
7955
  });
7810
- scheduleNextTick(false);
7956
+ scheduleNextTick();
7811
7957
  }
7812
7958
  } else {
7813
7959
  consecutiveFailures++;
@@ -7815,15 +7961,92 @@ function scheduleClaudeUsageReporting(state, options) {
7815
7961
  logActivity(state, {
7816
7962
  type: "info",
7817
7963
  level: claudeUsageFailureLogLevel(consecutiveFailures),
7818
- message: `Claude usage reporting failed: ${message}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
7964
+ message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
7819
7965
  });
7820
- scheduleNextTick(false);
7966
+ scheduleNextTick();
7821
7967
  }
7822
7968
  }
7823
7969
  };
7824
7970
  armProbe();
7825
7971
  return rearm;
7826
7972
  }
7973
+ var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
7974
+ var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
7975
+ var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
7976
+ function scheduleResourceUsageReporting(state, options) {
7977
+ const { enabled, warnings } = resolveResourceUsageReportingEnabled(
7978
+ options.resourceUsageReporting,
7979
+ process.env
7980
+ );
7981
+ for (const warning2 of warnings) {
7982
+ logActivity(state, {
7983
+ type: "info",
7984
+ level: "warn",
7985
+ message: `Resource usage reporting: ${warning2}`
7986
+ });
7987
+ }
7988
+ if (!enabled) {
7989
+ logActivity(state, {
7990
+ type: "info",
7991
+ level: "debug",
7992
+ message: "Resource usage reporting is off (--no-resource-usage-reporting)"
7993
+ });
7994
+ return;
7995
+ }
7996
+ const collect = createResourceUsageCollector();
7997
+ let consecutiveFailures = 0;
7998
+ const tick = async () => {
7999
+ try {
8000
+ const usage = collect();
8001
+ const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
8002
+ if (result.ok) {
8003
+ if (consecutiveFailures > 0) {
8004
+ logActivity(state, {
8005
+ type: "info",
8006
+ level: "info",
8007
+ message: "Resource usage reporting recovered"
8008
+ });
8009
+ }
8010
+ consecutiveFailures = 0;
8011
+ logActivity(state, {
8012
+ type: "info",
8013
+ level: "debug",
8014
+ message: "Reported resource usage to Evident"
8015
+ });
8016
+ } else {
8017
+ consecutiveFailures++;
8018
+ logActivity(state, {
8019
+ type: "info",
8020
+ level: reportFailureLogLevel(
8021
+ consecutiveFailures,
8022
+ RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
8023
+ ),
8024
+ message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8025
+ });
8026
+ }
8027
+ } catch (error2) {
8028
+ consecutiveFailures++;
8029
+ const message = error2 instanceof Error ? error2.message : String(error2);
8030
+ logActivity(state, {
8031
+ type: "info",
8032
+ level: reportFailureLogLevel(
8033
+ consecutiveFailures,
8034
+ RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
8035
+ ),
8036
+ message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8037
+ });
8038
+ } finally {
8039
+ state.resourceUsageTimer = setTimeout(
8040
+ () => void tick(),
8041
+ jitteredDelayMs(
8042
+ RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
8043
+ RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
8044
+ )
8045
+ );
8046
+ }
8047
+ };
8048
+ state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
8049
+ }
7827
8050
  async function notifyOffline(state) {
7828
8051
  if (!state.agentId || !state.authHeader) return;
7829
8052
  if (!state.connected) {
@@ -7864,6 +8087,10 @@ async function cleanup(state, opts = {}) {
7864
8087
  state.claudeUsageTimer = null;
7865
8088
  }
7866
8089
  state.claudeUsageRearm = null;
8090
+ if (state.resourceUsageTimer) {
8091
+ clearTimeout(state.resourceUsageTimer);
8092
+ state.resourceUsageTimer = null;
8093
+ }
7867
8094
  if (opts.graceful && state.channelDriver) {
7868
8095
  state.channelDriver.stop();
7869
8096
  log2(state, "Draining in-flight channel work before shutdown...");
@@ -7946,6 +8173,7 @@ async function run(options) {
7946
8173
  sessionCleanupTimers: [],
7947
8174
  claudeUsageTimer: null,
7948
8175
  claudeUsageRearm: null,
8176
+ resourceUsageTimer: null,
7949
8177
  authHeader: ""
7950
8178
  };
7951
8179
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
@@ -8345,6 +8573,7 @@ async function run(options) {
8345
8573
  }
8346
8574
  scheduleSessionCleanup(state, channelDriver, options);
8347
8575
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
8576
+ scheduleResourceUsageReporting(state, options);
8348
8577
  if (!interactive || state.json) {
8349
8578
  log2(state, "Driving channel messages...");
8350
8579
  }
@@ -8425,6 +8654,9 @@ program.command("run").description("Connect to Evident and process messages").op
8425
8654
  ).option(
8426
8655
  "--claude-usage-reporting <mode>",
8427
8656
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
8657
+ ).option(
8658
+ "--no-resource-usage-reporting",
8659
+ "Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off"
8428
8660
  ).option(
8429
8661
  "--enable-file-sync-to <dir>",
8430
8662
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
@@ -8457,6 +8689,9 @@ program.command("run").description("Connect to Evident and process messages").op
8457
8689
  // Raw string — the resolver in run.ts single-sources parsing
8458
8690
  // (resolveClaudeUsageReportingMode).
8459
8691
  claudeUsageReporting: options.claudeUsageReporting,
8692
+ // Raw value — resolution is single-sourced in run.ts's
8693
+ // resolveResourceUsageReportingEnabled.
8694
+ resourceUsageReporting: options.resourceUsageReporting,
8460
8695
  // Raw values — expansion/validation is single-sourced in run.ts's
8461
8696
  // resolveFileSyncDirectories.
8462
8697
  enableFileSyncTo: options.enableFileSyncTo,