ever-works-node 0.2.4 → 0.2.5

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.
Files changed (2) hide show
  1. package/cli.js +622 -22
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -5006,6 +5006,41 @@ function resolveExclusiveAgentCredentials(grantedNames, env) {
5006
5006
  return { names: grantedNames.filter((name) => !dropped.has(name)), notes };
5007
5007
  }
5008
5008
  __name(resolveExclusiveAgentCredentials, "resolveExclusiveAgentCredentials");
5009
+ var CONVENTIONAL_SUBJECT = /^([a-z]+)(\([^)\n]{1,40}\))?(!)?:\s+(\S.*)$/;
5010
+ function classifyFleetChangedPaths(paths) {
5011
+ const list = (paths ?? []).map((path2) => path2.replace(/\\/g, "/").trim()).filter(Boolean);
5012
+ if (list.length === 0) return "none";
5013
+ if (list.every(isDocsPath)) return "docs";
5014
+ if (list.every(isTestPath)) return "test";
5015
+ return "mixed";
5016
+ }
5017
+ __name(classifyFleetChangedPaths, "classifyFleetChangedPaths");
5018
+ function retypeFleetCommitMessage(message, changedPaths) {
5019
+ if (typeof message !== "string" || !message) return message;
5020
+ const newline = message.indexOf("\n");
5021
+ const firstLine = newline === -1 ? message : message.slice(0, newline);
5022
+ const rest = newline === -1 ? "" : message.slice(newline);
5023
+ const parsed = CONVENTIONAL_SUBJECT.exec(firstLine);
5024
+ if (!parsed) return message;
5025
+ const change = classifyFleetChangedPaths(changedPaths);
5026
+ const type = change === "docs" ? "docs" : change === "test" ? "test" : null;
5027
+ if (!type || parsed[1] === type) return message;
5028
+ return `${type}${firstLine.slice(parsed[1].length)}${rest}`;
5029
+ }
5030
+ __name(retypeFleetCommitMessage, "retypeFleetCommitMessage");
5031
+ var DOC_EXTENSIONS = /\.(?:md|mdx|markdown|rst|adoc|txt)$/i;
5032
+ var DOC_DIRECTORY = /(?:^|\/)(?:docs?|documentation)\//i;
5033
+ var DOC_FILENAMES = /(?:^|\/)(?:readme|changelog|contributing|license|licence|notice|authors|code_of_conduct)(?:\.[a-z]+)?$/i;
5034
+ function isDocsPath(path2) {
5035
+ return DOC_EXTENSIONS.test(path2) || DOC_DIRECTORY.test(path2) || DOC_FILENAMES.test(path2);
5036
+ }
5037
+ __name(isDocsPath, "isDocsPath");
5038
+ var TEST_FILE = /(?:^|[./_-])(?:spec|test|tests|e2e)\.[cm]?[jt]sx?$|_test\.(?:go|py|rb)$|(?:^|\/)test_[^/]+\.py$/i;
5039
+ var TEST_DIRECTORY = /(?:^|\/)(?:__tests__|__snapshots__|__mocks__|__fixtures__|tests?|e2e|spec|fixtures)\//i;
5040
+ function isTestPath(path2) {
5041
+ return TEST_FILE.test(path2) || TEST_DIRECTORY.test(path2) || /\.snap$/i.test(path2);
5042
+ }
5043
+ __name(isTestPath, "isTestPath");
5009
5044
  var INBOX_MAX_TITLE_CHARS = 300;
5010
5045
  var FLEET_TASK_WORKSPACE_MAX_MOUNTS = 8;
5011
5046
  var FLEET_TASK_WORKSPACE_MOUNT_DIR_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9_-])?$/;
@@ -5519,6 +5554,275 @@ function truncateToUtf8Bytes(value, maxBytes) {
5519
5554
  return { value: decoded.replace(/\uFFFD+$/, "").trimEnd(), truncated: true };
5520
5555
  }
5521
5556
  __name(truncateToUtf8Bytes, "truncateToUtf8Bytes");
5557
+ var FLEET_JOB_FAILURE_KIND_MODEL_USAGE_LIMIT = "model_usage_limit";
5558
+ var FLEET_MODEL_USAGE_LIMIT_FALLBACK_MS = 60 * 60 * 1e3;
5559
+ var FLEET_MODEL_USAGE_LIMIT_MIN_MS = 60 * 1e3;
5560
+ var FLEET_MODEL_USAGE_LIMIT_MAX_MS = 8 * 24 * 60 * 60 * 1e3;
5561
+ var FLEET_MODEL_USAGE_LIMIT_RESET_SLACK_MS = 60 * 1e3;
5562
+ var FLEET_MODEL_USAGE_LIMIT_MAX_MESSAGE_LENGTH = 300;
5563
+ var LIMIT_SIGNATURES = [
5564
+ { pattern: /\bhit (?:your|the) (?:[\w-]+ )?(?:usage |rate )?limit\b/i, strict: true },
5565
+ {
5566
+ pattern: /\b(?:usage|weekly|daily|session|5[- ]hour|five[- ]hour) limit (?:has been )?(?:reached|exceeded|hit)\b/i,
5567
+ strict: true
5568
+ },
5569
+ { pattern: /\busage[_ ]limit[_ ](?:reached|exceeded)\b/i, strict: true },
5570
+ { pattern: /\brate_limit_error\b/, strict: true },
5571
+ { pattern: /\bexceeded retry limit,? last status:? 429\b/i, strict: true },
5572
+ { pattern: /\b429\b[^\n]{0,40}\btoo many requests\b/i, strict: true },
5573
+ { pattern: /\bAPI Error:? 429\b/i, strict: true },
5574
+ { pattern: /\brate[- ]limit(?:ed)?\b[^\n]{0,20}\b(?:reached|exceeded)\b/i, strict: false },
5575
+ { pattern: /\bout of (?:extra )?usage\b/i, strict: false }
5576
+ ];
5577
+ function detectFleetModelUsageLimit(text, options = {}) {
5578
+ if (typeof text !== "string" || !text.trim()) return null;
5579
+ const strict = options.strict === true;
5580
+ const match = LIMIT_SIGNATURES.find((signature) => (!strict || signature.strict) && signature.pattern.test(text));
5581
+ if (!match) return null;
5582
+ const nowMs = toEpochMs(options.now);
5583
+ const provider = typeof options.provider === "string" && options.provider.trim() ? options.provider.trim() : "claude-code";
5584
+ const resetsAtMs = parseResetInstant(text, nowMs, options.timeZone);
5585
+ const resetsAt = resetsAtMs !== null && resetsAtMs > nowMs ? resetsAtMs : null;
5586
+ return {
5587
+ provider,
5588
+ window: classifyWindow(text),
5589
+ resetsAt: resetsAt === null ? null : new Date(resetsAt).toISOString(),
5590
+ retryAfter: new Date(
5591
+ clampRetryAfterMs(
5592
+ resetsAt === null ? nowMs + FLEET_MODEL_USAGE_LIMIT_FALLBACK_MS : resetsAt + FLEET_MODEL_USAGE_LIMIT_RESET_SLACK_MS,
5593
+ nowMs
5594
+ )
5595
+ ).toISOString(),
5596
+ message: describeLimitSentence(text, match.pattern)
5597
+ };
5598
+ }
5599
+ __name(detectFleetModelUsageLimit, "detectFleetModelUsageLimit");
5600
+ function clampRetryAfterMs(retryAfterMs, nowMs) {
5601
+ const floor = nowMs + FLEET_MODEL_USAGE_LIMIT_MIN_MS;
5602
+ const ceiling = nowMs + FLEET_MODEL_USAGE_LIMIT_MAX_MS;
5603
+ if (!Number.isFinite(retryAfterMs)) return nowMs + FLEET_MODEL_USAGE_LIMIT_FALLBACK_MS;
5604
+ return Math.min(Math.max(retryAfterMs, floor), ceiling);
5605
+ }
5606
+ __name(clampRetryAfterMs, "clampRetryAfterMs");
5607
+ function describeFleetModelProvider(provider) {
5608
+ switch (provider) {
5609
+ case "claude-code":
5610
+ return "Claude Code";
5611
+ case "codex":
5612
+ return "Codex";
5613
+ default:
5614
+ return provider;
5615
+ }
5616
+ }
5617
+ __name(describeFleetModelProvider, "describeFleetModelProvider");
5618
+ function describeFleetModelUsageLimitWindow(window) {
5619
+ switch (window) {
5620
+ case "weekly":
5621
+ return "weekly limit";
5622
+ case "session":
5623
+ return "5-hour limit";
5624
+ case "daily":
5625
+ return "daily limit";
5626
+ case "rate":
5627
+ return "rate limit";
5628
+ default:
5629
+ return "usage limit";
5630
+ }
5631
+ }
5632
+ __name(describeFleetModelUsageLimitWindow, "describeFleetModelUsageLimitWindow");
5633
+ function toEpochMs(value) {
5634
+ if (value instanceof Date) return value.getTime();
5635
+ if (typeof value === "number" && Number.isFinite(value)) return value;
5636
+ return Date.now();
5637
+ }
5638
+ __name(toEpochMs, "toEpochMs");
5639
+ function classifyWindow(text) {
5640
+ if (/\bweekly\b|\bper week\b/i.test(text)) return "weekly";
5641
+ if (/\b(?:5|five)[- ]hour\b|\bsession limit\b/i.test(text)) return "session";
5642
+ if (/\bdaily\b|\bper day\b/i.test(text)) return "daily";
5643
+ if (/\b429\b|rate_limit_error|\brate[- ]limit/i.test(text)) return "rate";
5644
+ return "usage";
5645
+ }
5646
+ __name(classifyWindow, "classifyWindow");
5647
+ function trimSentence(text) {
5648
+ const clean = text.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
5649
+ const points = Array.from(clean);
5650
+ return points.length > FLEET_MODEL_USAGE_LIMIT_MAX_MESSAGE_LENGTH ? `${points.slice(0, FLEET_MODEL_USAGE_LIMIT_MAX_MESSAGE_LENGTH - 1).join("").trimEnd()}\u2026` : clean;
5651
+ }
5652
+ __name(trimSentence, "trimSentence");
5653
+ function describeLimitSentence(text, pattern) {
5654
+ const line = text.split(/\r?\n/).find((candidate) => pattern.test(candidate));
5655
+ return trimSentence(line ?? text);
5656
+ }
5657
+ __name(describeLimitSentence, "describeLimitSentence");
5658
+ var MONTHS = {
5659
+ jan: 0,
5660
+ feb: 1,
5661
+ mar: 2,
5662
+ apr: 3,
5663
+ may: 4,
5664
+ jun: 5,
5665
+ jul: 6,
5666
+ aug: 7,
5667
+ sep: 8,
5668
+ sept: 8,
5669
+ oct: 9,
5670
+ nov: 10,
5671
+ dec: 11
5672
+ };
5673
+ function parseResetInstant(text, nowMs, timeZone) {
5674
+ const epoch = /usage limit reached\|(\d{10,13})\b/i.exec(text);
5675
+ if (epoch) {
5676
+ const value = Number(epoch[1]);
5677
+ return epoch[1].length >= 13 ? value : value * 1e3;
5678
+ }
5679
+ const iso = /\b(?:resets?|reset at|try again at|available again at|until)\s+(?:at\s+)?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2}))/i.exec(
5680
+ text
5681
+ );
5682
+ if (iso) {
5683
+ const value = Date.parse(iso[1]);
5684
+ if (Number.isFinite(value)) return value;
5685
+ }
5686
+ const relative6 = /\b(?:resets?|reset|try again|available again|retry)\s+in\s+((?:\d+\s*(?:d|days?|h|hrs?|hours?|m|mins?|minutes?|s|secs?|seconds?)\b(?:[\s,]+(?:and )?)?)+)/i.exec(
5687
+ text
5688
+ );
5689
+ if (relative6) {
5690
+ const ms = parseDuration(relative6[1]);
5691
+ if (ms > 0) return nowMs + ms;
5692
+ }
5693
+ const wall = /\b(?:resets?|reset at|will reset at|try again at|available again at)\s+(?:at\s+|on\s+)?(?:(jan|feb|mar|apr|may|jun|jul|aug|sept?|oct|nov|dec)[a-z]*\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+){0,2}|UTC|GMT)\))?/i.exec(
5694
+ text
5695
+ );
5696
+ if (wall) {
5697
+ const [, monthName, dayText, hourText, minuteText, meridiem, zoneText] = wall;
5698
+ let hour = Number(hourText);
5699
+ const minute = minuteText ? Number(minuteText) : 0;
5700
+ if (!meridiem && !minuteText && !monthName) {
5701
+ return null;
5702
+ }
5703
+ if (meridiem) {
5704
+ if (hour < 1 || hour > 12) return null;
5705
+ hour = hour % 12 + (meridiem.toLowerCase() === "pm" ? 12 : 0);
5706
+ }
5707
+ if (hour > 23 || minute > 59) return null;
5708
+ const zone = resolveZone(zoneText) ?? resolveZone(timeZone) ?? "UTC";
5709
+ const today = zonedParts(nowMs, zone);
5710
+ if (monthName) {
5711
+ const month = MONTHS[monthName.toLowerCase()];
5712
+ const day = Number(dayText);
5713
+ if (month === void 0 || day < 1 || day > 31) return null;
5714
+ let candidate2 = zonedWallTimeToUtc(today.year, month, day, hour, minute, zone);
5715
+ if (candidate2 < nowMs - 24 * 60 * 60 * 1e3) {
5716
+ candidate2 = zonedWallTimeToUtc(today.year + 1, month, day, hour, minute, zone);
5717
+ }
5718
+ return candidate2;
5719
+ }
5720
+ let candidate = zonedWallTimeToUtc(today.year, today.month, today.day, hour, minute, zone);
5721
+ if (candidate <= nowMs) {
5722
+ candidate = zonedWallTimeToUtc(today.year, today.month, today.day + 1, hour, minute, zone);
5723
+ }
5724
+ return candidate;
5725
+ }
5726
+ return null;
5727
+ }
5728
+ __name(parseResetInstant, "parseResetInstant");
5729
+ function parseDuration(text) {
5730
+ let total = 0;
5731
+ let index = 0;
5732
+ const length = Math.min(text.length, 200);
5733
+ while (index < length) {
5734
+ if (!isDigit(text.charCodeAt(index))) {
5735
+ index += 1;
5736
+ continue;
5737
+ }
5738
+ let value = 0;
5739
+ while (index < length && isDigit(text.charCodeAt(index))) {
5740
+ value = Math.min(value * 10 + (text.charCodeAt(index) - 48), 1e6);
5741
+ index += 1;
5742
+ }
5743
+ while (index < length && text[index] === " ") index += 1;
5744
+ let word = "";
5745
+ while (index < length && /[a-z]/i.test(text[index])) {
5746
+ word += text[index].toLowerCase();
5747
+ index += 1;
5748
+ }
5749
+ const unitMs = DURATION_UNITS[word];
5750
+ if (unitMs !== void 0) total += value * unitMs;
5751
+ }
5752
+ return total;
5753
+ }
5754
+ __name(parseDuration, "parseDuration");
5755
+ var DURATION_UNITS = {
5756
+ d: 864e5,
5757
+ day: 864e5,
5758
+ days: 864e5,
5759
+ h: 36e5,
5760
+ hr: 36e5,
5761
+ hrs: 36e5,
5762
+ hour: 36e5,
5763
+ hours: 36e5,
5764
+ m: 6e4,
5765
+ min: 6e4,
5766
+ mins: 6e4,
5767
+ minute: 6e4,
5768
+ minutes: 6e4,
5769
+ s: 1e3,
5770
+ sec: 1e3,
5771
+ secs: 1e3,
5772
+ second: 1e3,
5773
+ seconds: 1e3
5774
+ };
5775
+ function isDigit(code) {
5776
+ return code >= 48 && code <= 57;
5777
+ }
5778
+ __name(isDigit, "isDigit");
5779
+ function resolveZone(zone) {
5780
+ if (typeof zone !== "string" || !zone.trim()) return null;
5781
+ const candidate = zone.trim();
5782
+ try {
5783
+ new Intl.DateTimeFormat("en-US", { timeZone: candidate });
5784
+ return candidate;
5785
+ } catch {
5786
+ return null;
5787
+ }
5788
+ }
5789
+ __name(resolveZone, "resolveZone");
5790
+ function zonedParts(instantMs, zone) {
5791
+ const parts = new Intl.DateTimeFormat("en-US", {
5792
+ timeZone: zone,
5793
+ hourCycle: "h23",
5794
+ year: "numeric",
5795
+ month: "2-digit",
5796
+ day: "2-digit",
5797
+ hour: "2-digit",
5798
+ minute: "2-digit",
5799
+ second: "2-digit"
5800
+ }).formatToParts(new Date(instantMs));
5801
+ const get = /* @__PURE__ */ __name((type) => Number(parts.find((part) => part.type === type)?.value ?? 0), "get");
5802
+ return {
5803
+ year: get("year"),
5804
+ month: get("month") - 1,
5805
+ day: get("day"),
5806
+ hour: get("hour") % 24,
5807
+ minute: get("minute"),
5808
+ second: get("second")
5809
+ };
5810
+ }
5811
+ __name(zonedParts, "zonedParts");
5812
+ function zoneOffsetMs(instantMs, zone) {
5813
+ const whole = Math.floor(instantMs / 1e3) * 1e3;
5814
+ const p = zonedParts(whole, zone);
5815
+ return Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - whole;
5816
+ }
5817
+ __name(zoneOffsetMs, "zoneOffsetMs");
5818
+ function zonedWallTimeToUtc(year, month, day, hour, minute, zone) {
5819
+ const asIfUtc = Date.UTC(year, month, day, hour, minute, 0);
5820
+ const firstOffset = zoneOffsetMs(asIfUtc, zone);
5821
+ const guess = asIfUtc - firstOffset;
5822
+ const secondOffset = zoneOffsetMs(guess, zone);
5823
+ return secondOffset === firstOffset ? guess : asIfUtc - secondOffset;
5824
+ }
5825
+ __name(zonedWallTimeToUtc, "zonedWallTimeToUtc");
5522
5826
  var FLEET_ENROLLABLE_NODE_KINDS = ["desktop-node", "node"];
5523
5827
  function isFleetEnrollableNodeKind(value) {
5524
5828
  return typeof value === "string" && FLEET_ENROLLABLE_NODE_KINDS.includes(value);
@@ -6124,6 +6428,8 @@ var EVER_STATS_KEY_ID_HEADER = CONSTANTS.stats_headers.key_id;
6124
6428
  var EVER_STATS_SIGNATURE_PREFIX = CONSTANTS.stats_signature_prefix;
6125
6429
  var EVER_STATS_MAX_BODY_BYTES = CONSTANTS.stats.max_bytes;
6126
6430
  var DOMAIN_EVENT_CATALOG = [
6431
+ // EW-P2-6 — `org:write` (and the app) create Organizations.
6432
+ { type: "organization.created", domain: "organization", aggregateType: "organization", phase: "P2" },
6127
6433
  { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
6128
6434
  {
6129
6435
  type: "organization.member.added",
@@ -6170,10 +6476,10 @@ var DOMAIN_EVENT_CATALOG = [
6170
6476
  { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
6171
6477
  { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
6172
6478
  { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
6173
- { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
6174
- { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
6175
- { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
6176
- { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
6479
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
6480
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
6481
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
6482
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
6177
6483
  { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
6178
6484
  { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
6179
6485
  { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
@@ -6230,6 +6536,11 @@ var DOMAIN_EVENT_TYPES = DOMAIN_EVENT_CATALOG.map((entry) => entry.type);
6230
6536
  var CATALOG_BY_TYPE = new Map(
6231
6537
  DOMAIN_EVENT_CATALOG.map((entry) => [entry.type, entry])
6232
6538
  );
6539
+ var RECIPIENT_SCOPED_EVENT_DOMAINS = {
6540
+ inbox: "inbox:read",
6541
+ email: "email:read"
6542
+ };
6543
+ var RECIPIENT_EVENT_SCOPES = Object.values(RECIPIENT_SCOPED_EVENT_DOMAINS);
6233
6544
  var INTEGRATION_SCOPES = [
6234
6545
  "org:read",
6235
6546
  "org:write",
@@ -8689,6 +9000,7 @@ __name(measureWorkspaceFreeBytes, "measureWorkspaceFreeBytes");
8689
9000
 
8690
9001
  // src/core/worker-loop.ts
8691
9002
  var DEFAULT_IDLE_POLL_MS = 5e3;
9003
+ var MODEL_THROTTLE_SYNC_GRACE_MS = 12e4;
8692
9004
  var WORKER_BACKOFF_BASE_MS = 1e3;
8693
9005
  var WORKER_BACKOFF_MAX_MS = 6e4;
8694
9006
  var MIN_KEEPALIVE_MS = 5e3;
@@ -8787,6 +9099,8 @@ var WorkerLoop = class {
8787
9099
  monotonicNow;
8788
9100
  leasePollDrainTimeoutMs;
8789
9101
  publishFenceMarginMs;
9102
+ /** Fleet usage limits: provider → the throttle in force (expiry checked on read). */
9103
+ modelThrottles = /* @__PURE__ */ new Map();
8790
9104
  running = false;
8791
9105
  stopping = false;
8792
9106
  paused = false;
@@ -8846,7 +9160,118 @@ var WorkerLoop = class {
8846
9160
  return [...this.executors.keys()];
8847
9161
  }
8848
9162
  getState() {
8849
- return { ...this.state, activeJobIds: [...this.state.activeJobIds] };
9163
+ const modelThrottles = this.activeModelThrottles();
9164
+ return {
9165
+ ...this.state,
9166
+ activeJobIds: [...this.state.activeJobIds],
9167
+ ...modelThrottles.length > 0 ? { modelThrottles } : {}
9168
+ };
9169
+ }
9170
+ /**
9171
+ * Fleet usage limits — record that `limit.provider` is out of quota on this
9172
+ * machine until `limit.retryAfter`. From the next poll the loop leases no
9173
+ * `agent-task` for that CLI (all of `agent-task` when every CLI this node
9174
+ * has is out), and refuses — without starting it — any such job the
9175
+ * platform still hands it. Other job kinds keep leasing. A later reset
9176
+ * replaces an earlier one; an earlier one never shortens a later one.
9177
+ */
9178
+ noteModelUsageLimit(limit) {
9179
+ const provider = typeof limit.provider === "string" ? limit.provider.trim() : "";
9180
+ const until = Date.parse(limit.retryAfter);
9181
+ if (!provider || !Number.isFinite(until) || until <= this.now()) return;
9182
+ const previous = this.modelThrottles.get(provider);
9183
+ if (previous && previous.until >= until) return;
9184
+ this.modelThrottles.set(provider, { until, window: limit.window, reason: limit.message, setAt: this.now() });
9185
+ if (!previous || previous.until <= this.now()) {
9186
+ this.options.logger?.warn(
9187
+ `${describeFleetModelProvider(provider)} hit its ${describeFleetModelUsageLimitWindow(limit.window)} on this node \u2014 not leasing ${provider} agent-task jobs until ${new Date(until).toISOString()} (other work still runs)`
9188
+ );
9189
+ }
9190
+ this.patch({});
9191
+ }
9192
+ /**
9193
+ * Fleet usage limits — reconcile with the platform's view of this node's
9194
+ * throttles, as carried on every heartbeat response (`node.modelThrottles`).
9195
+ *
9196
+ * The platform is authoritative both ways:
9197
+ * - a throttle it holds that this loop does not (a restarted daemon, a
9198
+ * limit another path recorded) is ADOPTED, so the node stops offering
9199
+ * itself for that CLI and says so on its next beat;
9200
+ * - a throttle this loop holds that the platform no longer does — the
9201
+ * owner pressed "clear limit now" — is LIFTED, but only once it is older
9202
+ * than `graceMs`: a heartbeat answered just before the platform recorded
9203
+ * the limit this node just reported must not undo it.
9204
+ * `null` / absent (an API older than the field) changes nothing.
9205
+ */
9206
+ syncModelThrottles(server, graceMs = MODEL_THROTTLE_SYNC_GRACE_MS) {
9207
+ if (!Array.isArray(server)) return;
9208
+ const now = this.now();
9209
+ const held = /* @__PURE__ */ new Set();
9210
+ let changed = false;
9211
+ for (const entry of server) {
9212
+ const provider = typeof entry?.provider === "string" ? entry.provider.trim() : "";
9213
+ const until = typeof entry?.until === "string" ? Date.parse(entry.until) : Number.NaN;
9214
+ if (!provider || !Number.isFinite(until) || until <= now) continue;
9215
+ held.add(provider);
9216
+ const local = this.modelThrottles.get(provider);
9217
+ if (local && local.until >= until) continue;
9218
+ const window = typeof entry.window === "string" ? entry.window : "usage";
9219
+ const reason = typeof entry.reason === "string" ? entry.reason : `${provider} usage limit reached`;
9220
+ this.modelThrottles.set(provider, { until, window, reason, setAt: local?.setAt ?? now });
9221
+ changed = true;
9222
+ if (!local) {
9223
+ this.options.logger?.info(
9224
+ `${describeFleetModelProvider(provider)} is out of quota on this node per the platform \u2014 not leasing ${provider} agent-task jobs until ${new Date(until).toISOString()}`
9225
+ );
9226
+ }
9227
+ }
9228
+ for (const [provider, local] of this.modelThrottles) {
9229
+ if (held.has(provider) || now - local.setAt < graceMs) continue;
9230
+ this.modelThrottles.delete(provider);
9231
+ changed = true;
9232
+ this.options.logger?.info(
9233
+ `${describeFleetModelProvider(provider)} usage limit was cleared on the platform \u2014 leasing ${provider} agent-task jobs again`
9234
+ );
9235
+ }
9236
+ if (changed) {
9237
+ this.patch({});
9238
+ this.pollSoon();
9239
+ }
9240
+ }
9241
+ /** The usage-limit throttles still in force; expired ones are dropped (and logged) here. */
9242
+ activeModelThrottles() {
9243
+ const now = this.now();
9244
+ const active = [];
9245
+ for (const [provider, throttle] of this.modelThrottles) {
9246
+ if (throttle.until <= now) {
9247
+ this.modelThrottles.delete(provider);
9248
+ this.options.logger?.info(
9249
+ `${describeFleetModelProvider(provider)} usage limit has reset \u2014 leasing ${provider} agent-task jobs again`
9250
+ );
9251
+ continue;
9252
+ }
9253
+ active.push({
9254
+ provider,
9255
+ window: throttle.window,
9256
+ until: new Date(throttle.until).toISOString(),
9257
+ reason: throttle.reason
9258
+ });
9259
+ }
9260
+ return active.sort((a, b) => a.provider.localeCompare(b.provider));
9261
+ }
9262
+ /** True when every model CLI this node can run an agent-task with is out of quota. */
9263
+ allAgentCliThrottled() {
9264
+ const active = this.activeModelThrottles();
9265
+ if (active.length === 0) return false;
9266
+ let providers = [];
9267
+ try {
9268
+ providers = this.options.modelProviders?.() ?? [];
9269
+ } catch {
9270
+ providers = [];
9271
+ }
9272
+ if (providers.length === 0) return true;
9273
+ const out = new Set(active.map((throttle) => throttle.provider));
9274
+ return providers.every((provider) => out.has(provider));
8850
9275
  }
8851
9276
  onChange(listener) {
8852
9277
  this.listeners.add(listener);
@@ -9124,8 +9549,10 @@ var WorkerLoop = class {
9124
9549
  };
9125
9550
  if (this.options.capabilities) request.capabilities = this.options.capabilities;
9126
9551
  if (this.options.kinds && this.options.kinds.length > 0) request.kinds = [...this.options.kinds];
9127
- if (this.options.excludeKinds && this.options.excludeKinds.length > 0) {
9128
- request.excludeKinds = [...this.options.excludeKinds];
9552
+ const excludeKinds = new Set(this.options.excludeKinds ?? []);
9553
+ if (this.allAgentCliThrottled()) excludeKinds.add("agent-task");
9554
+ if (excludeKinds.size > 0) {
9555
+ request.excludeKinds = [...excludeKinds];
9129
9556
  }
9130
9557
  if (this.options.client.leaseOutcome) {
9131
9558
  const outcome = await this.options.client.leaseOutcome(request, pollController.signal);
@@ -9362,6 +9789,14 @@ var WorkerLoop = class {
9362
9789
  );
9363
9790
  return;
9364
9791
  }
9792
+ const throttled = this.throttledResultFor(job);
9793
+ if (throttled) {
9794
+ this.options.logger?.info(
9795
+ `Fleet job ${job.id} not started: ${String(throttled.failureReason)} \u2014 handed back for the platform to park`
9796
+ );
9797
+ await this.report(job.id, { success: true, result: throttled }, generation, run);
9798
+ return;
9799
+ }
9365
9800
  const keepAlive = this.startKeepAlive(run);
9366
9801
  let successAccepted = false;
9367
9802
  try {
@@ -9370,6 +9805,7 @@ var WorkerLoop = class {
9370
9805
  );
9371
9806
  const result = await executor(job, signal, keepAlive.handle);
9372
9807
  throwIfJobAborted(signal);
9808
+ this.noteUsageLimitResult(result);
9373
9809
  if (this.settleDeferred(job.id, keepAlive.deferral())) return;
9374
9810
  const accepted = await this.report(
9375
9811
  job.id,
@@ -9404,6 +9840,57 @@ var WorkerLoop = class {
9404
9840
  keepAlive.stop();
9405
9841
  }
9406
9842
  }
9843
+ /** A result that says its CLI hit a usage limit throttles that CLI here. */
9844
+ noteUsageLimitResult(result) {
9845
+ if (!result || result.failureKind !== FLEET_JOB_FAILURE_KIND_MODEL_USAGE_LIMIT) return;
9846
+ const limit = result.modelUsageLimit;
9847
+ if (!limit || typeof limit.provider !== "string" || typeof limit.retryAfter !== "string") return;
9848
+ this.noteModelUsageLimit({
9849
+ provider: limit.provider,
9850
+ window: limit.window ?? "usage",
9851
+ retryAfter: limit.retryAfter,
9852
+ message: typeof limit.message === "string" ? limit.message : `${limit.provider} usage limit reached`
9853
+ });
9854
+ }
9855
+ /**
9856
+ * The usage-limit outcome for an `agent-task` whose CLI is out of quota on
9857
+ * this node, or null. Shaped like a real run's result (model block
9858
+ * included) so the platform — current or older — reads it the same way.
9859
+ */
9860
+ throttledResultFor(job) {
9861
+ if (job.kind !== "agent-task") return null;
9862
+ const payload = job.payload ?? {};
9863
+ const execution = payload.execution;
9864
+ const provider = typeof execution?.provider === "string" ? execution.provider : null;
9865
+ if (!provider) return null;
9866
+ const throttle = this.activeModelThrottles().find((entry) => entry.provider === provider);
9867
+ if (!throttle) return null;
9868
+ const failureReason = `${provider} is out of quota on this node until ${throttle.until} (${describeFleetModelUsageLimitWindow(throttle.window)}) \u2014 not started`;
9869
+ return {
9870
+ status: "failed",
9871
+ taskId: typeof payload.taskId === "string" ? payload.taskId : "",
9872
+ runId: typeof payload.runId === "string" ? payload.runId : null,
9873
+ workspace: null,
9874
+ steps: [],
9875
+ model: {
9876
+ provider,
9877
+ status: "failed",
9878
+ exitCode: null,
9879
+ durationMs: 0,
9880
+ summary: throttle.reason
9881
+ },
9882
+ gateStatus: "none",
9883
+ failureReason,
9884
+ failureKind: FLEET_JOB_FAILURE_KIND_MODEL_USAGE_LIMIT,
9885
+ modelUsageLimit: {
9886
+ provider,
9887
+ window: throttle.window,
9888
+ resetsAt: null,
9889
+ retryAfter: throttle.until,
9890
+ message: throttle.reason
9891
+ }
9892
+ };
9893
+ }
9407
9894
  /**
9408
9895
  * Record a job the executor handed back UNSETTLED, and report nothing.
9409
9896
  *
@@ -10994,6 +11481,32 @@ __name(isReportableBytes, "isReportableBytes");
10994
11481
  // src/core/worker-health.ts
10995
11482
  function describeWorkerHealth(state) {
10996
11483
  if (!state) return null;
11484
+ const projected = projectWorkerState(state);
11485
+ const usage = describeModelThrottles(state);
11486
+ if (!usage) return projected;
11487
+ switch (projected.workerState) {
11488
+ case "idle":
11489
+ return health("throttled", usage);
11490
+ case "throttled":
11491
+ case "working":
11492
+ return health(
11493
+ projected.workerState,
11494
+ projected.workerStateReason ? `${projected.workerStateReason}; ${usage}` : usage
11495
+ );
11496
+ default:
11497
+ return projected;
11498
+ }
11499
+ }
11500
+ __name(describeWorkerHealth, "describeWorkerHealth");
11501
+ function describeModelThrottles(state) {
11502
+ const throttles = state.modelThrottles ?? [];
11503
+ if (throttles.length === 0) return null;
11504
+ return throttles.map(
11505
+ (throttle) => `${throttle.provider}: ${describeFleetModelUsageLimitWindow(throttle.window)} until ${throttle.until}`
11506
+ ).join("; ");
11507
+ }
11508
+ __name(describeModelThrottles, "describeModelThrottles");
11509
+ function projectWorkerState(state) {
10997
11510
  switch (state.state) {
10998
11511
  // The fail-closed stop. `lastError` carries the quarantine's own
10999
11512
  // message (`markUnsafe` writes it there), which is the single most
@@ -11023,7 +11536,7 @@ function describeWorkerHealth(state) {
11023
11536
  return health("idle", null);
11024
11537
  }
11025
11538
  }
11026
- __name(describeWorkerHealth, "describeWorkerHealth");
11539
+ __name(projectWorkerState, "projectWorkerState");
11027
11540
  function health(workerState, reason) {
11028
11541
  const trimmed = typeof reason === "string" ? reason.trim() : "";
11029
11542
  if (!trimmed) {
@@ -12983,6 +13496,31 @@ async function runResolvedAgentTask(context, io, signal) {
12983
13496
  containment = modelStep.containment;
12984
13497
  throwIfAgentTaskAborted(signal);
12985
13498
  if (model.status !== "succeeded") {
13499
+ const usageLimit = recognizeModelUsageLimit(model, io);
13500
+ if (usageLimit) {
13501
+ if (workspaceResolution.descriptor && io.removeRunEnvFiles) {
13502
+ await io.removeRunEnvFiles(taskId, workspaceResolution.descriptor).catch(() => void 0);
13503
+ }
13504
+ io.logger?.warn(
13505
+ `[fleet-node] job ${job.id}: ${model.provider} hit a usage limit (${usageLimit.window}) \u2014 retry after ${usageLimit.retryAfter}; steps, checks and publish skipped`
13506
+ );
13507
+ return fitNodeOutcomeToResultBudget({
13508
+ status: "failed",
13509
+ taskId,
13510
+ runId,
13511
+ workspace: workspaceResolution.descriptor,
13512
+ steps: [],
13513
+ ...setupResults.length > 0 ? { setup: setupResults } : {},
13514
+ ...setup.length > 0 ? { setupStatus } : {},
13515
+ model,
13516
+ gateStatus: "none",
13517
+ ...mcp ? { mcp } : {},
13518
+ ...containment ? { containment } : {},
13519
+ failureReason: `${describeModelFailure(model)} \u2014 usage limit, retry after ${usageLimit.retryAfter}`,
13520
+ failureKind: FLEET_JOB_FAILURE_KIND_MODEL_USAGE_LIMIT,
13521
+ modelUsageLimit: usageLimit
13522
+ });
13523
+ }
12986
13524
  failures.push(describeModelFailure(model));
12987
13525
  }
12988
13526
  }
@@ -13688,6 +14226,22 @@ function resumeNeverOpened(model) {
13688
14226
  return RESUME_NEVER_OPENED_SIGNATURES.some((signature) => signature.test(said));
13689
14227
  }
13690
14228
  __name(resumeNeverOpened, "resumeNeverOpened");
14229
+ function recognizeModelUsageLimit(model, io = {}) {
14230
+ if (model.status === "succeeded" || model.status === "timeout") return null;
14231
+ const now = io.usageLimitClock?.now() ?? Date.now();
14232
+ const timeZone = io.usageLimitClock?.timeZone ?? localTimeZone();
14233
+ const options = { provider: model.provider, now, ...timeZone ? { timeZone } : {} };
14234
+ return detectFleetModelUsageLimit(model.summary, options) ?? detectFleetModelUsageLimit(model.outputTail, { ...options, strict: true });
14235
+ }
14236
+ __name(recognizeModelUsageLimit, "recognizeModelUsageLimit");
14237
+ function localTimeZone() {
14238
+ try {
14239
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
14240
+ } catch {
14241
+ return void 0;
14242
+ }
14243
+ }
14244
+ __name(localTimeZone, "localTimeZone");
13691
14245
  function describeModelFailure(model) {
13692
14246
  switch (model.status) {
13693
14247
  case "timeout":
@@ -15995,6 +16549,8 @@ Object.freeze(
15995
16549
  ACTION_CATEGORIES2.filter((c) => c !== "read.internal")
15996
16550
  );
15997
16551
  var DOMAIN_EVENT_CATALOG2 = [
16552
+ // EW-P2-6 — `org:write` (and the app) create Organizations.
16553
+ { type: "organization.created", domain: "organization", aggregateType: "organization", phase: "P2" },
15998
16554
  { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
15999
16555
  {
16000
16556
  type: "organization.member.added",
@@ -16041,10 +16597,10 @@ var DOMAIN_EVENT_CATALOG2 = [
16041
16597
  { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
16042
16598
  { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
16043
16599
  { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
16044
- { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
16045
- { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
16046
- { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
16047
- { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
16600
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
16601
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
16602
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
16603
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
16048
16604
  { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
16049
16605
  { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
16050
16606
  { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
@@ -16591,6 +17147,7 @@ var LocalWorkspacePlugin = class _LocalWorkspacePlugin {
16591
17147
  );
16592
17148
  const dirty = status.stdout.trim().length > 0;
16593
17149
  if (dirty) {
17150
+ const commitMessage = rewriteCommitMessage(opts, status.stdout);
16594
17151
  const identity = opts.identity;
16595
17152
  await this.gitOrThrow(
16596
17153
  [
@@ -16602,7 +17159,7 @@ var LocalWorkspacePlugin = class _LocalWorkspacePlugin {
16602
17159
  ...opts.hardenRepoHooks ? ["--no-verify"] : [],
16603
17160
  ...identity ? [`--author=${identity.authorName} <${identity.authorEmail}>`] : [],
16604
17161
  "-m",
16605
- opts.commitMessage
17162
+ commitMessage
16606
17163
  ],
16607
17164
  dir,
16608
17165
  opts.auth,
@@ -17845,6 +18402,31 @@ function isSafeCloneArgument(url) {
17845
18402
  return !parsed.hostname.startsWith("-");
17846
18403
  }
17847
18404
  __name(isSafeCloneArgument, "isSafeCloneArgument");
18405
+ function parsePorcelainPaths(stdout) {
18406
+ const paths = [];
18407
+ for (const line of stdout.split(/\r?\n/)) {
18408
+ if (line.length < 4) continue;
18409
+ let path2 = line.slice(3);
18410
+ const arrow = path2.lastIndexOf(" -> ");
18411
+ if (arrow !== -1) path2 = path2.slice(arrow + 4);
18412
+ if (path2.startsWith('"') && path2.endsWith('"') && path2.length >= 2) {
18413
+ path2 = path2.slice(1, -1).replace(/\\(.)/g, "$1");
18414
+ }
18415
+ if (path2) paths.push(path2);
18416
+ }
18417
+ return paths;
18418
+ }
18419
+ __name(parsePorcelainPaths, "parsePorcelainPaths");
18420
+ function rewriteCommitMessage(opts, porcelain) {
18421
+ if (typeof opts.commitMessageFor !== "function") return opts.commitMessage;
18422
+ try {
18423
+ const next = opts.commitMessageFor(parsePorcelainPaths(porcelain), opts.commitMessage);
18424
+ return typeof next === "string" && next.trim() && !/[\0\r]/.test(next) ? next : opts.commitMessage;
18425
+ } catch {
18426
+ return opts.commitMessage;
18427
+ }
18428
+ }
18429
+ __name(rewriteCommitMessage, "rewriteCommitMessage");
17848
18430
 
17849
18431
  // src/core/workspaces/push-credential.ts
17850
18432
  var PUSH_CREDENTIAL_REVOKE_TIMEOUT_MS = 5e3;
@@ -18763,6 +19345,11 @@ var FleetTaskWorkspaceProvisioner = class {
18763
19345
  // pool config. The credentialed push already does this; the flag
18764
19346
  // extends it to the commit.
18765
19347
  hardenRepoHooks: true,
19348
+ // Fleet commit messages: once the change is staged, a
19349
+ // docs-only change commits as `docs`, a tests-only one as
19350
+ // `test`, whatever the Task title implied. Only the type token
19351
+ // of the subject changes; the body and trailers are untouched.
19352
+ commitMessageFor: /* @__PURE__ */ __name((changedPaths, message) => retypeFleetCommitMessage(message, changedPaths), "commitMessageFor"),
18766
19353
  ...signal ? { signal } : {},
18767
19354
  ...opts.publishFence ? { publishFence: opts.publishFence } : {},
18768
19355
  ...authorization.identity ? { identity: authorization.identity } : {},
@@ -20313,6 +20900,8 @@ Object.freeze(
20313
20900
  ACTION_CATEGORIES3.filter((c) => c !== "read.internal")
20314
20901
  );
20315
20902
  var DOMAIN_EVENT_CATALOG3 = [
20903
+ // EW-P2-6 — `org:write` (and the app) create Organizations.
20904
+ { type: "organization.created", domain: "organization", aggregateType: "organization", phase: "P2" },
20316
20905
  { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
20317
20906
  {
20318
20907
  type: "organization.member.added",
@@ -20359,10 +20948,10 @@ var DOMAIN_EVENT_CATALOG3 = [
20359
20948
  { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
20360
20949
  { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
20361
20950
  { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
20362
- { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
20363
- { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
20364
- { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
20365
- { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
20951
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
20952
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
20953
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
20954
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
20366
20955
  { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
20367
20956
  { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
20368
20957
  { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
@@ -21578,6 +22167,7 @@ function createNodeRuntime(config, io, options = {}) {
21578
22167
  let attendedWake = null;
21579
22168
  const holdableLanes = [];
21580
22169
  let limitTarget = null;
22170
+ let throttleTarget = null;
21581
22171
  const lifecycle = new NodeLifecycleTracker({
21582
22172
  daemonVersion: io.version,
21583
22173
  logger: io.logger,
@@ -21601,6 +22191,7 @@ function createNodeRuntime(config, io, options = {}) {
21601
22191
  onAccepted: /* @__PURE__ */ __name((response) => {
21602
22192
  attendedWake?.(response.pendingComputerSessions);
21603
22193
  lifecycle.applyHeartbeat(response);
22194
+ throttleTarget?.(response.node?.modelThrottles);
21604
22195
  }, "onAccepted")
21605
22196
  };
21606
22197
  const runtime = { client, loop: new HeartbeatLoop(loopOptions), lifecycle, cliCompat };
@@ -21642,7 +22233,11 @@ function createNodeRuntime(config, io, options = {}) {
21642
22233
  // attended lane alone — never parked in (or holding a slot of) the
21643
22234
  // work lane. Sent only under `--attend`, so a work-only node's lease
21644
22235
  // body is exactly what it always was.
21645
- ...options.attendEnabled ? { excludeKinds: ["computer-session"] } : {}
22236
+ ...options.attendEnabled ? { excludeKinds: ["computer-session"] } : {},
22237
+ // Fleet usage limits: the CLIs an agent-task can run with here, so a
22238
+ // node whose only CLI is out of quota stops leasing agent-task work
22239
+ // until the reset (other kinds keep leasing).
22240
+ modelProviders: /* @__PURE__ */ __name(() => Object.entries(options.modelCli ?? io.environment.modelCli ?? {}).filter(([, path2]) => typeof path2 === "string" && path2.length > 0).map(([provider]) => provider), "modelProviders")
21646
22241
  });
21647
22242
  holdableLanes.push(worker);
21648
22243
  worker.onUpgradeRequired((hold) => lifecycle.applyLeaseRefusal(hold.minNodeVersion));
@@ -21654,6 +22249,9 @@ function createNodeRuntime(config, io, options = {}) {
21654
22249
  }
21655
22250
  }, "limitTarget");
21656
22251
  telemetry.limits = () => worker.resourceLimits;
22252
+ throttleTarget = /* @__PURE__ */ __name((throttles) => {
22253
+ if (Array.isArray(throttles)) worker.syncModelThrottles(throttles);
22254
+ }, "throttleTarget");
21657
22255
  telemetry.workerHealth = () => describeWorkerHealth(worker.getState());
21658
22256
  const housekeeping = new NodeHousekeepingReporter({
21659
22257
  minFreeDiskBytes: io.diskProbe ? effectiveMinFreeDiskBytes(limits) : null,
@@ -23303,10 +23901,12 @@ async function runStart(deps, options) {
23303
23901
  }
23304
23902
  const { loop, worker } = runtime;
23305
23903
  const attendedWorker = runtime.attended?.worker ?? null;
23904
+ let lastLoggedBeat = null;
23306
23905
  loop.onChange((state) => {
23307
- if (state.state === "connected") {
23308
- deps.io.logger.info(`Heartbeat accepted \u2014 node ${config.nodeId} is online`);
23309
- }
23906
+ if (state.state !== "connected") return;
23907
+ if (lastLoggedBeat && lastLoggedBeat.node === state.node && lastLoggedBeat.at === state.lastHeartbeatAt) return;
23908
+ lastLoggedBeat = { node: state.node, at: state.lastHeartbeatAt };
23909
+ deps.io.logger.info(`Heartbeat accepted \u2014 node ${config.nodeId} is online`);
23310
23910
  });
23311
23911
  deps.out(`Starting node ${config.nodeId} \u2192 ${config.apiUrl} (every ${config.heartbeatIntervalMs / 1e3}s)`);
23312
23912
  let requestShutdown = /* @__PURE__ */ __name(() => void 0, "requestShutdown");
@@ -24105,7 +24705,7 @@ function createLogger(options = {}) {
24105
24705
  __name(createLogger, "createLogger");
24106
24706
 
24107
24707
  // src/version.ts
24108
- var NODE_APP_VERSION = "0.2.4";
24708
+ var NODE_APP_VERSION = "0.2.5";
24109
24709
 
24110
24710
  // src/cli.ts
24111
24711
  function buildDeps() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ever-works-node",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Ever Works Node - headless execution node (CLI + long-running service): fleet enrollment, heartbeat and capability reporting",
5
5
  "author": {
6
6
  "name": "Ever Co. LTD",