@alfe.ai/gateway 0.9.14 → 0.9.15
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/health.js +85 -7
- 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
|
-
*
|
|
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
|
-
|
|
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 >=
|
|
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 <
|
|
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 >
|
|
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.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"description": "Alfe local gateway daemon — persistent control plane for agent integrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,9 +23,9 @@
|
|
|
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",
|
|
27
26
|
"@alfe.ai/ai-proxy-local": "^0.0.17",
|
|
28
27
|
"@alfe.ai/config": "^0.4.1",
|
|
28
|
+
"@alfe.ai/agent-api-client": "^0.17.1",
|
|
29
29
|
"@alfe.ai/integration-manifest": "^0.4.0",
|
|
30
30
|
"@alfe.ai/integrations": "^0.6.4",
|
|
31
31
|
"@alfe.ai/mcp-bundler": "^0.4.1"
|