@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.
Files changed (2) hide show
  1. package/dist/health.js +122 -10
  2. 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 = deriveGatewayWsUrl(apiEndpoint);
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 proxyUrl = getAiServiceUrlFromToken(apiKey);
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
- * 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:
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
- constructor(stableUptimeMs) {
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 >= 6e5) {
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 < 6e5 || this.windowCount >= 5) {
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 > 50) {
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.13",
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.0",
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"