@alfe.ai/gateway 0.4.0 → 0.6.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({
@@ -5767,22 +5824,54 @@ var ReconciliationEngine = class {
5767
5824
  return;
5768
5825
  }
5769
5826
  if (local.version !== desired.version && desired.version !== "" && local.version !== "unknown") {
5770
- await this.ensureSuspended();
5771
- log$2.info(`Upgrading ${id}: ${local.version} → ${desired.version}`);
5772
- if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
5773
- report.installed.push(id);
5774
- report.activated.push(id);
5775
- report.results.push({
5776
- integrationId: id,
5777
- action: "activated",
5778
- actualStatus: "active"
5779
- });
5827
+ if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5828
+ await this.ensureSuspended();
5829
+ log$3.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} ${desired.version}`);
5830
+ this.manager.resetReinstallAttempts(id);
5831
+ this.activateAttempts.delete(id);
5832
+ if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
5833
+ report.installed.push(id);
5834
+ report.activated.push(id);
5835
+ report.results.push({
5836
+ integrationId: id,
5837
+ action: "installed",
5838
+ actualStatus: "active"
5839
+ });
5840
+ return;
5841
+ }
5842
+ log$3.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
5843
+ try {
5844
+ if ((await this.manager.upgrade(id, desired.version, desired.config, desired.customSource, { onBeforeRuntimeMutation: () => this.ensureSuspended() })).configApplied) report.configApplied = true;
5845
+ this.manager.resetReinstallAttempts(id);
5846
+ this.activateAttempts.delete(id);
5847
+ report.installed.push(id);
5848
+ report.activated.push(id);
5849
+ report.results.push({
5850
+ integrationId: id,
5851
+ action: "activated",
5852
+ actualStatus: "active"
5853
+ });
5854
+ } catch (upgradeErr) {
5855
+ const upgradeMsg = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
5856
+ log$3.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
5857
+ captureIntegrationFailure(id, "upgrade", upgradeErr);
5858
+ report.errors.push({
5859
+ integrationId: id,
5860
+ error: upgradeMsg
5861
+ });
5862
+ report.results.push({
5863
+ integrationId: id,
5864
+ action: "error",
5865
+ actualStatus: "error",
5866
+ errorMessage: upgradeMsg
5867
+ });
5868
+ }
5780
5869
  return;
5781
5870
  }
5782
5871
  if (local.status === "error") {
5783
5872
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5784
5873
  await this.ensureSuspended();
5785
- log$2.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5874
+ log$3.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5786
5875
  this.manager.resetReinstallAttempts(id);
5787
5876
  this.activateAttempts.delete(id);
5788
5877
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5798,7 +5887,7 @@ var ReconciliationEngine = class {
5798
5887
  if (!await this.manager.isInstallIntact(id)) {
5799
5888
  const attempts = this.manager.getReinstallAttempts(id);
5800
5889
  if (attempts >= 3) {
5801
- log$2.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5890
+ log$3.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5802
5891
  captureIntegrationFailure(id, "reinstall", `Max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`);
5803
5892
  report.errors.push({
5804
5893
  integrationId: id,
@@ -5813,7 +5902,7 @@ var ReconciliationEngine = class {
5813
5902
  return;
5814
5903
  }
5815
5904
  await this.ensureSuspended();
5816
- log$2.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5905
+ log$3.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5817
5906
  this.manager.incrementReinstallAttempts(id);
5818
5907
  try {
5819
5908
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5828,7 +5917,7 @@ var ReconciliationEngine = class {
5828
5917
  });
5829
5918
  } catch (reinstallErr) {
5830
5919
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
5831
- log$2.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5920
+ log$3.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5832
5921
  captureIntegrationFailure(id, "reinstall", reinstallErr);
5833
5922
  report.errors.push({
5834
5923
  integrationId: id,
@@ -5847,9 +5936,9 @@ var ReconciliationEngine = class {
5847
5936
  if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
5848
5937
  this.activateAttempts.set(id, activateAttempts + 1);
5849
5938
  await this.ensureSuspended();
5850
- log$2.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5939
+ log$3.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5851
5940
  try {
5852
- if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5941
+ if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
5853
5942
  this.activateAttempts.delete(id);
5854
5943
  report.activated.push(id);
5855
5944
  report.results.push({
@@ -5860,7 +5949,7 @@ var ReconciliationEngine = class {
5860
5949
  return;
5861
5950
  } catch (reactivateErr) {
5862
5951
  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}`);
5952
+ log$3.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
5864
5953
  captureIntegrationFailure(id, "reactivate", reactivateErr);
5865
5954
  report.errors.push({
5866
5955
  integrationId: id,
@@ -5875,7 +5964,7 @@ var ReconciliationEngine = class {
5875
5964
  return;
5876
5965
  }
5877
5966
  }
5878
- log$2.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5967
+ log$3.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5879
5968
  captureIntegrationFailure(id, "reactivate", `Integration ${id} stuck in error state — re-activation attempts exhausted`);
5880
5969
  report.errors.push({
5881
5970
  integrationId: id,
@@ -5891,8 +5980,8 @@ var ReconciliationEngine = class {
5891
5980
  }
5892
5981
  if (local.status !== "active") {
5893
5982
  await this.ensureSuspended();
5894
- log$2.info(`Activating ${id}`);
5895
- if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5983
+ log$3.info(`Activating ${id}`);
5984
+ if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
5896
5985
  report.activated.push(id);
5897
5986
  report.results.push({
5898
5987
  integrationId: id,
@@ -5903,7 +5992,7 @@ var ReconciliationEngine = class {
5903
5992
  }
5904
5993
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5905
5994
  await this.ensureSuspended();
5906
- log$2.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5995
+ log$3.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5907
5996
  this.manager.resetReinstallAttempts(id);
5908
5997
  this.activateAttempts.delete(id);
5909
5998
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5924,7 +6013,7 @@ var ReconciliationEngine = class {
5924
6013
  });
5925
6014
  } catch (err) {
5926
6015
  const message = err instanceof Error ? err.message : String(err);
5927
- log$2.error({ err }, `Error reconciling ${id}`);
6016
+ log$3.error({ err }, `Error reconciling ${id}`);
5928
6017
  captureIntegrationFailure(id, "reconcile_active", err);
5929
6018
  report.errors.push({
5930
6019
  integrationId: id,
@@ -5949,7 +6038,7 @@ var ReconciliationEngine = class {
5949
6038
  }
5950
6039
  try {
5951
6040
  await this.ensureSuspended();
5952
- log$2.info(`Removing ${id}`);
6041
+ log$3.info(`Removing ${id}`);
5953
6042
  if (local.status === "active") {
5954
6043
  await this.manager.deactivate(id);
5955
6044
  report.deactivated.push(id);
@@ -5963,7 +6052,7 @@ var ReconciliationEngine = class {
5963
6052
  });
5964
6053
  } catch (err) {
5965
6054
  const message = err instanceof Error ? err.message : String(err);
5966
- log$2.error({ err }, `Error removing ${id}`);
6055
+ log$3.error({ err }, `Error removing ${id}`);
5967
6056
  captureIntegrationFailure(id, "reconcile_removed", err);
5968
6057
  report.errors.push({
5969
6058
  integrationId: id,
@@ -22275,7 +22364,7 @@ var CaptureThrottle = class {
22275
22364
  * Spawns the runtime binary, pipes stdout/stderr to the daemon logger,
22276
22365
  * and restarts on crash with exponential backoff.
22277
22366
  */
22278
- const log$1 = createLogger("RuntimeProcess");
22367
+ const log$2 = createLogger("RuntimeProcess");
22279
22368
  /** Quiet-period flush for a pending multi-line error block (e.g. a traceback
22280
22369
  * followed by silence — no closing line ever arrives to complete it). */
22281
22370
  const DETECTOR_FLUSH_DEBOUNCE_MS = 1500;
@@ -22322,7 +22411,7 @@ var RuntimeProcess = class {
22322
22411
  if (this.stopped) return;
22323
22412
  const { command, args } = this.resolveCommand();
22324
22413
  this.lastStartTime = Date.now();
22325
- log$1.info({
22414
+ log$2.info({
22326
22415
  runtime: this.options.runtime,
22327
22416
  command,
22328
22417
  args,
@@ -22343,7 +22432,7 @@ var RuntimeProcess = class {
22343
22432
  this.child.stdout?.on("data", (data) => {
22344
22433
  const lines = data.toString().trim().split("\n");
22345
22434
  for (const line of lines) {
22346
- log$1.info({
22435
+ log$2.info({
22347
22436
  runtime: this.options.runtime,
22348
22437
  stream: "stdout"
22349
22438
  }, line);
@@ -22353,7 +22442,7 @@ var RuntimeProcess = class {
22353
22442
  this.child.stderr?.on("data", (data) => {
22354
22443
  const lines = data.toString().trim().split("\n");
22355
22444
  for (const line of lines) {
22356
- log$1.warn({
22445
+ log$2.warn({
22357
22446
  runtime: this.options.runtime,
22358
22447
  stream: "stderr"
22359
22448
  }, line);
@@ -22363,7 +22452,7 @@ var RuntimeProcess = class {
22363
22452
  this.child.on("exit", (code, signal) => {
22364
22453
  this.child = null;
22365
22454
  if (this.stopped) {
22366
- log$1.info({
22455
+ log$2.info({
22367
22456
  runtime: this.options.runtime,
22368
22457
  code,
22369
22458
  signal
@@ -22371,7 +22460,7 @@ var RuntimeProcess = class {
22371
22460
  return;
22372
22461
  }
22373
22462
  if (code === 0 && signal == null) {
22374
- log$1.info({
22463
+ log$2.info({
22375
22464
  runtime: this.options.runtime,
22376
22465
  code
22377
22466
  }, "Runtime exited gracefully — restarting");
@@ -22381,7 +22470,7 @@ var RuntimeProcess = class {
22381
22470
  }, 500);
22382
22471
  return;
22383
22472
  }
22384
- log$1.warn({
22473
+ log$2.warn({
22385
22474
  runtime: this.options.runtime,
22386
22475
  code,
22387
22476
  signal,
@@ -22402,7 +22491,7 @@ var RuntimeProcess = class {
22402
22491
  recentOutput: this.ringBuffer.snapshot(),
22403
22492
  crashesSuppressed: crash.suppressedCount
22404
22493
  });
22405
- else log$1.debug({
22494
+ else log$2.debug({
22406
22495
  runtime: this.options.runtime,
22407
22496
  suppressed: crash.suppressedCount
22408
22497
  }, "Crash capture suppressed by throttle");
@@ -22414,7 +22503,7 @@ var RuntimeProcess = class {
22414
22503
  this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
22415
22504
  });
22416
22505
  this.child.on("error", (err) => {
22417
- log$1.error({
22506
+ log$2.error({
22418
22507
  runtime: this.options.runtime,
22419
22508
  err: err.message
22420
22509
  }, "Runtime process error");
@@ -22453,7 +22542,7 @@ var RuntimeProcess = class {
22453
22542
  this.emitErrorBlock(this.detector.flush());
22454
22543
  }, DETECTOR_FLUSH_DEBOUNCE_MS);
22455
22544
  } catch (err) {
22456
- log$1.debug({
22545
+ log$2.debug({
22457
22546
  runtime: this.options.runtime,
22458
22547
  err: err instanceof Error ? err.message : String(err)
22459
22548
  }, "Runtime output observation failed");
@@ -22464,7 +22553,7 @@ var RuntimeProcess = class {
22464
22553
  if (!block) return;
22465
22554
  const { allow, suppressedCount } = this.throttle.allowErrorCapture(block.fingerprintKey);
22466
22555
  if (!allow) {
22467
- log$1.debug({
22556
+ log$2.debug({
22468
22557
  runtime: this.options.runtime,
22469
22558
  kind: block.kind,
22470
22559
  suppressed: suppressedCount
@@ -22498,7 +22587,7 @@ var RuntimeProcess = class {
22498
22587
  if (!child) return;
22499
22588
  return new Promise((resolve) => {
22500
22589
  const killTimer = setTimeout(() => {
22501
- log$1.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
22590
+ log$2.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
22502
22591
  child.kill("SIGKILL");
22503
22592
  }, 5e3);
22504
22593
  child.on("exit", () => {
@@ -22523,7 +22612,7 @@ var RuntimeProcess = class {
22523
22612
  * Restart the runtime process (stop then start with fresh backoff).
22524
22613
  */
22525
22614
  async restart() {
22526
- log$1.info({ runtime: this.options.runtime }, "Restarting runtime...");
22615
+ log$2.info({ runtime: this.options.runtime }, "Restarting runtime...");
22527
22616
  await this.stop();
22528
22617
  this.stopped = false;
22529
22618
  this.backoffMs = BACKOFF_INITIAL_MS;
@@ -22545,7 +22634,7 @@ var RuntimeProcess = class {
22545
22634
  * this registry from active integration manifests. Handler files are loaded
22546
22635
  * via ESM dynamic import on first use and cached until the registry is cleared.
22547
22636
  */
22548
- const log = createLogger("CommandRegistry");
22637
+ const log$1 = createLogger("CommandRegistry");
22549
22638
  var CommandRegistry = class {
22550
22639
  commands = /* @__PURE__ */ new Map();
22551
22640
  version = 0;
@@ -22556,7 +22645,7 @@ var CommandRegistry = class {
22556
22645
  register(integrationId, name, handlerPath, method = "handle", timeoutMs = 3e4) {
22557
22646
  const existing = this.commands.get(name);
22558
22647
  if (existing) {
22559
- log.warn({
22648
+ log$1.warn({
22560
22649
  command: name,
22561
22650
  existing: existing.integrationId,
22562
22651
  attempted: integrationId
@@ -22564,7 +22653,7 @@ var CommandRegistry = class {
22564
22653
  return;
22565
22654
  }
22566
22655
  if (!existsSync(handlerPath)) {
22567
- log.warn({
22656
+ log$1.warn({
22568
22657
  command: name,
22569
22658
  handlerPath,
22570
22659
  integrationId
@@ -22579,7 +22668,7 @@ var CommandRegistry = class {
22579
22668
  timeoutMs,
22580
22669
  handler: null
22581
22670
  });
22582
- log.info({
22671
+ log$1.info({
22583
22672
  command: name,
22584
22673
  integrationId,
22585
22674
  handlerPath,
@@ -22593,7 +22682,7 @@ var CommandRegistry = class {
22593
22682
  unregisterAll(integrationId) {
22594
22683
  for (const [name, entry] of this.commands) if (entry.integrationId === integrationId) {
22595
22684
  this.commands.delete(name);
22596
- log.info({
22685
+ log$1.info({
22597
22686
  command: name,
22598
22687
  integrationId
22599
22688
  }, "Unregistered command");
@@ -22631,7 +22720,7 @@ var CommandRegistry = class {
22631
22720
  entry.handler = fn;
22632
22721
  } catch (err) {
22633
22722
  const message = err instanceof Error ? err.message : String(err);
22634
- log.error({
22723
+ log$1.error({
22635
22724
  err: message,
22636
22725
  command: name,
22637
22726
  handlerPath: entry.handlerPath
@@ -22652,7 +22741,7 @@ var CommandRegistry = class {
22652
22741
  })]);
22653
22742
  } catch (err) {
22654
22743
  const message = err instanceof Error ? err.message : String(err);
22655
- log.error({
22744
+ log$1.error({
22656
22745
  err: message,
22657
22746
  command: name
22658
22747
  }, "Command execution failed");
@@ -22681,7 +22770,7 @@ var CommandRegistry = class {
22681
22770
  clear() {
22682
22771
  this.commands.clear();
22683
22772
  this.version++;
22684
- log.info({ version: this.version }, "Command registry cleared");
22773
+ log$1.info({ version: this.version }, "Command registry cleared");
22685
22774
  }
22686
22775
  };
22687
22776
  //#endregion
@@ -22970,6 +23059,93 @@ function writeSentinel(path) {
22970
23059
  } catch {}
22971
23060
  }
22972
23061
  //#endregion
23062
+ //#region src/mcp-error-capture.ts
23063
+ const log = createLogger("McpErrorCapture");
23064
+ /** Quiet-period flush for a pending multi-line stderr block. */
23065
+ const MCP_STDERR_FLUSH_DEBOUNCE_MS = 1500;
23066
+ function createMcpErrorHooks() {
23067
+ const resultErrorThrottle = new CaptureThrottle(6e4);
23068
+ const errorThrottle = new CaptureThrottle(6e4);
23069
+ const crashThrottle = new CaptureThrottle(6e4);
23070
+ const throttleFor = (kind) => kind === "tool-result-error" ? resultErrorThrottle : kind === "server-crash" ? crashThrottle : errorThrottle;
23071
+ const detectors = /* @__PURE__ */ new Map();
23072
+ const flushTimers = /* @__PURE__ */ new Map();
23073
+ const capture = (opts) => {
23074
+ const { allow, suppressedCount } = throttleFor(opts.kind).allowErrorCapture(opts.fingerprintKey);
23075
+ if (!allow) {
23076
+ log.debug({
23077
+ server: opts.server,
23078
+ kind: opts.kind,
23079
+ suppressed: suppressedCount
23080
+ }, "MCP failure capture suppressed by throttle");
23081
+ return;
23082
+ }
23083
+ captureMcpFailure({
23084
+ ...opts,
23085
+ suppressedCount
23086
+ });
23087
+ };
23088
+ const emitBlock = (server, block) => {
23089
+ if (!block) return;
23090
+ capture({
23091
+ server,
23092
+ kind: "stderr-output",
23093
+ message: block.lines[0],
23094
+ lines: block.lines,
23095
+ fingerprintKey: `stderr:${server}:${block.fingerprintKey}`
23096
+ });
23097
+ };
23098
+ return {
23099
+ onToolError: (info) => {
23100
+ capture({
23101
+ server: info.server,
23102
+ tool: info.tool,
23103
+ kind: info.kind === "thrown" ? "tool-thrown" : "tool-result-error",
23104
+ message: info.message,
23105
+ fingerprintKey: `tool:${info.server}:${info.tool}:${toFingerprintKey(info.message)}`
23106
+ });
23107
+ },
23108
+ onServerCrash: (server) => {
23109
+ const timer = flushTimers.get(server);
23110
+ if (timer) {
23111
+ clearTimeout(timer);
23112
+ flushTimers.delete(server);
23113
+ }
23114
+ emitBlock(server, detectors.get(server)?.flush() ?? null);
23115
+ capture({
23116
+ server,
23117
+ kind: "server-crash",
23118
+ message: "MCP server connection closed unexpectedly",
23119
+ fingerprintKey: `crash:${server}`
23120
+ });
23121
+ },
23122
+ onServerStderr: (server, line) => {
23123
+ let detector = detectors.get(server);
23124
+ if (!detector) {
23125
+ detector = new ErrorLineDetector();
23126
+ detectors.set(server, detector);
23127
+ }
23128
+ for (const block of detector.feed("stderr", line)) emitBlock(server, block);
23129
+ const existing = flushTimers.get(server);
23130
+ if (existing) {
23131
+ clearTimeout(existing);
23132
+ flushTimers.delete(server);
23133
+ }
23134
+ if (detector.isCollecting) {
23135
+ const target = detector;
23136
+ flushTimers.set(server, setTimeout(() => {
23137
+ flushTimers.delete(server);
23138
+ emitBlock(server, target.flush());
23139
+ }, MCP_STDERR_FLUSH_DEBOUNCE_MS));
23140
+ }
23141
+ },
23142
+ dispose: () => {
23143
+ for (const timer of flushTimers.values()) clearTimeout(timer);
23144
+ flushTimers.clear();
23145
+ }
23146
+ };
23147
+ }
23148
+ //#endregion
22973
23149
  //#region src/daemon.ts
22974
23150
  /**
22975
23151
  * Alfe Gateway Daemon — main entry point.
@@ -23354,9 +23530,11 @@ async function startDaemon() {
23354
23530
  });
23355
23531
  const mcpManager = new Manager({ logger: logger$1 });
23356
23532
  mcpManagerRef = mcpManager;
23533
+ const mcpErrorHooks = createMcpErrorHooks();
23357
23534
  mcpBundler = new McpBundler({
23358
23535
  logger: logger$1,
23359
- idleTtlMs: 0
23536
+ idleTtlMs: 0,
23537
+ ...mcpErrorHooks
23360
23538
  }, { connect: defaultConnect });
23361
23539
  if (config.runtime === "hermes") logger$1.info("Hermes runtime — daemon MCP bundler left idle (store mirrored to config.yaml by HermesMcpSync)");
23362
23540
  else {
@@ -23483,6 +23661,7 @@ async function startDaemon() {
23483
23661
  mcpBundler = null;
23484
23662
  logger$1.debug("MCP bundler stopped");
23485
23663
  }
23664
+ mcpErrorHooks.dispose();
23486
23665
  logger$1.debug("Stopping MCP bundler manager...");
23487
23666
  await mcpManager.dispose();
23488
23667
  mcpManagerRef = null;
@@ -24207,4 +24386,4 @@ function formatDuration(ms) {
24207
24386
  return `${String(Math.round(seconds / 3600))}h`;
24208
24387
  }
24209
24388
  //#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 };
24389
+ 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.6.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.3.0",
31
+ "@alfe.ai/mcp-bundler": "^0.3.0"
32
32
  },
33
33
  "license": "UNLICENSED",
34
34
  "homepage": "https://alfe.ai",