@alfe.ai/gateway 0.4.0 → 0.5.0

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as SOCKET_PATH, a as installService, c as uninstallService, i as checkExistingDaemon, n as queryDaemonHealth, r as startDaemon, s as stopExistingDaemon, t as formatHealthReport } from "../health.js";
2
+ import { a as installService, c as uninstallService, i as checkExistingDaemon, n as queryDaemonHealth, r as startDaemon, s as stopExistingDaemon, t as formatHealthReport, w as SOCKET_PATH } from "../health.js";
3
3
  import { t as LOG_FILE } from "../logger.js";
4
4
  import { spawn } from "node:child_process";
5
5
  //#region bin/gateway.ts
package/dist/health.js CHANGED
@@ -5266,7 +5266,7 @@ function isIPCResponse(msg) {
5266
5266
  const PROTOCOL_VERSION = 1;
5267
5267
  //#endregion
5268
5268
  //#region src/runtime-gate.ts
5269
- const log$3 = logger$1.child({ component: "RuntimeGate" });
5269
+ const log$4 = logger$1.child({ component: "RuntimeGate" });
5270
5270
  var RuntimeGate = class {
5271
5271
  depth = 0;
5272
5272
  wasRunning = false;
@@ -5284,7 +5284,7 @@ var RuntimeGate = class {
5284
5284
  const rp = this.getRuntime();
5285
5285
  this.wasRunning = rp?.isRunning ?? false;
5286
5286
  if (rp && this.wasRunning) {
5287
- log$3.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5287
+ log$4.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5288
5288
  await rp.stop();
5289
5289
  }
5290
5290
  }
@@ -5299,7 +5299,7 @@ var RuntimeGate = class {
5299
5299
  if (this.depth < 0) this.depth = 0;
5300
5300
  const rp = this.getRuntime();
5301
5301
  if (rp && this.wasRunning) {
5302
- log$3.info("Resuming runtime after mutating reconcile");
5302
+ log$4.info("Resuming runtime after mutating reconcile");
5303
5303
  rp.resume();
5304
5304
  }
5305
5305
  this.wasRunning = false;
@@ -5314,12 +5314,12 @@ var RuntimeGate = class {
5314
5314
  */
5315
5315
  requestRestart() {
5316
5316
  if (this.depth > 0) {
5317
- log$3.info("Runtime restart requested while suspended — deferring to pending resume");
5317
+ log$4.info("Runtime restart requested while suspended — deferring to pending resume");
5318
5318
  return;
5319
5319
  }
5320
5320
  const rp = this.getRuntime();
5321
5321
  if (rp) rp.restart().catch((err) => {
5322
- log$3.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5322
+ log$4.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5323
5323
  });
5324
5324
  }
5325
5325
  };
@@ -5374,6 +5374,13 @@ const CLI_SENTRY_DSN = "https://82dde4631336561f2fcc89d7531623de@o45110082394521
5374
5374
  * Mirrors `SENTRY_DSNS.agentRuntime` in `config/config.ts`.
5375
5375
  */
5376
5376
  const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
5377
+ /**
5378
+ * MCP tool/server failures on agent VMs — reported by the daemon-hosted MCP
5379
+ * bundler (tool errors, child crashes, stderr output). This is the SAME
5380
+ * project as the cloud MCP Fly service (`SENTRY_DSNS.mcp`); agent-side events
5381
+ * are distinguished by `source: agent-daemon` + `agentId` tags.
5382
+ */
5383
+ const AGENT_MCP_SENTRY_DSN = "https://638309eac96146a945f46b578f10c806@o4511008239452160.ingest.us.sentry.io/4511146364174336";
5377
5384
  /** Surface → Sentry project: `cli` and `agent-daemon` respectively. */
5378
5385
  const SURFACE_DSNS = {
5379
5386
  cli: CLI_SENTRY_DSN,
@@ -5382,13 +5389,16 @@ const SURFACE_DSNS = {
5382
5389
  /** Held after a successful init so the capture helpers can run synchronously. */
5383
5390
  let sentry = null;
5384
5391
  /**
5385
- * Second client for the `agent-runtime` project (daemon surface only). The
5386
- * default client keeps the daemon's own DSN; runtime child-process events are
5387
- * routed here via an explicitly-bound Scope — the documented multi-client
5388
- * pattern. `null` until `initAgentSentry({ surface: "daemon" })` succeeds.
5392
+ * Additional bound clients (daemon surface only). The default client keeps the
5393
+ * daemon's own DSN; runtime child-process events and MCP failures are routed
5394
+ * to their own projects via explicitly-bound Scopes — the documented
5395
+ * multi-client pattern. `null` until `initAgentSentry({ surface: "daemon" })`
5396
+ * succeeds.
5389
5397
  */
5390
5398
  let runtimeClient = null;
5391
5399
  let runtimeScope = null;
5400
+ let mcpClient = null;
5401
+ let mcpScope = null;
5392
5402
  /**
5393
5403
  * Map the configured API URL to a coarse environment tag. Best-effort — returns
5394
5404
  * `"unknown"` when no config is present yet (e.g. `alfe login` pre-setup).
@@ -5470,32 +5480,46 @@ async function initAgentSentry(options) {
5470
5480
  const dsn = SURFACE_DSNS[options.surface].trim();
5471
5481
  if (!dsn) return;
5472
5482
  const mod = await import("@sentry/node");
5473
- if (options.surface === "daemon" && !runtimeScope) try {
5474
- const dsn = AGENT_RUNTIME_SENTRY_DSN.trim();
5475
- {
5476
- const client = new mod.NodeClient({
5477
- dsn,
5478
- environment: deriveEnvironment(),
5479
- ...options.release ? { release: options.release } : {},
5480
- sampleRate: 1,
5481
- tracesSampleRate: 0,
5482
- sendDefaultPii: false,
5483
- transport: mod.makeNodeTransport,
5484
- stackParser: mod.defaultStackParser,
5485
- integrations: [],
5486
- beforeSend: (event) => scrubEvent(event)
5487
- });
5488
- client.init();
5489
- const scope = new mod.Scope();
5490
- scope.setClient(client);
5491
- const runtime = deriveRuntime();
5492
- if (runtime) scope.setTag("runtime", runtime);
5493
- runtimeClient = client;
5494
- runtimeScope = scope;
5483
+ if (options.surface === "daemon") {
5484
+ const buildBoundScope = (dsn, extraTags) => {
5485
+ try {
5486
+ if (!dsn.trim()) return null;
5487
+ const client = new mod.NodeClient({
5488
+ dsn: dsn.trim(),
5489
+ environment: deriveEnvironment(),
5490
+ ...options.release ? { release: options.release } : {},
5491
+ sampleRate: 1,
5492
+ tracesSampleRate: 0,
5493
+ sendDefaultPii: false,
5494
+ transport: mod.makeNodeTransport,
5495
+ stackParser: mod.defaultStackParser,
5496
+ integrations: [],
5497
+ beforeSend: (event) => scrubEvent(event)
5498
+ });
5499
+ client.init();
5500
+ const scope = new mod.Scope();
5501
+ scope.setClient(client);
5502
+ const runtime = deriveRuntime();
5503
+ if (runtime) scope.setTag("runtime", runtime);
5504
+ for (const [k, v] of Object.entries(extraTags)) scope.setTag(k, v);
5505
+ return {
5506
+ client,
5507
+ scope
5508
+ };
5509
+ } catch {
5510
+ return null;
5511
+ }
5512
+ };
5513
+ if (!runtimeScope) {
5514
+ const bound = buildBoundScope(AGENT_RUNTIME_SENTRY_DSN, {});
5515
+ runtimeClient = bound?.client ?? null;
5516
+ runtimeScope = bound?.scope ?? null;
5517
+ }
5518
+ if (!mcpScope) {
5519
+ const bound = buildBoundScope(AGENT_MCP_SENTRY_DSN, { source: "agent-daemon" });
5520
+ mcpClient = bound?.client ?? null;
5521
+ mcpScope = bound?.scope ?? null;
5495
5522
  }
5496
- } catch {
5497
- runtimeClient = null;
5498
- runtimeScope = null;
5499
5523
  }
5500
5524
  if (mod.getClient()) {
5501
5525
  sentry = mod;
@@ -5527,11 +5551,14 @@ async function initAgentSentry(options) {
5527
5551
  function setAgentContext(context) {
5528
5552
  if (!sentry) return;
5529
5553
  try {
5530
- if (context.agentId) sentry.setTag("agentId", context.agentId);
5531
- if (context.runtime) sentry.setTag("runtime", context.runtime);
5532
- if (runtimeScope) {
5533
- if (context.agentId) runtimeScope.setTag("agentId", context.agentId);
5534
- if (context.runtime) runtimeScope.setTag("runtime", context.runtime);
5554
+ for (const scope of [
5555
+ sentry,
5556
+ runtimeScope,
5557
+ mcpScope
5558
+ ]) {
5559
+ if (!scope) continue;
5560
+ if (context.agentId) scope.setTag("agentId", context.agentId);
5561
+ if (context.runtime) scope.setTag("runtime", context.runtime);
5535
5562
  }
5536
5563
  } catch {}
5537
5564
  }
@@ -5641,10 +5668,40 @@ function captureRuntimeErrorOutput(opts) {
5641
5668
  scope.captureMessage(opts.lines[0].slice(0, 300), "error");
5642
5669
  } catch {}
5643
5670
  }
5671
+ /**
5672
+ * Capture an MCP failure (tool error, server crash, or error-looking stderr
5673
+ * output) into the `mcp` project via the daemon's bound MCP client. Throttling
5674
+ * is the caller's job (`mcp-error-capture.ts`). No-op when the MCP client is
5675
+ * not initialised. Never throws.
5676
+ */
5677
+ function captureMcpFailure(opts) {
5678
+ if (!mcpScope) return;
5679
+ try {
5680
+ const scope = mcpScope.clone();
5681
+ for (const line of opts.lines ?? []) scope.addBreadcrumb({
5682
+ category: `mcp.${opts.server}`,
5683
+ level: "error",
5684
+ message: line
5685
+ });
5686
+ scope.setFingerprint([
5687
+ "agent-mcp",
5688
+ opts.server,
5689
+ opts.kind,
5690
+ ...opts.tool ? [opts.tool] : []
5691
+ ]);
5692
+ scope.setTags({
5693
+ server: opts.server,
5694
+ kind: opts.kind,
5695
+ ...opts.tool ? { tool: opts.tool } : {},
5696
+ suppressedCount: String(opts.suppressedCount ?? 0)
5697
+ });
5698
+ scope.captureMessage(`MCP ${opts.server}${opts.tool ? `.${opts.tool}` : ""} ${opts.kind}: ${opts.message.slice(0, 300)}`, "error");
5699
+ } catch {}
5700
+ }
5644
5701
  /** Flush queued events (small timeout) so short-lived commands deliver them. */
5645
5702
  async function flushSentry(timeoutMs = 2e3) {
5646
- try {
5647
- if (runtimeClient) await runtimeClient.flush(timeoutMs);
5703
+ for (const client of [runtimeClient, mcpClient]) try {
5704
+ if (client) await client.flush(timeoutMs);
5648
5705
  } catch {}
5649
5706
  if (!sentry) return;
5650
5707
  try {
@@ -5664,7 +5721,7 @@ function captureFatal(cause) {
5664
5721
  }
5665
5722
  //#endregion
5666
5723
  //#region src/reconciliation.ts
5667
- const log$2 = logger$1.child({ component: "Reconciliation" });
5724
+ const log$3 = logger$1.child({ component: "Reconciliation" });
5668
5725
  /**
5669
5726
  * How many times reconcile will re-attempt activation of an intact integration
5670
5727
  * stuck in `error` (with no reinstall requested) before giving up and waiting
@@ -5734,7 +5791,7 @@ var ReconciliationEngine = class {
5734
5791
  try {
5735
5792
  localIntegrations = await this.manager.getInstalledIntegrations();
5736
5793
  } catch (err) {
5737
- log$2.error({ err }, "Failed to get local integrations");
5794
+ log$3.error({ err }, "Failed to get local integrations");
5738
5795
  localIntegrations = [];
5739
5796
  }
5740
5797
  const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
@@ -5753,10 +5810,10 @@ var ReconciliationEngine = class {
5753
5810
  try {
5754
5811
  if (!local) {
5755
5812
  await this.ensureSuspended();
5756
- log$2.info(`Installing ${id}@${desired.version}`);
5813
+ log$3.info(`Installing ${id}@${desired.version}`);
5757
5814
  await this.manager.install(id, desired.version, desired.config, desired.customSource);
5758
5815
  report.installed.push(id);
5759
- log$2.info(`Activating ${id}`);
5816
+ log$3.info(`Activating ${id}`);
5760
5817
  if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5761
5818
  report.activated.push(id);
5762
5819
  report.results.push({
@@ -5768,7 +5825,7 @@ var ReconciliationEngine = class {
5768
5825
  }
5769
5826
  if (local.version !== desired.version && desired.version !== "" && local.version !== "unknown") {
5770
5827
  await this.ensureSuspended();
5771
- log$2.info(`Upgrading ${id}: ${local.version} → ${desired.version}`);
5828
+ log$3.info(`Upgrading ${id}: ${local.version} → ${desired.version}`);
5772
5829
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
5773
5830
  report.installed.push(id);
5774
5831
  report.activated.push(id);
@@ -5782,7 +5839,7 @@ var ReconciliationEngine = class {
5782
5839
  if (local.status === "error") {
5783
5840
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5784
5841
  await this.ensureSuspended();
5785
- log$2.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5842
+ log$3.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5786
5843
  this.manager.resetReinstallAttempts(id);
5787
5844
  this.activateAttempts.delete(id);
5788
5845
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5798,7 +5855,7 @@ var ReconciliationEngine = class {
5798
5855
  if (!await this.manager.isInstallIntact(id)) {
5799
5856
  const attempts = this.manager.getReinstallAttempts(id);
5800
5857
  if (attempts >= 3) {
5801
- log$2.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5858
+ log$3.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5802
5859
  captureIntegrationFailure(id, "reinstall", `Max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`);
5803
5860
  report.errors.push({
5804
5861
  integrationId: id,
@@ -5813,7 +5870,7 @@ var ReconciliationEngine = class {
5813
5870
  return;
5814
5871
  }
5815
5872
  await this.ensureSuspended();
5816
- log$2.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5873
+ log$3.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5817
5874
  this.manager.incrementReinstallAttempts(id);
5818
5875
  try {
5819
5876
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5828,7 +5885,7 @@ var ReconciliationEngine = class {
5828
5885
  });
5829
5886
  } catch (reinstallErr) {
5830
5887
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
5831
- log$2.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5888
+ log$3.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5832
5889
  captureIntegrationFailure(id, "reinstall", reinstallErr);
5833
5890
  report.errors.push({
5834
5891
  integrationId: id,
@@ -5847,7 +5904,7 @@ var ReconciliationEngine = class {
5847
5904
  if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
5848
5905
  this.activateAttempts.set(id, activateAttempts + 1);
5849
5906
  await this.ensureSuspended();
5850
- log$2.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5907
+ log$3.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5851
5908
  try {
5852
5909
  if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5853
5910
  this.activateAttempts.delete(id);
@@ -5860,7 +5917,7 @@ var ReconciliationEngine = class {
5860
5917
  return;
5861
5918
  } catch (reactivateErr) {
5862
5919
  const reactivateMsg = reactivateErr instanceof Error ? reactivateErr.message : String(reactivateErr);
5863
- log$2.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
5920
+ log$3.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
5864
5921
  captureIntegrationFailure(id, "reactivate", reactivateErr);
5865
5922
  report.errors.push({
5866
5923
  integrationId: id,
@@ -5875,7 +5932,7 @@ var ReconciliationEngine = class {
5875
5932
  return;
5876
5933
  }
5877
5934
  }
5878
- log$2.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5935
+ log$3.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5879
5936
  captureIntegrationFailure(id, "reactivate", `Integration ${id} stuck in error state — re-activation attempts exhausted`);
5880
5937
  report.errors.push({
5881
5938
  integrationId: id,
@@ -5891,7 +5948,7 @@ var ReconciliationEngine = class {
5891
5948
  }
5892
5949
  if (local.status !== "active") {
5893
5950
  await this.ensureSuspended();
5894
- log$2.info(`Activating ${id}`);
5951
+ log$3.info(`Activating ${id}`);
5895
5952
  if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5896
5953
  report.activated.push(id);
5897
5954
  report.results.push({
@@ -5903,7 +5960,7 @@ var ReconciliationEngine = class {
5903
5960
  }
5904
5961
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5905
5962
  await this.ensureSuspended();
5906
- log$2.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5963
+ log$3.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5907
5964
  this.manager.resetReinstallAttempts(id);
5908
5965
  this.activateAttempts.delete(id);
5909
5966
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5924,7 +5981,7 @@ var ReconciliationEngine = class {
5924
5981
  });
5925
5982
  } catch (err) {
5926
5983
  const message = err instanceof Error ? err.message : String(err);
5927
- log$2.error({ err }, `Error reconciling ${id}`);
5984
+ log$3.error({ err }, `Error reconciling ${id}`);
5928
5985
  captureIntegrationFailure(id, "reconcile_active", err);
5929
5986
  report.errors.push({
5930
5987
  integrationId: id,
@@ -5949,7 +6006,7 @@ var ReconciliationEngine = class {
5949
6006
  }
5950
6007
  try {
5951
6008
  await this.ensureSuspended();
5952
- log$2.info(`Removing ${id}`);
6009
+ log$3.info(`Removing ${id}`);
5953
6010
  if (local.status === "active") {
5954
6011
  await this.manager.deactivate(id);
5955
6012
  report.deactivated.push(id);
@@ -5963,7 +6020,7 @@ var ReconciliationEngine = class {
5963
6020
  });
5964
6021
  } catch (err) {
5965
6022
  const message = err instanceof Error ? err.message : String(err);
5966
- log$2.error({ err }, `Error removing ${id}`);
6023
+ log$3.error({ err }, `Error removing ${id}`);
5967
6024
  captureIntegrationFailure(id, "reconcile_removed", err);
5968
6025
  report.errors.push({
5969
6026
  integrationId: id,
@@ -22275,7 +22332,7 @@ var CaptureThrottle = class {
22275
22332
  * Spawns the runtime binary, pipes stdout/stderr to the daemon logger,
22276
22333
  * and restarts on crash with exponential backoff.
22277
22334
  */
22278
- const log$1 = createLogger("RuntimeProcess");
22335
+ const log$2 = createLogger("RuntimeProcess");
22279
22336
  /** Quiet-period flush for a pending multi-line error block (e.g. a traceback
22280
22337
  * followed by silence — no closing line ever arrives to complete it). */
22281
22338
  const DETECTOR_FLUSH_DEBOUNCE_MS = 1500;
@@ -22322,7 +22379,7 @@ var RuntimeProcess = class {
22322
22379
  if (this.stopped) return;
22323
22380
  const { command, args } = this.resolveCommand();
22324
22381
  this.lastStartTime = Date.now();
22325
- log$1.info({
22382
+ log$2.info({
22326
22383
  runtime: this.options.runtime,
22327
22384
  command,
22328
22385
  args,
@@ -22343,7 +22400,7 @@ var RuntimeProcess = class {
22343
22400
  this.child.stdout?.on("data", (data) => {
22344
22401
  const lines = data.toString().trim().split("\n");
22345
22402
  for (const line of lines) {
22346
- log$1.info({
22403
+ log$2.info({
22347
22404
  runtime: this.options.runtime,
22348
22405
  stream: "stdout"
22349
22406
  }, line);
@@ -22353,7 +22410,7 @@ var RuntimeProcess = class {
22353
22410
  this.child.stderr?.on("data", (data) => {
22354
22411
  const lines = data.toString().trim().split("\n");
22355
22412
  for (const line of lines) {
22356
- log$1.warn({
22413
+ log$2.warn({
22357
22414
  runtime: this.options.runtime,
22358
22415
  stream: "stderr"
22359
22416
  }, line);
@@ -22363,7 +22420,7 @@ var RuntimeProcess = class {
22363
22420
  this.child.on("exit", (code, signal) => {
22364
22421
  this.child = null;
22365
22422
  if (this.stopped) {
22366
- log$1.info({
22423
+ log$2.info({
22367
22424
  runtime: this.options.runtime,
22368
22425
  code,
22369
22426
  signal
@@ -22371,7 +22428,7 @@ var RuntimeProcess = class {
22371
22428
  return;
22372
22429
  }
22373
22430
  if (code === 0 && signal == null) {
22374
- log$1.info({
22431
+ log$2.info({
22375
22432
  runtime: this.options.runtime,
22376
22433
  code
22377
22434
  }, "Runtime exited gracefully — restarting");
@@ -22381,7 +22438,7 @@ var RuntimeProcess = class {
22381
22438
  }, 500);
22382
22439
  return;
22383
22440
  }
22384
- log$1.warn({
22441
+ log$2.warn({
22385
22442
  runtime: this.options.runtime,
22386
22443
  code,
22387
22444
  signal,
@@ -22402,7 +22459,7 @@ var RuntimeProcess = class {
22402
22459
  recentOutput: this.ringBuffer.snapshot(),
22403
22460
  crashesSuppressed: crash.suppressedCount
22404
22461
  });
22405
- else log$1.debug({
22462
+ else log$2.debug({
22406
22463
  runtime: this.options.runtime,
22407
22464
  suppressed: crash.suppressedCount
22408
22465
  }, "Crash capture suppressed by throttle");
@@ -22414,7 +22471,7 @@ var RuntimeProcess = class {
22414
22471
  this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
22415
22472
  });
22416
22473
  this.child.on("error", (err) => {
22417
- log$1.error({
22474
+ log$2.error({
22418
22475
  runtime: this.options.runtime,
22419
22476
  err: err.message
22420
22477
  }, "Runtime process error");
@@ -22453,7 +22510,7 @@ var RuntimeProcess = class {
22453
22510
  this.emitErrorBlock(this.detector.flush());
22454
22511
  }, DETECTOR_FLUSH_DEBOUNCE_MS);
22455
22512
  } catch (err) {
22456
- log$1.debug({
22513
+ log$2.debug({
22457
22514
  runtime: this.options.runtime,
22458
22515
  err: err instanceof Error ? err.message : String(err)
22459
22516
  }, "Runtime output observation failed");
@@ -22464,7 +22521,7 @@ var RuntimeProcess = class {
22464
22521
  if (!block) return;
22465
22522
  const { allow, suppressedCount } = this.throttle.allowErrorCapture(block.fingerprintKey);
22466
22523
  if (!allow) {
22467
- log$1.debug({
22524
+ log$2.debug({
22468
22525
  runtime: this.options.runtime,
22469
22526
  kind: block.kind,
22470
22527
  suppressed: suppressedCount
@@ -22498,7 +22555,7 @@ var RuntimeProcess = class {
22498
22555
  if (!child) return;
22499
22556
  return new Promise((resolve) => {
22500
22557
  const killTimer = setTimeout(() => {
22501
- log$1.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
22558
+ log$2.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
22502
22559
  child.kill("SIGKILL");
22503
22560
  }, 5e3);
22504
22561
  child.on("exit", () => {
@@ -22523,7 +22580,7 @@ var RuntimeProcess = class {
22523
22580
  * Restart the runtime process (stop then start with fresh backoff).
22524
22581
  */
22525
22582
  async restart() {
22526
- log$1.info({ runtime: this.options.runtime }, "Restarting runtime...");
22583
+ log$2.info({ runtime: this.options.runtime }, "Restarting runtime...");
22527
22584
  await this.stop();
22528
22585
  this.stopped = false;
22529
22586
  this.backoffMs = BACKOFF_INITIAL_MS;
@@ -22545,7 +22602,7 @@ var RuntimeProcess = class {
22545
22602
  * this registry from active integration manifests. Handler files are loaded
22546
22603
  * via ESM dynamic import on first use and cached until the registry is cleared.
22547
22604
  */
22548
- const log = createLogger("CommandRegistry");
22605
+ const log$1 = createLogger("CommandRegistry");
22549
22606
  var CommandRegistry = class {
22550
22607
  commands = /* @__PURE__ */ new Map();
22551
22608
  version = 0;
@@ -22556,7 +22613,7 @@ var CommandRegistry = class {
22556
22613
  register(integrationId, name, handlerPath, method = "handle", timeoutMs = 3e4) {
22557
22614
  const existing = this.commands.get(name);
22558
22615
  if (existing) {
22559
- log.warn({
22616
+ log$1.warn({
22560
22617
  command: name,
22561
22618
  existing: existing.integrationId,
22562
22619
  attempted: integrationId
@@ -22564,7 +22621,7 @@ var CommandRegistry = class {
22564
22621
  return;
22565
22622
  }
22566
22623
  if (!existsSync(handlerPath)) {
22567
- log.warn({
22624
+ log$1.warn({
22568
22625
  command: name,
22569
22626
  handlerPath,
22570
22627
  integrationId
@@ -22579,7 +22636,7 @@ var CommandRegistry = class {
22579
22636
  timeoutMs,
22580
22637
  handler: null
22581
22638
  });
22582
- log.info({
22639
+ log$1.info({
22583
22640
  command: name,
22584
22641
  integrationId,
22585
22642
  handlerPath,
@@ -22593,7 +22650,7 @@ var CommandRegistry = class {
22593
22650
  unregisterAll(integrationId) {
22594
22651
  for (const [name, entry] of this.commands) if (entry.integrationId === integrationId) {
22595
22652
  this.commands.delete(name);
22596
- log.info({
22653
+ log$1.info({
22597
22654
  command: name,
22598
22655
  integrationId
22599
22656
  }, "Unregistered command");
@@ -22631,7 +22688,7 @@ var CommandRegistry = class {
22631
22688
  entry.handler = fn;
22632
22689
  } catch (err) {
22633
22690
  const message = err instanceof Error ? err.message : String(err);
22634
- log.error({
22691
+ log$1.error({
22635
22692
  err: message,
22636
22693
  command: name,
22637
22694
  handlerPath: entry.handlerPath
@@ -22652,7 +22709,7 @@ var CommandRegistry = class {
22652
22709
  })]);
22653
22710
  } catch (err) {
22654
22711
  const message = err instanceof Error ? err.message : String(err);
22655
- log.error({
22712
+ log$1.error({
22656
22713
  err: message,
22657
22714
  command: name
22658
22715
  }, "Command execution failed");
@@ -22681,7 +22738,7 @@ var CommandRegistry = class {
22681
22738
  clear() {
22682
22739
  this.commands.clear();
22683
22740
  this.version++;
22684
- log.info({ version: this.version }, "Command registry cleared");
22741
+ log$1.info({ version: this.version }, "Command registry cleared");
22685
22742
  }
22686
22743
  };
22687
22744
  //#endregion
@@ -22970,6 +23027,93 @@ function writeSentinel(path) {
22970
23027
  } catch {}
22971
23028
  }
22972
23029
  //#endregion
23030
+ //#region src/mcp-error-capture.ts
23031
+ const log = createLogger("McpErrorCapture");
23032
+ /** Quiet-period flush for a pending multi-line stderr block. */
23033
+ const MCP_STDERR_FLUSH_DEBOUNCE_MS = 1500;
23034
+ function createMcpErrorHooks() {
23035
+ const resultErrorThrottle = new CaptureThrottle(6e4);
23036
+ const errorThrottle = new CaptureThrottle(6e4);
23037
+ const crashThrottle = new CaptureThrottle(6e4);
23038
+ const throttleFor = (kind) => kind === "tool-result-error" ? resultErrorThrottle : kind === "server-crash" ? crashThrottle : errorThrottle;
23039
+ const detectors = /* @__PURE__ */ new Map();
23040
+ const flushTimers = /* @__PURE__ */ new Map();
23041
+ const capture = (opts) => {
23042
+ const { allow, suppressedCount } = throttleFor(opts.kind).allowErrorCapture(opts.fingerprintKey);
23043
+ if (!allow) {
23044
+ log.debug({
23045
+ server: opts.server,
23046
+ kind: opts.kind,
23047
+ suppressed: suppressedCount
23048
+ }, "MCP failure capture suppressed by throttle");
23049
+ return;
23050
+ }
23051
+ captureMcpFailure({
23052
+ ...opts,
23053
+ suppressedCount
23054
+ });
23055
+ };
23056
+ const emitBlock = (server, block) => {
23057
+ if (!block) return;
23058
+ capture({
23059
+ server,
23060
+ kind: "stderr-output",
23061
+ message: block.lines[0],
23062
+ lines: block.lines,
23063
+ fingerprintKey: `stderr:${server}:${block.fingerprintKey}`
23064
+ });
23065
+ };
23066
+ return {
23067
+ onToolError: (info) => {
23068
+ capture({
23069
+ server: info.server,
23070
+ tool: info.tool,
23071
+ kind: info.kind === "thrown" ? "tool-thrown" : "tool-result-error",
23072
+ message: info.message,
23073
+ fingerprintKey: `tool:${info.server}:${info.tool}:${toFingerprintKey(info.message)}`
23074
+ });
23075
+ },
23076
+ onServerCrash: (server) => {
23077
+ const timer = flushTimers.get(server);
23078
+ if (timer) {
23079
+ clearTimeout(timer);
23080
+ flushTimers.delete(server);
23081
+ }
23082
+ emitBlock(server, detectors.get(server)?.flush() ?? null);
23083
+ capture({
23084
+ server,
23085
+ kind: "server-crash",
23086
+ message: "MCP server connection closed unexpectedly",
23087
+ fingerprintKey: `crash:${server}`
23088
+ });
23089
+ },
23090
+ onServerStderr: (server, line) => {
23091
+ let detector = detectors.get(server);
23092
+ if (!detector) {
23093
+ detector = new ErrorLineDetector();
23094
+ detectors.set(server, detector);
23095
+ }
23096
+ for (const block of detector.feed("stderr", line)) emitBlock(server, block);
23097
+ const existing = flushTimers.get(server);
23098
+ if (existing) {
23099
+ clearTimeout(existing);
23100
+ flushTimers.delete(server);
23101
+ }
23102
+ if (detector.isCollecting) {
23103
+ const target = detector;
23104
+ flushTimers.set(server, setTimeout(() => {
23105
+ flushTimers.delete(server);
23106
+ emitBlock(server, target.flush());
23107
+ }, MCP_STDERR_FLUSH_DEBOUNCE_MS));
23108
+ }
23109
+ },
23110
+ dispose: () => {
23111
+ for (const timer of flushTimers.values()) clearTimeout(timer);
23112
+ flushTimers.clear();
23113
+ }
23114
+ };
23115
+ }
23116
+ //#endregion
22973
23117
  //#region src/daemon.ts
22974
23118
  /**
22975
23119
  * Alfe Gateway Daemon — main entry point.
@@ -23354,9 +23498,11 @@ async function startDaemon() {
23354
23498
  });
23355
23499
  const mcpManager = new Manager({ logger: logger$1 });
23356
23500
  mcpManagerRef = mcpManager;
23501
+ const mcpErrorHooks = createMcpErrorHooks();
23357
23502
  mcpBundler = new McpBundler({
23358
23503
  logger: logger$1,
23359
- idleTtlMs: 0
23504
+ idleTtlMs: 0,
23505
+ ...mcpErrorHooks
23360
23506
  }, { connect: defaultConnect });
23361
23507
  if (config.runtime === "hermes") logger$1.info("Hermes runtime — daemon MCP bundler left idle (store mirrored to config.yaml by HermesMcpSync)");
23362
23508
  else {
@@ -23483,6 +23629,7 @@ async function startDaemon() {
23483
23629
  mcpBundler = null;
23484
23630
  logger$1.debug("MCP bundler stopped");
23485
23631
  }
23632
+ mcpErrorHooks.dispose();
23486
23633
  logger$1.debug("Stopping MCP bundler manager...");
23487
23634
  await mcpManager.dispose();
23488
23635
  mcpManagerRef = null;
@@ -24207,4 +24354,4 @@ function formatDuration(ms) {
24207
24354
  return `${String(Math.round(seconds / 3600))}h`;
24208
24355
  }
24209
24356
  //#endregion
24210
- export { fetchAgentConfig as C, PINNED_OPENCLAW_VERSION as E, SOCKET_PATH as S, resolveAgentIdentity as T, initAgentSentry as _, installService as a, ALFE_DIR as b, uninstallService as c, captureCliFailure as d, captureFatal as f, flushSentry as g, captureRuntimeErrorOutput as h, checkExistingDaemon as i, AGENT_DAEMON_SENTRY_DSN as l, captureRuntimeCrash as m, queryDaemonHealth as n, startService as o, captureIntegrationFailure as p, startDaemon as r, stopExistingDaemon as s, formatHealthReport as t, AGENT_RUNTIME_SENTRY_DSN as u, setAgentContext as v, loadDaemonConfig as w, PID_PATH as x, PROTOCOL_VERSION as y };
24357
+ export { PID_PATH as C, resolveAgentIdentity as D, loadDaemonConfig as E, PINNED_OPENCLAW_VERSION as O, ALFE_DIR as S, fetchAgentConfig as T, captureRuntimeErrorOutput as _, installService as a, setAgentContext as b, uninstallService as c, AGENT_RUNTIME_SENTRY_DSN as d, captureCliFailure as f, captureRuntimeCrash as g, captureMcpFailure as h, checkExistingDaemon as i, AGENT_DAEMON_SENTRY_DSN as l, captureIntegrationFailure as m, queryDaemonHealth as n, startService as o, captureFatal as p, startDaemon as r, stopExistingDaemon as s, formatHealthReport as t, AGENT_MCP_SENTRY_DSN as u, flushSentry as v, SOCKET_PATH as w, PROTOCOL_VERSION as x, initAgentSentry as y };
@@ -81,6 +81,13 @@ declare const AGENT_DAEMON_SENTRY_DSN = "https://a47f010d8d39fb4350f913492189f28
81
81
  * Mirrors `SENTRY_DSNS.agentRuntime` in `config/config.ts`.
82
82
  */
83
83
  declare const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
84
+ /**
85
+ * MCP tool/server failures on agent VMs — reported by the daemon-hosted MCP
86
+ * bundler (tool errors, child crashes, stderr output). This is the SAME
87
+ * project as the cloud MCP Fly service (`SENTRY_DSNS.mcp`); agent-side events
88
+ * are distinguished by `source: agent-daemon` + `agentId` tags.
89
+ */
90
+ declare const AGENT_MCP_SENTRY_DSN = "https://638309eac96146a945f46b578f10c806@o4511008239452160.ingest.us.sentry.io/4511146364174336";
84
91
  /** Which surface initialised Sentry — tagged on every event. */
85
92
  type SentrySurface = "cli" | "daemon";
86
93
  interface InitAgentSentryOptions {
@@ -157,6 +164,21 @@ declare function captureRuntimeErrorOutput(opts: {
157
164
  fingerprintKey: string;
158
165
  suppressedCount?: number;
159
166
  }): void;
167
+ /**
168
+ * Capture an MCP failure (tool error, server crash, or error-looking stderr
169
+ * output) into the `mcp` project via the daemon's bound MCP client. Throttling
170
+ * is the caller's job (`mcp-error-capture.ts`). No-op when the MCP client is
171
+ * not initialised. Never throws.
172
+ */
173
+ declare function captureMcpFailure(opts: {
174
+ server: string;
175
+ tool?: string;
176
+ kind: "tool-thrown" | "tool-result-error" | "server-crash" | "stderr-output";
177
+ message: string;
178
+ /** Optional context lines (stderr block) — attached as breadcrumbs. */
179
+ lines?: readonly string[];
180
+ suppressedCount?: number;
181
+ }): void;
160
182
  /** Flush queued events (small timeout) so short-lived commands deliver them. */
161
183
  declare function flushSentry(timeoutMs?: number): Promise<void>;
162
184
  /**
@@ -338,4 +360,4 @@ declare function checkExistingDaemon(): Promise<number | null>;
338
360
  */
339
361
  declare function stopExistingDaemon(): Promise<boolean>;
340
362
  //#endregion
341
- export { AGENT_DAEMON_SENTRY_DSN, AGENT_RUNTIME_SENTRY_DSN, ALFE_DIR, type AgentIdentity, type AgentWorkspaceConfig, type DaemonConfig, type DaemonHealth, type IPCEvent, type IPCRequest, type IPCResponse, type InitAgentSentryOptions, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, type RuntimeOutputLine, SOCKET_PATH, type SentrySurface, captureCliFailure, captureFatal, captureIntegrationFailure, captureRuntimeCrash, captureRuntimeErrorOutput, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
363
+ export { AGENT_DAEMON_SENTRY_DSN, AGENT_MCP_SENTRY_DSN, AGENT_RUNTIME_SENTRY_DSN, ALFE_DIR, type AgentIdentity, type AgentWorkspaceConfig, type DaemonConfig, type DaemonHealth, type IPCEvent, type IPCRequest, type IPCResponse, type InitAgentSentryOptions, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, type RuntimeOutputLine, SOCKET_PATH, type SentrySurface, captureCliFailure, captureFatal, captureIntegrationFailure, captureMcpFailure, captureRuntimeCrash, captureRuntimeErrorOutput, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
package/dist/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { C as fetchAgentConfig, E as PINNED_OPENCLAW_VERSION, S as SOCKET_PATH, T as resolveAgentIdentity, _ as initAgentSentry, a as installService, b as ALFE_DIR, c as uninstallService, d as captureCliFailure, f as captureFatal, g as flushSentry, h as captureRuntimeErrorOutput, i as checkExistingDaemon, l as AGENT_DAEMON_SENTRY_DSN, m as captureRuntimeCrash, n as queryDaemonHealth, o as startService, p as captureIntegrationFailure, r as startDaemon, s as stopExistingDaemon, t as formatHealthReport, u as AGENT_RUNTIME_SENTRY_DSN, v as setAgentContext, w as loadDaemonConfig, x as PID_PATH, y as PROTOCOL_VERSION } from "../health.js";
1
+ import { C as PID_PATH, D as resolveAgentIdentity, E as loadDaemonConfig, O as PINNED_OPENCLAW_VERSION, S as ALFE_DIR, T as fetchAgentConfig, _ as captureRuntimeErrorOutput, a as installService, b as setAgentContext, c as uninstallService, d as AGENT_RUNTIME_SENTRY_DSN, f as captureCliFailure, g as captureRuntimeCrash, h as captureMcpFailure, i as checkExistingDaemon, l as AGENT_DAEMON_SENTRY_DSN, m as captureIntegrationFailure, n as queryDaemonHealth, o as startService, p as captureFatal, r as startDaemon, s as stopExistingDaemon, t as formatHealthReport, u as AGENT_MCP_SENTRY_DSN, v as flushSentry, w as SOCKET_PATH, x as PROTOCOL_VERSION, y as initAgentSentry } from "../health.js";
2
2
  import { n as logger } from "../logger.js";
3
- export { AGENT_DAEMON_SENTRY_DSN, AGENT_RUNTIME_SENTRY_DSN, ALFE_DIR, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, SOCKET_PATH, captureCliFailure, captureFatal, captureIntegrationFailure, captureRuntimeCrash, captureRuntimeErrorOutput, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
3
+ export { AGENT_DAEMON_SENTRY_DSN, AGENT_MCP_SENTRY_DSN, AGENT_RUNTIME_SENTRY_DSN, ALFE_DIR, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, SOCKET_PATH, captureCliFailure, captureFatal, captureIntegrationFailure, captureMcpFailure, captureRuntimeCrash, captureRuntimeErrorOutput, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,12 +23,12 @@
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.7.0",
26
+ "@alfe.ai/agent-api-client": "^0.8.0",
27
27
  "@alfe.ai/ai-proxy-local": "^0.0.13",
28
28
  "@alfe.ai/config": "^0.3.0",
29
29
  "@alfe.ai/integration-manifest": "^0.3.1",
30
- "@alfe.ai/integrations": "^0.2.8",
31
- "@alfe.ai/mcp-bundler": "^0.2.2"
30
+ "@alfe.ai/integrations": "^0.2.10",
31
+ "@alfe.ai/mcp-bundler": "^0.3.0"
32
32
  },
33
33
  "license": "UNLICENSED",
34
34
  "homepage": "https://alfe.ai",