@alfe.ai/gateway 0.9.14 → 0.9.16

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/dist/health.js +85 -7
  2. package/package.json +2 -2
package/dist/health.js CHANGED
@@ -23484,6 +23484,51 @@ function createLogger(component, additionalData) {
23484
23484
  component
23485
23485
  });
23486
23486
  }
23487
+ /**
23488
+ * Strict profile for the HIGH-VOLUME, LOW-SIGNAL families: MCP
23489
+ * `tool-result-error` and the plugin `[ERROR] alfe-tool … result-error:` line.
23490
+ * These are the NORMAL model-facing error path — a model omitting a required
23491
+ * argument produces one every turn — so a single agent can hold an issue open
23492
+ * indefinitely at the default budget.
23493
+ *
23494
+ * We keep sending them (continued visibility is the point); we just send far
23495
+ * fewer. Numbers and why:
23496
+ *
23497
+ * - `dedupeWindowMs: 6h` — a repeating fingerprint reports at most 4×/day per
23498
+ * agent instead of the default 144×/day. This is the lever that actually
23499
+ * collapses the observed burn, because the noisy issues repeat one message
23500
+ * shape over and over.
23501
+ * - `errorCapturesMax: 2` per `errorCapturesWindowMs: 1h` — the family ceiling
23502
+ * drops from 720 events/day/agent (5 per 10 min) to 48. An hourly window,
23503
+ * rather than folding the budget into the 6h dedupe, keeps a NEWLY-failing
23504
+ * tool visible within the hour instead of behind a 6h blackout.
23505
+ * - `dedupeMapMaxKeys: 200` — the dedupe map must now retain keys 36× longer,
23506
+ * so 50 slots would evict still-live entries on a wide tool surface and let
23507
+ * them re-fire. 200 covers every tool an agent realistically exposes.
23508
+ *
23509
+ * These are PER-DAEMON-PROCESS (i.e. per agent VM) budgets; fleet-wide volume
23510
+ * scales with agent count and is unbounded from here.
23511
+ *
23512
+ * Suppression is not silent, but it is COARSE: for the error family,
23513
+ * `errorsSuppressed` is one counter per throttle INSTANCE, incremented across
23514
+ * every fingerprint and reset on each allowed capture (see
23515
+ * {@link CaptureThrottle.allowErrorCapture}). The tag therefore reads "error
23516
+ * events this instance suppressed since its last allowed error capture",
23517
+ * landing on whichever issue happened to be the one let through — it is NOT
23518
+ * that issue's own occurrence count. Use it as a family noise-level signal,
23519
+ * not as per-issue volume.
23520
+ *
23521
+ * The crash family is accounted SEPARATELY via `crashesSuppressed`
23522
+ * ({@link CaptureThrottle.allowCrashCapture}). A suppressed error never
23523
+ * contributes to the next allowed crash capture, or vice versa, even on a
23524
+ * shared instance.
23525
+ */
23526
+ const STRICT_ERROR_CAPTURE_PROFILE = Object.freeze({
23527
+ errorCapturesMax: 2,
23528
+ errorCapturesWindowMs: 60 * 6e4,
23529
+ dedupeWindowMs: 360 * 6e4,
23530
+ dedupeMapMaxKeys: 200
23531
+ });
23487
23532
  const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
23488
23533
  function stripAnsi(line) {
23489
23534
  return line.replace(ANSI_RE, "");
@@ -23595,8 +23640,32 @@ var ErrorLineDetector = class {
23595
23640
  }
23596
23641
  };
23597
23642
  /**
23598
- * Bounds Sentry quota per RuntimeProcess. Worst case per agent:
23643
+ * Exact shape emitted by `installToolErrorCapture` in
23644
+ * `@alfe.ai/agent-api-client` for a tool that RETURNED an error result:
23645
+ *
23646
+ * [ERROR] alfe-tool plugin=<plugin> tool=<name> result-error: <msg>
23647
+ *
23648
+ * `plugin` / `tool` are `safeToken`-sanitised there (`[A-Za-z0-9_.@/-]`), so
23649
+ * they never contain whitespace. Deliberately anchored and `result-error`-only:
23650
+ * the sibling `thrown:` kind is a genuine unhandled exception and keeps the
23651
+ * default budget, as does any other `[ERROR]` line or runtime stack trace.
23652
+ */
23653
+ const ALFE_TOOL_RESULT_ERROR_LINE = /^\[ERROR\] alfe-tool plugin=[A-Za-z0-9_.@/-]+ tool=[A-Za-z0-9_.@/-]+ result-error:/;
23654
+ /**
23655
+ * True for the plugin tool-`result-error` line above — the runtime-side twin of
23656
+ * MCP's `tool-result-error`, i.e. the NORMAL model-facing failure path rather
23657
+ * than a runtime fault. Callers route these to a strict
23658
+ * {@link STRICT_ERROR_CAPTURE_PROFILE} throttle so genuine crashes and stderr
23659
+ * keep the default budget.
23660
+ */
23661
+ function isPluginToolResultErrorLine(line) {
23662
+ return ALFE_TOOL_RESULT_ERROR_LINE.test(stripAnsi(line));
23663
+ }
23664
+ /**
23665
+ * Bounds Sentry quota per instance. Worst case at the default limits:
23599
23666
  * ≤ `ERROR_CAPTURES_MAX` output events + ≤ 1 crash event per 10-minute window.
23667
+ * Pass {@link CaptureThrottleOptions} (e.g. {@link STRICT_ERROR_CAPTURE_PROFILE})
23668
+ * to give a noisier family its own, tighter budget.
23600
23669
  */
23601
23670
  var CaptureThrottle = class {
23602
23671
  windowStart = 0;
@@ -23606,16 +23675,24 @@ var CaptureThrottle = class {
23606
23675
  lastCrashCaptureAt = 0;
23607
23676
  hasCapturedCrash = false;
23608
23677
  crashesSuppressed = 0;
23609
- constructor(stableUptimeMs) {
23678
+ errorCapturesMax;
23679
+ errorCapturesWindowMs;
23680
+ dedupeWindowMs;
23681
+ dedupeMapMaxKeys;
23682
+ constructor(stableUptimeMs, options = {}) {
23610
23683
  this.stableUptimeMs = stableUptimeMs;
23684
+ this.errorCapturesMax = options.errorCapturesMax ?? 5;
23685
+ this.errorCapturesWindowMs = options.errorCapturesWindowMs ?? 6e5;
23686
+ this.dedupeWindowMs = options.dedupeWindowMs ?? 6e5;
23687
+ this.dedupeMapMaxKeys = options.dedupeMapMaxKeys ?? 50;
23611
23688
  }
23612
23689
  allowErrorCapture(fingerprintKey, now = Date.now()) {
23613
- if (now - this.windowStart >= 6e5) {
23690
+ if (now - this.windowStart >= this.errorCapturesWindowMs) {
23614
23691
  this.windowStart = now;
23615
23692
  this.windowCount = 0;
23616
23693
  }
23617
23694
  const lastSent = this.lastSentByKey.get(fingerprintKey);
23618
- if (lastSent !== void 0 && now - lastSent < 6e5 || this.windowCount >= 5) {
23695
+ if (lastSent !== void 0 && now - lastSent < this.dedupeWindowMs || this.windowCount >= this.errorCapturesMax) {
23619
23696
  this.errorsSuppressed++;
23620
23697
  return {
23621
23698
  allow: false,
@@ -23624,7 +23701,7 @@ var CaptureThrottle = class {
23624
23701
  }
23625
23702
  this.windowCount++;
23626
23703
  this.lastSentByKey.set(fingerprintKey, now);
23627
- if (this.lastSentByKey.size > 50) {
23704
+ if (this.lastSentByKey.size > this.dedupeMapMaxKeys) {
23628
23705
  const oldest = this.lastSentByKey.keys().next().value;
23629
23706
  if (oldest !== void 0) this.lastSentByKey.delete(oldest);
23630
23707
  }
@@ -23687,6 +23764,7 @@ var RuntimeProcess = class {
23687
23764
  ringBuffer = new OutputRingBuffer();
23688
23765
  detector = new ErrorLineDetector();
23689
23766
  throttle = new CaptureThrottle(STABLE_UPTIME_MS);
23767
+ toolResultErrorThrottle = new CaptureThrottle(STABLE_UPTIME_MS, STRICT_ERROR_CAPTURE_PROFILE);
23690
23768
  detectorFlushTimer = null;
23691
23769
  turnActivityProbe = null;
23692
23770
  constructor(options) {
@@ -23871,7 +23949,7 @@ var RuntimeProcess = class {
23871
23949
  /** Report a completed error block to Sentry, throttle permitting. */
23872
23950
  emitErrorBlock(block) {
23873
23951
  if (!block) return;
23874
- const { allow, suppressedCount } = this.throttle.allowErrorCapture(block.fingerprintKey);
23952
+ const { allow, suppressedCount } = (isPluginToolResultErrorLine(block.lines[0]) ? this.toolResultErrorThrottle : this.throttle).allowErrorCapture(block.fingerprintKey);
23875
23953
  if (!allow) {
23876
23954
  log$3.debug({
23877
23955
  runtime: this.options.runtime,
@@ -24597,7 +24675,7 @@ const log = createLogger("McpErrorCapture");
24597
24675
  /** Quiet-period flush for a pending multi-line stderr block. */
24598
24676
  const MCP_STDERR_FLUSH_DEBOUNCE_MS = 1500;
24599
24677
  function createMcpErrorHooks() {
24600
- const resultErrorThrottle = new CaptureThrottle(6e4);
24678
+ const resultErrorThrottle = new CaptureThrottle(6e4, STRICT_ERROR_CAPTURE_PROFILE);
24601
24679
  const errorThrottle = new CaptureThrottle(6e4);
24602
24680
  const crashThrottle = new CaptureThrottle(6e4);
24603
24681
  const throttleFor = (kind) => kind === "tool-result-error" ? resultErrorThrottle : kind === "server-crash" ? crashThrottle : errorThrottle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "pino-roll": "^1.2.0",
24
24
  "smol-toml": ">=1.6.1",
25
25
  "ws": "^8.18.0",
26
- "@alfe.ai/agent-api-client": "^0.17.1",
26
+ "@alfe.ai/agent-api-client": "^0.18.0",
27
27
  "@alfe.ai/ai-proxy-local": "^0.0.17",
28
28
  "@alfe.ai/config": "^0.4.1",
29
29
  "@alfe.ai/integration-manifest": "^0.4.0",