@alfe.ai/gateway 0.9.13 → 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 +122 -10
- package/package.json +3 -3
package/dist/health.js
CHANGED
|
@@ -4533,6 +4533,7 @@ _enum(MOONSHOT_MODELS);
|
|
|
4533
4533
|
_enum(QWEN_MODELS);
|
|
4534
4534
|
string().trim().min(1);
|
|
4535
4535
|
AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
|
|
4536
|
+
({ Qwen3_8B: "alfe-qwen3-8b" }).Qwen3_8B;
|
|
4536
4537
|
AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
|
|
4537
4538
|
enumValues({
|
|
4538
4539
|
Standard: "standard",
|
|
@@ -5397,6 +5398,24 @@ function deriveGatewayWsUrl(apiEndpoint) {
|
|
|
5397
5398
|
return "wss://gateway.alfe.ai/ws";
|
|
5398
5399
|
}
|
|
5399
5400
|
}
|
|
5401
|
+
function validatePrivateManagedWsUrl(value, service) {
|
|
5402
|
+
let url;
|
|
5403
|
+
try {
|
|
5404
|
+
url = new URL(value);
|
|
5405
|
+
} catch {
|
|
5406
|
+
throw new Error(`${service} WebSocket URL must be a valid regional service URL`);
|
|
5407
|
+
}
|
|
5408
|
+
if (url.protocol !== "ws:" || !url.hostname.endsWith(".local") && !url.hostname.endsWith(".sst") || url.pathname !== "/ws" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") throw new Error(`${service} WebSocket URL must be a canonical VPC-local regional service URL`);
|
|
5409
|
+
return url.toString();
|
|
5410
|
+
}
|
|
5411
|
+
function _resolveManagedGatewayWsUrl(apiEndpoint) {
|
|
5412
|
+
const override = process.env.ALFE_GATEWAY_WS_URL?.trim();
|
|
5413
|
+
if (process.env.ALFE_PRIVATE_DATA_PLANE === "true") {
|
|
5414
|
+
if (!override) throw new Error("ALFE_GATEWAY_WS_URL is required in AWS-private managed mode");
|
|
5415
|
+
return validatePrivateManagedWsUrl(override, "Gateway");
|
|
5416
|
+
}
|
|
5417
|
+
return override && override.length > 0 ? override : deriveGatewayWsUrl(apiEndpoint);
|
|
5418
|
+
}
|
|
5400
5419
|
/**
|
|
5401
5420
|
* Parse [runtimes.*] sections from config.toml.
|
|
5402
5421
|
* Returns a map of runtime name → RuntimeConfig.
|
|
@@ -5471,7 +5490,7 @@ async function loadManagedConfig() {
|
|
|
5471
5490
|
const apiEndpoint = process.env.ALFE_API_ENDPOINT ?? getEndpointFromToken(apiKey);
|
|
5472
5491
|
logger$1.debug({ apiEndpoint }, "Environment loaded, resolving identity...");
|
|
5473
5492
|
const identity = await resolveAgentIdentity(apiKey, apiEndpoint);
|
|
5474
|
-
const gatewayWsUrl =
|
|
5493
|
+
const gatewayWsUrl = _resolveManagedGatewayWsUrl(apiEndpoint);
|
|
5475
5494
|
logger$1.debug({
|
|
5476
5495
|
agentId: identity.agentId,
|
|
5477
5496
|
orgId: identity.orgId,
|
|
@@ -6950,6 +6969,15 @@ const SYSTEMD_SERVICE = "alfe-gateway";
|
|
|
6950
6969
|
const MANAGED_ENV_VARS = [
|
|
6951
6970
|
"ALFE_MANAGED",
|
|
6952
6971
|
"ALFE_API_KEY",
|
|
6972
|
+
"ALFE_API_ENDPOINT",
|
|
6973
|
+
"ALFE_API_URL",
|
|
6974
|
+
"ALFE_AI_PROXY_URL",
|
|
6975
|
+
"ALFE_CHAT_WS_URL",
|
|
6976
|
+
"ALFE_GATEWAY_WS_URL",
|
|
6977
|
+
"ALFE_SYNC_RELAY_WS_URL",
|
|
6978
|
+
"ALFE_PRIVATE_DATA_PLANE",
|
|
6979
|
+
"ALFE_PRIVATE_DATA_PLANE_REGION",
|
|
6980
|
+
"ALFE_PRIVATE_DATA_PLANE_RELEASE",
|
|
6953
6981
|
"LOG_LEVEL",
|
|
6954
6982
|
"ALFE_CLI_VERSION"
|
|
6955
6983
|
];
|
|
@@ -7992,12 +8020,18 @@ async function startAiProxy(apiKey) {
|
|
|
7992
8020
|
try {
|
|
7993
8021
|
const { createProxyServer, DEFAULT_AI_PROXY_PORT } = await import("@alfe.ai/ai-proxy-local");
|
|
7994
8022
|
const { getAiServiceUrlFromToken } = await import("@alfe.ai/config");
|
|
7995
|
-
const
|
|
8023
|
+
const privateDataPlane = process.env.ALFE_PRIVATE_DATA_PLANE === "true";
|
|
8024
|
+
const proxyUrlOverride = process.env.ALFE_AI_PROXY_URL?.trim();
|
|
8025
|
+
const configuredProxyUrl = proxyUrlOverride && proxyUrlOverride.length > 0 ? proxyUrlOverride : void 0;
|
|
8026
|
+
if (privateDataPlane && configuredProxyUrl === void 0) throw new Error("AWS-private AI proxy target is not configured");
|
|
8027
|
+
const proxyUrl = configuredProxyUrl ?? getAiServiceUrlFromToken(apiKey);
|
|
7996
8028
|
const port = DEFAULT_AI_PROXY_PORT ?? 18193;
|
|
7997
8029
|
handle.server = createProxyServer({
|
|
7998
8030
|
port,
|
|
7999
8031
|
apiKey,
|
|
8000
|
-
proxyUrl
|
|
8032
|
+
proxyUrl,
|
|
8033
|
+
allowPrivateAwsTarget: privateDataPlane,
|
|
8034
|
+
requirePrivateAwsTarget: privateDataPlane
|
|
8001
8035
|
});
|
|
8002
8036
|
const server = handle.server;
|
|
8003
8037
|
await new Promise((resolve, reject) => {
|
|
@@ -23450,6 +23484,51 @@ function createLogger(component, additionalData) {
|
|
|
23450
23484
|
component
|
|
23451
23485
|
});
|
|
23452
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
|
+
});
|
|
23453
23532
|
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
23454
23533
|
function stripAnsi(line) {
|
|
23455
23534
|
return line.replace(ANSI_RE, "");
|
|
@@ -23561,8 +23640,32 @@ var ErrorLineDetector = class {
|
|
|
23561
23640
|
}
|
|
23562
23641
|
};
|
|
23563
23642
|
/**
|
|
23564
|
-
*
|
|
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:
|
|
23565
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.
|
|
23566
23669
|
*/
|
|
23567
23670
|
var CaptureThrottle = class {
|
|
23568
23671
|
windowStart = 0;
|
|
@@ -23572,16 +23675,24 @@ var CaptureThrottle = class {
|
|
|
23572
23675
|
lastCrashCaptureAt = 0;
|
|
23573
23676
|
hasCapturedCrash = false;
|
|
23574
23677
|
crashesSuppressed = 0;
|
|
23575
|
-
|
|
23678
|
+
errorCapturesMax;
|
|
23679
|
+
errorCapturesWindowMs;
|
|
23680
|
+
dedupeWindowMs;
|
|
23681
|
+
dedupeMapMaxKeys;
|
|
23682
|
+
constructor(stableUptimeMs, options = {}) {
|
|
23576
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;
|
|
23577
23688
|
}
|
|
23578
23689
|
allowErrorCapture(fingerprintKey, now = Date.now()) {
|
|
23579
|
-
if (now - this.windowStart >=
|
|
23690
|
+
if (now - this.windowStart >= this.errorCapturesWindowMs) {
|
|
23580
23691
|
this.windowStart = now;
|
|
23581
23692
|
this.windowCount = 0;
|
|
23582
23693
|
}
|
|
23583
23694
|
const lastSent = this.lastSentByKey.get(fingerprintKey);
|
|
23584
|
-
if (lastSent !== void 0 && now - lastSent <
|
|
23695
|
+
if (lastSent !== void 0 && now - lastSent < this.dedupeWindowMs || this.windowCount >= this.errorCapturesMax) {
|
|
23585
23696
|
this.errorsSuppressed++;
|
|
23586
23697
|
return {
|
|
23587
23698
|
allow: false,
|
|
@@ -23590,7 +23701,7 @@ var CaptureThrottle = class {
|
|
|
23590
23701
|
}
|
|
23591
23702
|
this.windowCount++;
|
|
23592
23703
|
this.lastSentByKey.set(fingerprintKey, now);
|
|
23593
|
-
if (this.lastSentByKey.size >
|
|
23704
|
+
if (this.lastSentByKey.size > this.dedupeMapMaxKeys) {
|
|
23594
23705
|
const oldest = this.lastSentByKey.keys().next().value;
|
|
23595
23706
|
if (oldest !== void 0) this.lastSentByKey.delete(oldest);
|
|
23596
23707
|
}
|
|
@@ -23653,6 +23764,7 @@ var RuntimeProcess = class {
|
|
|
23653
23764
|
ringBuffer = new OutputRingBuffer();
|
|
23654
23765
|
detector = new ErrorLineDetector();
|
|
23655
23766
|
throttle = new CaptureThrottle(STABLE_UPTIME_MS);
|
|
23767
|
+
toolResultErrorThrottle = new CaptureThrottle(STABLE_UPTIME_MS, STRICT_ERROR_CAPTURE_PROFILE);
|
|
23656
23768
|
detectorFlushTimer = null;
|
|
23657
23769
|
turnActivityProbe = null;
|
|
23658
23770
|
constructor(options) {
|
|
@@ -23837,7 +23949,7 @@ var RuntimeProcess = class {
|
|
|
23837
23949
|
/** Report a completed error block to Sentry, throttle permitting. */
|
|
23838
23950
|
emitErrorBlock(block) {
|
|
23839
23951
|
if (!block) return;
|
|
23840
|
-
const { allow, suppressedCount } = this.throttle.allowErrorCapture(block.fingerprintKey);
|
|
23952
|
+
const { allow, suppressedCount } = (isPluginToolResultErrorLine(block.lines[0]) ? this.toolResultErrorThrottle : this.throttle).allowErrorCapture(block.fingerprintKey);
|
|
23841
23953
|
if (!allow) {
|
|
23842
23954
|
log$3.debug({
|
|
23843
23955
|
runtime: this.options.runtime,
|
|
@@ -24563,7 +24675,7 @@ const log = createLogger("McpErrorCapture");
|
|
|
24563
24675
|
/** Quiet-period flush for a pending multi-line stderr block. */
|
|
24564
24676
|
const MCP_STDERR_FLUSH_DEBOUNCE_MS = 1500;
|
|
24565
24677
|
function createMcpErrorHooks() {
|
|
24566
|
-
const resultErrorThrottle = new CaptureThrottle(6e4);
|
|
24678
|
+
const resultErrorThrottle = new CaptureThrottle(6e4, STRICT_ERROR_CAPTURE_PROFILE);
|
|
24567
24679
|
const errorThrottle = new CaptureThrottle(6e4);
|
|
24568
24680
|
const crashThrottle = new CaptureThrottle(6e4);
|
|
24569
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/
|
|
27
|
-
"@alfe.ai/ai-proxy-local": "^0.0.16",
|
|
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"
|