@evident-ai/cli 3.3.1-dev.dd703c3 → 3.4.0

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, core count, disk space, and the
110
+ OpenCode session-store size to Evident, so it shows on the runner page. On
111
+ by default; nothing else about the machine leaves it. Env:
112
+ `EVIDENT_RESOURCE_USAGE_REPORTING=off` (the flag wins if 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,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 {
@@ -2962,6 +2991,21 @@ function writeTunnelReadyMarker(path, agentId) {
2962
2991
  }
2963
2992
  }
2964
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
+
2965
3009
  // src/lib/claude-usage-reporting.ts
2966
3010
  var VALID_MODES = ["auto", "on", "off"];
2967
3011
  function resolveClaudeUsageReportingMode(flagValue, env) {
@@ -2984,13 +3028,167 @@ function resolveClaudeUsageReportingMode(flagValue, env) {
2984
3028
  var BASE_REPORT_DELAY_MS = 10 * 6e4;
2985
3029
  var REPORT_DELAY_JITTER_FRACTION = 0.2;
2986
3030
  function nextReportDelayMs(random = Math.random) {
2987
- const jitterRangeMs = BASE_REPORT_DELAY_MS * REPORT_DELAY_JITTER_FRACTION;
2988
- return BASE_REPORT_DELAY_MS - jitterRangeMs + random() * (2 * jitterRangeMs);
3031
+ return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);
2989
3032
  }
2990
- var FIRST_REPORT_DELAY_MS = 5e3 + Math.random() * 1e4;
3033
+ var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
2991
3034
  var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
2992
3035
  function claudeUsageFailureLogLevel(consecutiveFailures) {
2993
- 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
+ };
2994
3192
  }
2995
3193
 
2996
3194
  // src/lib/channels/driver.ts
@@ -4851,6 +5049,17 @@ var ChannelDriver = class _ChannelDriver {
4851
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.`,
4852
5050
  conversation_id: conv.id
4853
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
+ }
4854
5063
  this.sessions.delete(conv.id);
4855
5064
  return { sessionId: await this.createAndBindSession(conv.id), created: true };
4856
5065
  }
@@ -7735,9 +7944,6 @@ function scheduleSessionCleanup(state, driver, options) {
7735
7944
  );
7736
7945
  state.sessionCleanupTimers.push(interval, firstSweep);
7737
7946
  }
7738
- function claudeUsageFailureStreakSuffix(consecutiveFailures) {
7739
- return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : "";
7740
- }
7741
7947
  function scheduleClaudeUsageReporting(state, options) {
7742
7948
  const { mode, warnings } = resolveClaudeUsageReportingMode(
7743
7949
  options.claudeUsageReporting,
@@ -7819,7 +8025,7 @@ function scheduleClaudeUsageReporting(state, options) {
7819
8025
  logActivity(state, {
7820
8026
  type: "info",
7821
8027
  level: claudeUsageFailureLogLevel(consecutiveFailures),
7822
- message: `Failed to report Claude usage: ${result.error}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
8028
+ message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
7823
8029
  });
7824
8030
  }
7825
8031
  scheduleNextTick();
@@ -7854,7 +8060,7 @@ function scheduleClaudeUsageReporting(state, options) {
7854
8060
  logActivity(state, {
7855
8061
  type: "info",
7856
8062
  level: claudeUsageFailureLogLevel(consecutiveFailures),
7857
- message: `Claude usage reporting failed: ${message}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
8063
+ message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
7858
8064
  });
7859
8065
  scheduleNextTick();
7860
8066
  }
@@ -7863,6 +8069,90 @@ function scheduleClaudeUsageReporting(state, options) {
7863
8069
  armProbe();
7864
8070
  return rearm;
7865
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
+ }
7866
8156
  async function notifyOffline(state) {
7867
8157
  if (!state.agentId || !state.authHeader) return;
7868
8158
  if (!state.connected) {
@@ -7903,6 +8193,10 @@ async function cleanup(state, opts = {}) {
7903
8193
  state.claudeUsageTimer = null;
7904
8194
  }
7905
8195
  state.claudeUsageRearm = null;
8196
+ if (state.resourceUsageTimer) {
8197
+ clearTimeout(state.resourceUsageTimer);
8198
+ state.resourceUsageTimer = null;
8199
+ }
7906
8200
  if (opts.graceful && state.channelDriver) {
7907
8201
  state.channelDriver.stop();
7908
8202
  log2(state, "Draining in-flight channel work before shutdown...");
@@ -7985,6 +8279,7 @@ async function run(options) {
7985
8279
  sessionCleanupTimers: [],
7986
8280
  claudeUsageTimer: null,
7987
8281
  claudeUsageRearm: null,
8282
+ resourceUsageTimer: null,
7988
8283
  authHeader: ""
7989
8284
  };
7990
8285
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
@@ -8384,6 +8679,7 @@ async function run(options) {
8384
8679
  }
8385
8680
  scheduleSessionCleanup(state, channelDriver, options);
8386
8681
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
8682
+ scheduleResourceUsageReporting(state, options);
8387
8683
  if (!interactive || state.json) {
8388
8684
  log2(state, "Driving channel messages...");
8389
8685
  }
@@ -8464,6 +8760,9 @@ program.command("run").description("Connect to Evident and process messages").op
8464
8760
  ).option(
8465
8761
  "--claude-usage-reporting <mode>",
8466
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"
8467
8766
  ).option(
8468
8767
  "--enable-file-sync-to <dir>",
8469
8768
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
@@ -8496,6 +8795,9 @@ program.command("run").description("Connect to Evident and process messages").op
8496
8795
  // Raw string — the resolver in run.ts single-sources parsing
8497
8796
  // (resolveClaudeUsageReportingMode).
8498
8797
  claudeUsageReporting: options.claudeUsageReporting,
8798
+ // Raw value — resolution is single-sourced in run.ts's
8799
+ // resolveResourceUsageReportingEnabled.
8800
+ resourceUsageReporting: options.resourceUsageReporting,
8499
8801
  // Raw values — expansion/validation is single-sourced in run.ts's
8500
8802
  // resolveFileSyncDirectories.
8501
8803
  enableFileSyncTo: options.enableFileSyncTo,