@evident-ai/cli 3.3.1-dev.8b3848f → 3.3.1-dev.8e3d9ea

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,35 @@ 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
+ disk_total_bytes: usage.diskTotalBytes,
761
+ disk_free_bytes: usage.diskFreeBytes,
762
+ opencode_db_bytes: usage.opencodeDbBytes
763
+ }),
764
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
765
+ });
766
+ if (!response.ok) {
767
+ const serverMessage = await readErrorMessage(response);
768
+ return {
769
+ ok: false,
770
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
771
+ };
772
+ }
773
+ return { ok: true };
774
+ } catch (error2) {
775
+ return { ok: false, error: describeBestEffortError(error2) };
776
+ }
777
+ }
749
778
  async function getAgentInfo(agentId, authHeader) {
750
779
  const apiUrl = getApiUrlConfig();
751
780
  try {
@@ -934,6 +963,7 @@ import { homedir } from "os";
934
963
  import { join } from "path";
935
964
  var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
936
965
  var KEYCHAIN_SERVICE = "Claude Code-credentials";
966
+ var CLAUDE_CREDENTIALS_SEGMENTS = [".claude", ".credentials.json"];
937
967
  function parseClaudeCliCredentials(raw) {
938
968
  let parsed;
939
969
  try {
@@ -967,7 +997,7 @@ function readClaudeCliCredentials() {
967
997
  }
968
998
  }
969
999
  try {
970
- const raw = readFileSync(join(homedir(), ".claude", ".credentials.json"), "utf-8");
1000
+ const raw = readFileSync(join(homedir(), ...CLAUDE_CREDENTIALS_SEGMENTS), "utf-8");
971
1001
  return parseClaudeCliCredentials(raw);
972
1002
  } catch (err) {
973
1003
  const code = err.code;
@@ -1063,7 +1093,7 @@ async function claudeUsage() {
1063
1093
 
1064
1094
  // src/commands/run.ts
1065
1095
  import { homedir as homedir3 } from "os";
1066
- import { isAbsolute as isAbsolute2, join as join4, parse, resolve as resolvePath } from "path";
1096
+ import { isAbsolute as isAbsolute2, join as join5, parse, resolve as resolvePath } from "path";
1067
1097
  import chalk6 from "chalk";
1068
1098
 
1069
1099
  // ../../packages/types/src/agents/index.ts
@@ -2961,6 +2991,21 @@ function writeTunnelReadyMarker(path, agentId) {
2961
2991
  }
2962
2992
  }
2963
2993
 
2994
+ // src/lib/reporting-schedule.ts
2995
+ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2996
+ const jitterRangeMs = baseMs * jitterFraction;
2997
+ return baseMs - jitterRangeMs + random() * (2 * jitterRangeMs);
2998
+ }
2999
+ function firstReportDelayMs(random = Math.random) {
3000
+ return 5e3 + random() * 1e4;
3001
+ }
3002
+ function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
3003
+ return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
3004
+ }
3005
+ function failureStreakSuffix(consecutiveFailures) {
3006
+ return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : "";
3007
+ }
3008
+
2964
3009
  // src/lib/claude-usage-reporting.ts
2965
3010
  var VALID_MODES = ["auto", "on", "off"];
2966
3011
  function resolveClaudeUsageReportingMode(flagValue, env) {
@@ -2983,18 +3028,175 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
2983
3028
  var BASE_REPORT_DELAY_MS = 10 * 6e4;
2984
3029
  var REPORT_DELAY_JITTER_FRACTION = 0.2;
2985
3030
  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);
3031
+ return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);
2988
3032
  }
2989
- var FIRST_REPORT_DELAY_MS = 5e3 + Math.random() * 1e4;
3033
+ var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
2990
3034
  var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
2991
3035
  function claudeUsageFailureLogLevel(consecutiveFailures) {
2992
- return consecutiveFailures === 1 || consecutiveFailures % CLAUDE_USAGE_FAILURE_REESCALATION_TICKS === 0 ? "warn" : "debug";
3036
+ return reportFailureLogLevel(consecutiveFailures, CLAUDE_USAGE_FAILURE_REESCALATION_TICKS);
3037
+ }
3038
+
3039
+ // src/lib/resource-usage-reporting.ts
3040
+ var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
3041
+ var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
3042
+ function resolveResourceUsageReportingEnabled(flagValue, env) {
3043
+ if (flagValue === false) {
3044
+ return { enabled: false, warnings: [] };
3045
+ }
3046
+ const raw = env.EVIDENT_RESOURCE_USAGE_REPORTING;
3047
+ if (raw === void 0 || raw === "") {
3048
+ return { enabled: true, warnings: [] };
3049
+ }
3050
+ const normalized = raw.trim().toLowerCase();
3051
+ if (DISABLED_VALUES.has(normalized)) {
3052
+ return { enabled: false, warnings: [] };
3053
+ }
3054
+ if (ENABLED_VALUES.has(normalized)) {
3055
+ return { enabled: true, warnings: [] };
3056
+ }
3057
+ return {
3058
+ enabled: true,
3059
+ warnings: [
3060
+ `Ignoring invalid EVIDENT_RESOURCE_USAGE_REPORTING "${raw}": expected on or off; leaving reporting on`
3061
+ ]
3062
+ };
3063
+ }
3064
+
3065
+ // src/lib/resource-usage.ts
3066
+ import { cpus, totalmem, freemem } from "os";
3067
+ import { statfsSync as statfsSync2 } from "fs";
3068
+
3069
+ // src/lib/ecs-task-metadata.ts
3070
+ var ECS_METADATA_TIMEOUT_MS = 2e3;
3071
+ function parseEcsTaskLimits(payload) {
3072
+ if (typeof payload !== "object" || payload === null) return null;
3073
+ const limits = payload.Limits;
3074
+ if (typeof limits !== "object" || limits === null) return null;
3075
+ const cpu = limits.CPU;
3076
+ const memory = limits.Memory;
3077
+ if (typeof cpu !== "number" || !Number.isFinite(cpu) || cpu <= 0) return null;
3078
+ if (typeof memory !== "number" || !Number.isFinite(memory) || memory <= 0) return null;
3079
+ return {
3080
+ cpuCount: Math.max(1, Math.round(cpu)),
3081
+ memoryTotalBytes: memory * 1024 * 1024
3082
+ };
3083
+ }
3084
+ async function readEcsTaskLimits(env) {
3085
+ const uri = env.ECS_CONTAINER_METADATA_URI_V4;
3086
+ if (!uri) {
3087
+ return { limits: null };
3088
+ }
3089
+ const url = `${uri}/task`;
3090
+ try {
3091
+ const response = await fetch(url, { signal: AbortSignal.timeout(ECS_METADATA_TIMEOUT_MS) });
3092
+ if (!response.ok) {
3093
+ return {
3094
+ limits: null,
3095
+ warning: `ECS task metadata fetch (${url}) returned HTTP ${response.status}`
3096
+ };
3097
+ }
3098
+ const payload = await response.json();
3099
+ const limits = parseEcsTaskLimits(payload);
3100
+ if (limits === null) {
3101
+ return {
3102
+ limits: null,
3103
+ warning: `ECS task metadata fetch (${url}) returned an unexpected payload`
3104
+ };
3105
+ }
3106
+ return { limits };
3107
+ } catch (error2) {
3108
+ const message = error2 instanceof Error ? error2.message : String(error2);
3109
+ return { limits: null, warning: `ECS task metadata fetch (${url}) failed: ${message}` };
3110
+ }
3111
+ }
3112
+
3113
+ // src/lib/resource-usage.ts
3114
+ function readCpuSample() {
3115
+ let busyMs = 0;
3116
+ let idleMs = 0;
3117
+ for (const cpu of cpus()) {
3118
+ busyMs += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq;
3119
+ idleMs += cpu.times.idle;
3120
+ }
3121
+ return { busyMs, idleMs };
3122
+ }
3123
+ function cpuPercentBetween(previous, current) {
3124
+ const deltaBusy = current.busyMs - previous.busyMs;
3125
+ const deltaIdle = current.idleMs - previous.idleMs;
3126
+ const total = deltaBusy + deltaIdle;
3127
+ if (total === 0) return null;
3128
+ return Math.round((deltaBusy / total * 100 + Number.EPSILON) * 100) / 100;
3129
+ }
3130
+ function clamp(value, min, max) {
3131
+ return Math.min(Math.max(value, min), max);
3132
+ }
3133
+ function round2(value) {
3134
+ return Math.round((value + Number.EPSILON) * 100) / 100;
3135
+ }
3136
+ function readDisk(homeDir) {
3137
+ try {
3138
+ const stats = statfsSync2(homeDir);
3139
+ return {
3140
+ totalBytes: stats.bsize * stats.blocks,
3141
+ freeBytes: stats.bsize * stats.bavail
3142
+ };
3143
+ } catch (error2) {
3144
+ const message = error2 instanceof Error ? error2.message : String(error2);
3145
+ return {
3146
+ totalBytes: null,
3147
+ freeBytes: null,
3148
+ warning: `Could not read disk usage for ${homeDir}: ${message}`
3149
+ };
3150
+ }
3151
+ }
3152
+ function createResourceUsageCollector(homeDir) {
3153
+ let previous = readCpuSample();
3154
+ return async () => {
3155
+ const current = readCpuSample();
3156
+ const hostCpuPercent = cpuPercentBetween(previous, current);
3157
+ const hostCpuCount = cpus().length;
3158
+ previous = current;
3159
+ const disk = readDisk(homeDir);
3160
+ const opencodeDbBytes = statSessionDbBytes(homeDir);
3161
+ const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
3162
+ const warnings = [];
3163
+ if (disk.warning) warnings.push(disk.warning);
3164
+ if (ecsWarning) warnings.push(ecsWarning);
3165
+ let cpuPercent = hostCpuPercent;
3166
+ let cpuCount = hostCpuCount;
3167
+ let memoryTotalBytes = totalmem();
3168
+ let memoryAvailableBytes = freemem();
3169
+ if (limits !== null) {
3170
+ cpuCount = limits.cpuCount;
3171
+ memoryTotalBytes = limits.memoryTotalBytes;
3172
+ memoryAvailableBytes = clamp(
3173
+ limits.memoryTotalBytes - (totalmem() - freemem()),
3174
+ 0,
3175
+ limits.memoryTotalBytes
3176
+ );
3177
+ cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
3178
+ }
3179
+ return {
3180
+ usage: {
3181
+ cpuPercent,
3182
+ cpuCount,
3183
+ memoryTotalBytes,
3184
+ memoryAvailableBytes,
3185
+ diskTotalBytes: disk.totalBytes,
3186
+ diskFreeBytes: disk.freeBytes,
3187
+ opencodeDbBytes
3188
+ },
3189
+ warnings
3190
+ };
3191
+ };
2993
3192
  }
2994
3193
 
2995
3194
  // src/lib/channels/driver.ts
2996
3195
  import { homedir as homedir2 } from "os";
2997
3196
 
3197
+ // src/lib/runner-file-sync.ts
3198
+ import { join as join4 } from "path";
3199
+
2998
3200
  // src/lib/file-push.ts
2999
3201
  import { randomUUID } from "crypto";
3000
3202
  import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
@@ -3188,17 +3390,20 @@ async function syncPendingRunnerFiles(options) {
3188
3390
  for (const id of options.ackFailures.keys()) {
3189
3391
  if (!pendingIds.has(id)) options.ackFailures.delete(id);
3190
3392
  }
3191
- if (pending.length === 0) return 0;
3393
+ if (pending.length === 0) return { applied: 0, claudeCredentialApplied: false };
3192
3394
  options.log({
3193
3395
  level: "info",
3194
3396
  message: `Runner file sync: ${pending.length} file(s) queued for this runner`
3195
3397
  });
3196
3398
  let applied = 0;
3399
+ let claudeCredentialApplied = false;
3197
3400
  for (const file of pending) {
3198
3401
  if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
3199
- if (await applyOne(options, file)) applied += 1;
3402
+ const outcome = await applyOne(options, file);
3403
+ if (outcome.applied) applied += 1;
3404
+ if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
3200
3405
  }
3201
- return applied;
3406
+ return { applied, claudeCredentialApplied };
3202
3407
  }
3203
3408
  async function listPendingFiles(options) {
3204
3409
  let res;
@@ -3259,6 +3464,11 @@ function asPendingFile(entry) {
3259
3464
  if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
3260
3465
  return { id, path, size };
3261
3466
  }
3467
+ var NOT_APPLIED = { applied: false, claudeCredentialApplied: false };
3468
+ 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);
3471
+ }
3262
3472
  async function applyOne(options, file) {
3263
3473
  const label = `${file.id.slice(0, 8)} (${file.path})`;
3264
3474
  if (options.allowedDirectories.length === 0) {
@@ -3267,7 +3477,7 @@ async function applyOne(options, file) {
3267
3477
  message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
3268
3478
  });
3269
3479
  await ack(options, file, "rejected", "file_sync_disabled");
3270
- return false;
3480
+ return NOT_APPLIED;
3271
3481
  }
3272
3482
  if (file.size > MAX_FILE_PUSH_BYTES) {
3273
3483
  options.log({
@@ -3275,12 +3485,12 @@ async function applyOne(options, file) {
3275
3485
  message: `Runner file ${label} rejected: declared ${file.size} bytes, the limit is ${MAX_FILE_PUSH_BYTES}`
3276
3486
  });
3277
3487
  await ack(options, file, "rejected", "file_too_large");
3278
- return false;
3488
+ return NOT_APPLIED;
3279
3489
  }
3280
3490
  const download = await downloadContent(options, file, label);
3281
3491
  if (!download.ok) {
3282
3492
  if (download.terminal) await ack(options, file, "rejected", download.code);
3283
- return false;
3493
+ return NOT_APPLIED;
3284
3494
  }
3285
3495
  let outcome;
3286
3496
  try {
@@ -3296,7 +3506,7 @@ async function applyOne(options, file) {
3296
3506
  message: `Runner file ${label} could not be written: ${describe(err)}`
3297
3507
  });
3298
3508
  await ack(options, file, "rejected", "write_failed");
3299
- return false;
3509
+ return NOT_APPLIED;
3300
3510
  }
3301
3511
  if (!outcome.ok) {
3302
3512
  options.log({
@@ -3304,14 +3514,17 @@ async function applyOne(options, file) {
3304
3514
  message: `Runner file ${label} rejected (${outcome.code}): ${outcome.message}`
3305
3515
  });
3306
3516
  await ack(options, file, "rejected", outcome.code);
3307
- return false;
3517
+ return NOT_APPLIED;
3308
3518
  }
3309
3519
  options.log({
3310
3520
  level: "info",
3311
3521
  message: `Runner file ${label} applied (${download.content.byteLength} bytes)`
3312
3522
  });
3313
3523
  await ack(options, file, "applied");
3314
- return true;
3524
+ return {
3525
+ applied: true,
3526
+ claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
3527
+ };
3315
3528
  }
3316
3529
  function durableDownloadCode(status2) {
3317
3530
  return status2 === 413 ? "file_too_large" : "write_failed";
@@ -3756,6 +3969,14 @@ var ChannelDriver = class _ChannelDriver {
3756
3969
  * same trick `lastProxiedActivityAt` uses.
3757
3970
  */
3758
3971
  appliedFileCount = 0;
3972
+ /**
3973
+ * Generation counter, NOT a tally (#1656): advances by exactly one per sync
3974
+ * batch that applied the Claude CLI credential file, not by how many
3975
+ * credential files were in that batch. `run.ts` only ever tests inequality
3976
+ * against the value it saw last cycle, so magnitude is meaningless — keep it
3977
+ * that way rather than "fixing" it into a count.
3978
+ */
3979
+ claudeCredentialApplyCount = 0;
3759
3980
  /**
3760
3981
  * The currently-executing `drainPending()` promise, or null when idle. Lets a
3761
3982
  * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
@@ -3848,7 +4069,7 @@ var ChannelDriver = class _ChannelDriver {
3848
4069
  if (this.syncingFiles) return 0;
3849
4070
  this.syncingFiles = true;
3850
4071
  try {
3851
- const applied = await syncPendingRunnerFiles({
4072
+ const result = await syncPendingRunnerFiles({
3852
4073
  agentId: this.agentId,
3853
4074
  apiUrl: this.apiUrl,
3854
4075
  getAuthHeader: this.getAuthHeader,
@@ -3858,8 +4079,9 @@ var ChannelDriver = class _ChannelDriver {
3858
4079
  ackFailures: this.fileAckFailures,
3859
4080
  log: this.log
3860
4081
  });
3861
- this.appliedFileCount += applied;
3862
- return applied;
4082
+ this.appliedFileCount += result.applied;
4083
+ if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
4084
+ return result.applied;
3863
4085
  } catch (err) {
3864
4086
  this.log({
3865
4087
  level: "error",
@@ -3950,12 +4172,24 @@ var ChannelDriver = class _ChannelDriver {
3950
4172
  * `appliedFiles` is monotonic so a pull that started AND finished between two
3951
4173
  * idle checks still shows up as an advance.
3952
4174
  *
4175
+ * A THIRD signal, `claudeCredentialApplies`, is a separate re-arm trigger
4176
+ * (#1656), not idle accounting: `run.ts` gates re-probing Claude usage
4177
+ * reporting on it advancing, so an unrelated file sync can never disturb a
4178
+ * healthy reporting cadence (#1627) — it never even reaches that trigger, let
4179
+ * alone gets declined by it. Keep this narrower signal OUT of `appliedFiles`,
4180
+ * whose consumer is idle-timeout suppression and must key on ANY file, not
4181
+ * just a Claude credential.
4182
+ *
3953
4183
  * CALLER CONTRACT: sample `inFlight` BEFORE calling `syncPendingFiles()` for
3954
4184
  * the cycle. `syncPendingFiles` sets the flag synchronously, so a caller that
3955
4185
  * samples afterwards reads `true` every single cycle and can never idle out.
3956
4186
  */
3957
4187
  fileSyncActivity() {
3958
- return { appliedFiles: this.appliedFileCount, inFlight: this.syncingFiles };
4188
+ return {
4189
+ appliedFiles: this.appliedFileCount,
4190
+ inFlight: this.syncingFiles,
4191
+ claudeCredentialApplies: this.claudeCredentialApplyCount
4192
+ };
3959
4193
  }
3960
4194
  /**
3961
4195
  * OpenCode session ids the session-cleanup sweep (issue #190) must NOT delete:
@@ -4815,6 +5049,17 @@ var ChannelDriver = class _ChannelDriver {
4815
5049
  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
5050
  conversation_id: conv.id
4817
5051
  });
5052
+ const watcher = this.watchers.get(bound);
5053
+ if (watcher) {
5054
+ for (const [evidentMessageId, inFlight] of [...watcher.inFlight]) {
5055
+ this.recordReleasedOpencodeId(evidentMessageId, bound, inFlight.opencodeMessageId);
5056
+ this.removeInFlight(watcher, evidentMessageId);
5057
+ void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
5058
+ recovery: "session_gone_released"
5059
+ });
5060
+ }
5061
+ this.watchers.delete(bound);
5062
+ }
4818
5063
  this.sessions.delete(conv.id);
4819
5064
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
4820
5065
  }
@@ -7280,7 +7525,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
7280
7525
  if (trimmed === "") {
7281
7526
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
7282
7527
  }
7283
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join4(homeDir, trimmed.slice(2)) : trimmed;
7528
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join5(homeDir, trimmed.slice(2)) : trimmed;
7284
7529
  if (!isAbsolute2(expanded)) {
7285
7530
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
7286
7531
  }
@@ -7487,6 +7732,7 @@ async function driveChannels(state, driver) {
7487
7732
  let unreachableMs = 0;
7488
7733
  let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7489
7734
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
7735
+ let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
7490
7736
  while (state.running) {
7491
7737
  const cycleStartedAtMs = performance.now();
7492
7738
  let idleThisCycle = false;
@@ -7510,11 +7756,15 @@ async function driveChannels(state, driver) {
7510
7756
  state.messageCount += processed;
7511
7757
  const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
7512
7758
  lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7513
- const appliedFiles = driver.fileSyncActivity().appliedFiles;
7759
+ const fileActivitySnapshot = driver.fileSyncActivity();
7760
+ const appliedFiles = fileActivitySnapshot.appliedFiles;
7514
7761
  const filesApplied = appliedFiles !== lastSeenAppliedFiles;
7515
7762
  const fileActivity = carriedOverFileSync || filesApplied;
7516
7763
  lastSeenAppliedFiles = appliedFiles;
7517
- if (filesApplied) state.claudeUsageRearm?.();
7764
+ const claudeCredentialApplies = fileActivitySnapshot.claudeCredentialApplies;
7765
+ const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
7766
+ lastSeenClaudeApplies = claudeCredentialApplies;
7767
+ if (claudeCredentialApplied) state.claudeUsageRearm?.();
7518
7768
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
7519
7769
  idlePolls = 0;
7520
7770
  idleMs = 0;
@@ -7583,7 +7833,7 @@ async function driveChannels(state, driver) {
7583
7833
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
7584
7834
  var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
7585
7835
  function sessionDbPath() {
7586
- return join4(homedir3(), ".local", "share", "opencode", "opencode.db");
7836
+ return join5(homedir3(), ".local", "share", "opencode", "opencode.db");
7587
7837
  }
7588
7838
  async function runSweep(state, driver, config) {
7589
7839
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
@@ -7694,9 +7944,6 @@ function scheduleSessionCleanup(state, driver, options) {
7694
7944
  );
7695
7945
  state.sessionCleanupTimers.push(interval, firstSweep);
7696
7946
  }
7697
- function claudeUsageFailureStreakSuffix(consecutiveFailures) {
7698
- return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : "";
7699
- }
7700
7947
  function scheduleClaudeUsageReporting(state, options) {
7701
7948
  const { mode, warnings } = resolveClaudeUsageReportingMode(
7702
7949
  options.claudeUsageReporting,
@@ -7724,13 +7971,13 @@ function scheduleClaudeUsageReporting(state, options) {
7724
7971
  phase = "probe-pending";
7725
7972
  state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
7726
7973
  };
7727
- const scheduleNextTick = (reportedSuccessfully) => {
7974
+ const scheduleNextTick = () => {
7728
7975
  if (rearmRequested) {
7729
7976
  rearmRequested = false;
7730
7977
  armProbe();
7731
7978
  return;
7732
7979
  }
7733
- phase = reportedSuccessfully ? "steady-pending-healthy" : "steady-pending-retry";
7980
+ phase = "steady-pending";
7734
7981
  state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
7735
7982
  };
7736
7983
  const rearm = () => {
@@ -7740,9 +7987,7 @@ function scheduleClaudeUsageReporting(state, options) {
7740
7987
  return;
7741
7988
  case "probe-pending":
7742
7989
  return;
7743
- case "steady-pending-healthy":
7744
- return;
7745
- case "steady-pending-retry":
7990
+ case "steady-pending":
7746
7991
  if (state.claudeUsageTimer) {
7747
7992
  clearTimeout(state.claudeUsageTimer);
7748
7993
  state.claudeUsageTimer = null;
@@ -7780,10 +8025,10 @@ function scheduleClaudeUsageReporting(state, options) {
7780
8025
  logActivity(state, {
7781
8026
  type: "info",
7782
8027
  level: claudeUsageFailureLogLevel(consecutiveFailures),
7783
- message: `Failed to report Claude usage: ${result.error}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
8028
+ message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
7784
8029
  });
7785
8030
  }
7786
- scheduleNextTick(result.ok);
8031
+ scheduleNextTick();
7787
8032
  } catch (error2) {
7788
8033
  if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
7789
8034
  if (mode === "on") {
@@ -7792,7 +8037,7 @@ function scheduleClaudeUsageReporting(state, options) {
7792
8037
  level: "warn",
7793
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"
7794
8039
  });
7795
- scheduleNextTick(false);
8040
+ scheduleNextTick();
7796
8041
  } else if (isProbe) {
7797
8042
  logActivity(state, {
7798
8043
  type: "info",
@@ -7807,7 +8052,7 @@ function scheduleClaudeUsageReporting(state, options) {
7807
8052
  level: "debug",
7808
8053
  message: `Claude usage reporting: ${error2.message}`
7809
8054
  });
7810
- scheduleNextTick(false);
8055
+ scheduleNextTick();
7811
8056
  }
7812
8057
  } else {
7813
8058
  consecutiveFailures++;
@@ -7815,15 +8060,99 @@ function scheduleClaudeUsageReporting(state, options) {
7815
8060
  logActivity(state, {
7816
8061
  type: "info",
7817
8062
  level: claudeUsageFailureLogLevel(consecutiveFailures),
7818
- message: `Claude usage reporting failed: ${message}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
8063
+ message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
7819
8064
  });
7820
- scheduleNextTick(false);
8065
+ scheduleNextTick();
7821
8066
  }
7822
8067
  }
7823
8068
  };
7824
8069
  armProbe();
7825
8070
  return rearm;
7826
8071
  }
8072
+ var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
8073
+ var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
8074
+ var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
8075
+ function scheduleResourceUsageReporting(state, options) {
8076
+ const { enabled, warnings } = resolveResourceUsageReportingEnabled(
8077
+ options.resourceUsageReporting,
8078
+ process.env
8079
+ );
8080
+ for (const warning2 of warnings) {
8081
+ logActivity(state, {
8082
+ type: "info",
8083
+ level: "warn",
8084
+ message: `Resource usage reporting: ${warning2}`
8085
+ });
8086
+ }
8087
+ if (!enabled) {
8088
+ logActivity(state, {
8089
+ type: "info",
8090
+ level: "debug",
8091
+ message: "Resource usage reporting is off (--no-resource-usage-reporting)"
8092
+ });
8093
+ return;
8094
+ }
8095
+ const collect = createResourceUsageCollector(homedir3());
8096
+ let consecutiveFailures = 0;
8097
+ const tick = async () => {
8098
+ try {
8099
+ const { usage, warnings: collectWarnings } = await collect();
8100
+ for (const warning2 of collectWarnings) {
8101
+ logActivity(state, {
8102
+ type: "info",
8103
+ level: "debug",
8104
+ message: `Resource usage collection: ${warning2}`
8105
+ });
8106
+ }
8107
+ const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
8108
+ if (result.ok) {
8109
+ if (consecutiveFailures > 0) {
8110
+ logActivity(state, {
8111
+ type: "info",
8112
+ level: "info",
8113
+ message: "Resource usage reporting recovered"
8114
+ });
8115
+ }
8116
+ consecutiveFailures = 0;
8117
+ logActivity(state, {
8118
+ type: "info",
8119
+ level: "debug",
8120
+ message: "Reported resource usage to Evident"
8121
+ });
8122
+ } else {
8123
+ consecutiveFailures++;
8124
+ logActivity(state, {
8125
+ type: "info",
8126
+ level: reportFailureLogLevel(
8127
+ consecutiveFailures,
8128
+ RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
8129
+ ),
8130
+ message: `Failed to report resource usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8131
+ });
8132
+ }
8133
+ } catch (error2) {
8134
+ consecutiveFailures++;
8135
+ const message = error2 instanceof Error ? error2.message : String(error2);
8136
+ logActivity(state, {
8137
+ type: "info",
8138
+ level: reportFailureLogLevel(
8139
+ consecutiveFailures,
8140
+ RESOURCE_USAGE_FAILURE_REESCALATION_TICKS
8141
+ ),
8142
+ message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8143
+ });
8144
+ } finally {
8145
+ state.resourceUsageTimer = setTimeout(
8146
+ () => void tick(),
8147
+ jitteredDelayMs(
8148
+ RESOURCE_USAGE_BASE_REPORT_DELAY_MS,
8149
+ RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION
8150
+ )
8151
+ );
8152
+ }
8153
+ };
8154
+ state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
8155
+ }
7827
8156
  async function notifyOffline(state) {
7828
8157
  if (!state.agentId || !state.authHeader) return;
7829
8158
  if (!state.connected) {
@@ -7864,6 +8193,10 @@ async function cleanup(state, opts = {}) {
7864
8193
  state.claudeUsageTimer = null;
7865
8194
  }
7866
8195
  state.claudeUsageRearm = null;
8196
+ if (state.resourceUsageTimer) {
8197
+ clearTimeout(state.resourceUsageTimer);
8198
+ state.resourceUsageTimer = null;
8199
+ }
7867
8200
  if (opts.graceful && state.channelDriver) {
7868
8201
  state.channelDriver.stop();
7869
8202
  log2(state, "Draining in-flight channel work before shutdown...");
@@ -7946,6 +8279,7 @@ async function run(options) {
7946
8279
  sessionCleanupTimers: [],
7947
8280
  claudeUsageTimer: null,
7948
8281
  claudeUsageRearm: null,
8282
+ resourceUsageTimer: null,
7949
8283
  authHeader: ""
7950
8284
  };
7951
8285
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
@@ -8345,6 +8679,7 @@ async function run(options) {
8345
8679
  }
8346
8680
  scheduleSessionCleanup(state, channelDriver, options);
8347
8681
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
8682
+ scheduleResourceUsageReporting(state, options);
8348
8683
  if (!interactive || state.json) {
8349
8684
  log2(state, "Driving channel messages...");
8350
8685
  }
@@ -8425,6 +8760,9 @@ program.command("run").description("Connect to Evident and process messages").op
8425
8760
  ).option(
8426
8761
  "--claude-usage-reporting <mode>",
8427
8762
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
8763
+ ).option(
8764
+ "--no-resource-usage-reporting",
8765
+ "Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off"
8428
8766
  ).option(
8429
8767
  "--enable-file-sync-to <dir>",
8430
8768
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
@@ -8457,6 +8795,9 @@ program.command("run").description("Connect to Evident and process messages").op
8457
8795
  // Raw string — the resolver in run.ts single-sources parsing
8458
8796
  // (resolveClaudeUsageReportingMode).
8459
8797
  claudeUsageReporting: options.claudeUsageReporting,
8798
+ // Raw value — resolution is single-sourced in run.ts's
8799
+ // resolveResourceUsageReportingEnabled.
8800
+ resourceUsageReporting: options.resourceUsageReporting,
8460
8801
  // Raw values — expansion/validation is single-sourced in run.ts's
8461
8802
  // resolveFileSyncDirectories.
8462
8803
  enableFileSyncTo: options.enableFileSyncTo,