@alfe.ai/gateway 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/health.js CHANGED
@@ -493,6 +493,31 @@ var AuthService = class {
493
493
  getMyPermissions() {
494
494
  return this.client.request(`${this.prefix}/me/permissions`);
495
495
  }
496
+ /**
497
+ * Preview what deleting the caller's own account will do. Drives the Danger
498
+ * Zone copy so the user sees an honest, kind-appropriate warning before the
499
+ * irreversible action (a last-admin cascade wipes the whole workspace).
500
+ */
501
+ getDeletionPreview() {
502
+ return this.client.request(`${this.prefix}/me/deletion-preview`);
503
+ }
504
+ /**
505
+ * Permanently delete the caller's own account. Returns `{ status: "deleting" }`
506
+ * and tears the account down asynchronously — the Clerk user is deleted shortly
507
+ * after, so the caller's session dies. On success, sign out and route the user
508
+ * to a terminal "account is being deleted" state; do NOT retry.
509
+ *
510
+ * Pass `confirmKind` (the accountKind the user actually confirmed, from the
511
+ * deletion preview): if membership changed between preview and execute, the
512
+ * server answers 409 `account_kind_changed` instead of cascading a scope the
513
+ * user never agreed to.
514
+ */
515
+ deleteAccount(confirmKind) {
516
+ return this.client.request(`${this.prefix}/me`, {
517
+ method: "DELETE",
518
+ ...confirmKind ? { body: JSON.stringify({ confirmKind }) } : {}
519
+ });
520
+ }
496
521
  getSubscriptionStatus() {
497
522
  return this.client.request(`${this.prefix}/subscription`);
498
523
  }
@@ -511,6 +536,15 @@ var AuthService = class {
511
536
  body: JSON.stringify(input)
512
537
  });
513
538
  }
539
+ /**
540
+ * Accept an approved Startup Program invite. Call after the applicant has a
541
+ * card on file (confirmed or freshly added) — this mints the Professional
542
+ * grant subscription. Idempotent: a repeat call on an already-accepted
543
+ * application returns `{ status: "accepted" }` without re-charging.
544
+ */
545
+ acceptStartup() {
546
+ return this.client.request(`${this.prefix}/subscription/accept-startup`, { method: "POST" });
547
+ }
514
548
  createCheckoutSession(input) {
515
549
  return this.client.request(`${this.prefix}/subscription/checkout`, {
516
550
  method: "POST",
@@ -4459,6 +4493,7 @@ enumValues({
4459
4493
  enumValues({
4460
4494
  Pending: "pending",
4461
4495
  Approved: "approved",
4496
+ Accepted: "accepted",
4462
4497
  Denied: "denied"
4463
4498
  });
4464
4499
  enumValues({
@@ -5245,6 +5280,18 @@ function createReconciliationReport(results) {
5245
5280
  };
5246
5281
  }
5247
5282
  /**
5283
+ * Create a config-only RECONCILIATION_REPORT (empty `results`) carrying just the
5284
+ * config-reconcile outcome. Sent immediately after the config pass so the
5285
+ * dashboard config chip updates without waiting for the integration passes.
5286
+ */
5287
+ function createConfigReport(config) {
5288
+ return {
5289
+ type: "RECONCILIATION_REPORT",
5290
+ results: [],
5291
+ config
5292
+ };
5293
+ }
5294
+ /**
5248
5295
  * Type guard: is this a cloud PING message?
5249
5296
  */
5250
5297
  function isCloudPing(msg) {
@@ -5266,7 +5313,7 @@ function isIPCResponse(msg) {
5266
5313
  const PROTOCOL_VERSION = 1;
5267
5314
  //#endregion
5268
5315
  //#region src/runtime-gate.ts
5269
- const log$4 = logger$1.child({ component: "RuntimeGate" });
5316
+ const log$5 = logger$1.child({ component: "RuntimeGate" });
5270
5317
  var RuntimeGate = class {
5271
5318
  depth = 0;
5272
5319
  wasRunning = false;
@@ -5284,7 +5331,7 @@ var RuntimeGate = class {
5284
5331
  const rp = this.getRuntime();
5285
5332
  this.wasRunning = rp?.isRunning ?? false;
5286
5333
  if (rp && this.wasRunning) {
5287
- log$4.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5334
+ log$5.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5288
5335
  await rp.stop();
5289
5336
  }
5290
5337
  }
@@ -5299,7 +5346,7 @@ var RuntimeGate = class {
5299
5346
  if (this.depth < 0) this.depth = 0;
5300
5347
  const rp = this.getRuntime();
5301
5348
  if (rp && this.wasRunning) {
5302
- log$4.info("Resuming runtime after mutating reconcile");
5349
+ log$5.info("Resuming runtime after mutating reconcile");
5303
5350
  rp.resume();
5304
5351
  }
5305
5352
  this.wasRunning = false;
@@ -5314,12 +5361,12 @@ var RuntimeGate = class {
5314
5361
  */
5315
5362
  requestRestart() {
5316
5363
  if (this.depth > 0) {
5317
- log$4.info("Runtime restart requested while suspended — deferring to pending resume");
5364
+ log$5.info("Runtime restart requested while suspended — deferring to pending resume");
5318
5365
  return;
5319
5366
  }
5320
5367
  const rp = this.getRuntime();
5321
5368
  if (rp) rp.restart().catch((err) => {
5322
- log$4.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5369
+ log$5.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5323
5370
  });
5324
5371
  }
5325
5372
  };
@@ -5376,11 +5423,13 @@ const CLI_SENTRY_DSN = "https://82dde4631336561f2fcc89d7531623de@o45110082394521
5376
5423
  const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
5377
5424
  /**
5378
5425
  * 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.
5426
+ * bundler (tool errors, child crashes, stderr output). Dedicated `local-mcp`
5427
+ * project for the locally-installed MCP surface: the cloud MCP Fly service
5428
+ * keeps the `mcp` project (`SENTRY_DSNS.mcp`), so planned daemon restarts
5429
+ * tearing down MCP children never pollute the remote service's signal.
5430
+ * The `source: agent-daemon` + `agentId` tags are kept for continuity.
5382
5431
  */
5383
- const AGENT_MCP_SENTRY_DSN = "https://638309eac96146a945f46b578f10c806@o4511008239452160.ingest.us.sentry.io/4511146364174336";
5432
+ const AGENT_MCP_SENTRY_DSN = "https://b4c4b4e19135692206bacabcd12a7fd3@o4511008239452160.ingest.us.sentry.io/4511697094377472";
5384
5433
  /** Surface → Sentry project: `cli` and `agent-daemon` respectively. */
5385
5434
  const SURFACE_DSNS = {
5386
5435
  cli: CLI_SENTRY_DSN,
@@ -5721,7 +5770,7 @@ function captureFatal(cause) {
5721
5770
  }
5722
5771
  //#endregion
5723
5772
  //#region src/reconciliation.ts
5724
- const log$3 = logger$1.child({ component: "Reconciliation" });
5773
+ const log$4 = logger$1.child({ component: "Reconciliation" });
5725
5774
  /**
5726
5775
  * How many times reconcile will re-attempt activation of an intact integration
5727
5776
  * stuck in `error` (with no reinstall requested) before giving up and waiting
@@ -5791,7 +5840,7 @@ var ReconciliationEngine = class {
5791
5840
  try {
5792
5841
  localIntegrations = await this.manager.getInstalledIntegrations();
5793
5842
  } catch (err) {
5794
- log$3.error({ err }, "Failed to get local integrations");
5843
+ log$4.error({ err }, "Failed to get local integrations");
5795
5844
  localIntegrations = [];
5796
5845
  }
5797
5846
  const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
@@ -5810,10 +5859,10 @@ var ReconciliationEngine = class {
5810
5859
  try {
5811
5860
  if (!local) {
5812
5861
  await this.ensureSuspended();
5813
- log$3.info(`Installing ${id}@${desired.version}`);
5862
+ log$4.info(`Installing ${id}@${desired.version}`);
5814
5863
  await this.manager.install(id, desired.version, desired.config, desired.customSource);
5815
5864
  report.installed.push(id);
5816
- log$3.info(`Activating ${id}`);
5865
+ log$4.info(`Activating ${id}`);
5817
5866
  if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5818
5867
  report.activated.push(id);
5819
5868
  report.results.push({
@@ -5826,7 +5875,7 @@ var ReconciliationEngine = class {
5826
5875
  if (local.version !== desired.version && desired.version !== "" && local.version !== "unknown") {
5827
5876
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5828
5877
  await this.ensureSuspended();
5829
- log$3.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
5878
+ log$4.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
5830
5879
  this.manager.resetReinstallAttempts(id);
5831
5880
  this.activateAttempts.delete(id);
5832
5881
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5839,7 +5888,7 @@ var ReconciliationEngine = class {
5839
5888
  });
5840
5889
  return;
5841
5890
  }
5842
- log$3.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
5891
+ log$4.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
5843
5892
  try {
5844
5893
  if ((await this.manager.upgrade(id, desired.version, desired.config, desired.customSource, { onBeforeRuntimeMutation: () => this.ensureSuspended() })).configApplied) report.configApplied = true;
5845
5894
  this.manager.resetReinstallAttempts(id);
@@ -5853,7 +5902,7 @@ var ReconciliationEngine = class {
5853
5902
  });
5854
5903
  } catch (upgradeErr) {
5855
5904
  const upgradeMsg = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
5856
- log$3.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
5905
+ log$4.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
5857
5906
  captureIntegrationFailure(id, "upgrade", upgradeErr);
5858
5907
  report.errors.push({
5859
5908
  integrationId: id,
@@ -5871,7 +5920,7 @@ var ReconciliationEngine = class {
5871
5920
  if (local.status === "error") {
5872
5921
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5873
5922
  await this.ensureSuspended();
5874
- log$3.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5923
+ log$4.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5875
5924
  this.manager.resetReinstallAttempts(id);
5876
5925
  this.activateAttempts.delete(id);
5877
5926
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5887,7 +5936,7 @@ var ReconciliationEngine = class {
5887
5936
  if (!await this.manager.isInstallIntact(id)) {
5888
5937
  const attempts = this.manager.getReinstallAttempts(id);
5889
5938
  if (attempts >= 3) {
5890
- log$3.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5939
+ log$4.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5891
5940
  captureIntegrationFailure(id, "reinstall", `Max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`);
5892
5941
  report.errors.push({
5893
5942
  integrationId: id,
@@ -5902,7 +5951,7 @@ var ReconciliationEngine = class {
5902
5951
  return;
5903
5952
  }
5904
5953
  await this.ensureSuspended();
5905
- log$3.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5954
+ log$4.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5906
5955
  this.manager.incrementReinstallAttempts(id);
5907
5956
  try {
5908
5957
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5917,7 +5966,7 @@ var ReconciliationEngine = class {
5917
5966
  });
5918
5967
  } catch (reinstallErr) {
5919
5968
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
5920
- log$3.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5969
+ log$4.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5921
5970
  captureIntegrationFailure(id, "reinstall", reinstallErr);
5922
5971
  report.errors.push({
5923
5972
  integrationId: id,
@@ -5936,7 +5985,7 @@ var ReconciliationEngine = class {
5936
5985
  if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
5937
5986
  this.activateAttempts.set(id, activateAttempts + 1);
5938
5987
  await this.ensureSuspended();
5939
- log$3.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5988
+ log$4.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5940
5989
  try {
5941
5990
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
5942
5991
  this.activateAttempts.delete(id);
@@ -5949,7 +5998,7 @@ var ReconciliationEngine = class {
5949
5998
  return;
5950
5999
  } catch (reactivateErr) {
5951
6000
  const reactivateMsg = reactivateErr instanceof Error ? reactivateErr.message : String(reactivateErr);
5952
- log$3.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
6001
+ log$4.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
5953
6002
  captureIntegrationFailure(id, "reactivate", reactivateErr);
5954
6003
  report.errors.push({
5955
6004
  integrationId: id,
@@ -5964,7 +6013,7 @@ var ReconciliationEngine = class {
5964
6013
  return;
5965
6014
  }
5966
6015
  }
5967
- log$3.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
6016
+ log$4.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5968
6017
  captureIntegrationFailure(id, "reactivate", `Integration ${id} stuck in error state — re-activation attempts exhausted`);
5969
6018
  report.errors.push({
5970
6019
  integrationId: id,
@@ -5981,7 +6030,7 @@ var ReconciliationEngine = class {
5981
6030
  if (local.status === "installing") {
5982
6031
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5983
6032
  await this.ensureSuspended();
5984
- log$3.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6033
+ log$4.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5985
6034
  this.manager.resetReinstallAttempts(id);
5986
6035
  this.activateAttempts.delete(id);
5987
6036
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5997,7 +6046,7 @@ var ReconciliationEngine = class {
5997
6046
  const attempts = this.manager.getReinstallAttempts(id);
5998
6047
  if (attempts >= 3) {
5999
6048
  const errorMessage = `Stale installing state — max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`;
6000
- log$3.error(`Auto-reinstall blocked for ${id} — stuck in "installing", ${String(attempts)} consecutive failures. Manual reinstall required.`);
6049
+ log$4.error(`Auto-reinstall blocked for ${id} — stuck in "installing", ${String(attempts)} consecutive failures. Manual reinstall required.`);
6001
6050
  captureIntegrationFailure(id, "reinstall", errorMessage);
6002
6051
  report.errors.push({
6003
6052
  integrationId: id,
@@ -6012,7 +6061,7 @@ var ReconciliationEngine = class {
6012
6061
  return;
6013
6062
  }
6014
6063
  await this.ensureSuspended();
6015
- log$3.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
6064
+ log$4.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
6016
6065
  this.manager.incrementReinstallAttempts(id);
6017
6066
  try {
6018
6067
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6027,7 +6076,7 @@ var ReconciliationEngine = class {
6027
6076
  });
6028
6077
  } catch (reinstallErr) {
6029
6078
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
6030
- log$3.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
6079
+ log$4.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
6031
6080
  captureIntegrationFailure(id, "reinstall", reinstallErr);
6032
6081
  report.errors.push({
6033
6082
  integrationId: id,
@@ -6044,7 +6093,7 @@ var ReconciliationEngine = class {
6044
6093
  }
6045
6094
  if (local.status !== "active") {
6046
6095
  await this.ensureSuspended();
6047
- log$3.info(`Activating ${id}`);
6096
+ log$4.info(`Activating ${id}`);
6048
6097
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
6049
6098
  report.activated.push(id);
6050
6099
  report.results.push({
@@ -6056,7 +6105,7 @@ var ReconciliationEngine = class {
6056
6105
  }
6057
6106
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6058
6107
  await this.ensureSuspended();
6059
- log$3.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6108
+ log$4.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6060
6109
  this.manager.resetReinstallAttempts(id);
6061
6110
  this.activateAttempts.delete(id);
6062
6111
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6077,7 +6126,7 @@ var ReconciliationEngine = class {
6077
6126
  });
6078
6127
  } catch (err) {
6079
6128
  const message = err instanceof Error ? err.message : String(err);
6080
- log$3.error({ err }, `Error reconciling ${id}`);
6129
+ log$4.error({ err }, `Error reconciling ${id}`);
6081
6130
  captureIntegrationFailure(id, "reconcile_active", err);
6082
6131
  report.errors.push({
6083
6132
  integrationId: id,
@@ -6102,7 +6151,7 @@ var ReconciliationEngine = class {
6102
6151
  }
6103
6152
  try {
6104
6153
  await this.ensureSuspended();
6105
- log$3.info(`Removing ${id}`);
6154
+ log$4.info(`Removing ${id}`);
6106
6155
  if (local.status === "active") {
6107
6156
  await this.manager.deactivate(id);
6108
6157
  report.deactivated.push(id);
@@ -6116,7 +6165,7 @@ var ReconciliationEngine = class {
6116
6165
  });
6117
6166
  } catch (err) {
6118
6167
  const message = err instanceof Error ? err.message : String(err);
6119
- log$3.error({ err }, `Error removing ${id}`);
6168
+ log$4.error({ err }, `Error removing ${id}`);
6120
6169
  captureIntegrationFailure(id, "reconcile_removed", err);
6121
6170
  report.errors.push({
6122
6171
  integrationId: id,
@@ -6154,6 +6203,7 @@ var CloudClient = class {
6154
6203
  onRuntimeRestartNeeded = null;
6155
6204
  onReconciliationComplete = null;
6156
6205
  reconciliationEngine = null;
6206
+ configReconciler = null;
6157
6207
  reconciling = false;
6158
6208
  pendingDesiredState = null;
6159
6209
  constructor(config) {
@@ -6202,6 +6252,14 @@ var CloudClient = class {
6202
6252
  this.reconciliationEngine = new ReconciliationEngine(manager, gate);
6203
6253
  }
6204
6254
  /**
6255
+ * Set the config reconciler for the DESIRED_STATE `config` block (plan A4).
6256
+ * When set, a DESIRED_STATE carrying `config` runs a config-reconcile pass
6257
+ * BEFORE the integrations loop and reports config status immediately.
6258
+ */
6259
+ setConfigReconciler(reconciler) {
6260
+ this.configReconciler = reconciler;
6261
+ }
6262
+ /**
6205
6263
  * Start the cloud connection with auto-reconnect.
6206
6264
  */
6207
6265
  start() {
@@ -6424,6 +6482,19 @@ var CloudClient = class {
6424
6482
  }
6425
6483
  }
6426
6484
  async runReconciliation(msg) {
6485
+ if (msg.config && this.configReconciler) try {
6486
+ const configReport = await this.configReconciler.reconcile(msg.config);
6487
+ if (configReport) {
6488
+ this.send(createConfigReport(configReport));
6489
+ logger$1.info({
6490
+ version: configReport.appliedVersion,
6491
+ status: configReport.status
6492
+ }, "Cloud: sent config reconcile report");
6493
+ }
6494
+ } catch (err) {
6495
+ const message = err instanceof Error ? err.message : String(err);
6496
+ logger$1.error({ err: message }, "Cloud: config reconcile failed");
6497
+ }
6427
6498
  if (!this.reconciliationEngine) return;
6428
6499
  const report = await this.reconciliationEngine.reconcile(msg.integrations);
6429
6500
  logger$1.info({
@@ -6476,6 +6547,86 @@ var CloudClient = class {
6476
6547
  }
6477
6548
  };
6478
6549
  //#endregion
6550
+ //#region src/config-reconciler.ts
6551
+ const log$3 = logger$1.child({ component: "ConfigReconciler" });
6552
+ const DEFAULT_VERIFY_RETRIES = 3;
6553
+ const DEFAULT_VERIFY_RETRY_DELAY_MS = 750;
6554
+ const delay = (ms) => new Promise((resolve) => {
6555
+ setTimeout(resolve, ms);
6556
+ });
6557
+ var ConfigReconciler = class {
6558
+ getApplier;
6559
+ verifyRetries;
6560
+ verifyRetryDelayMs;
6561
+ constructor(options) {
6562
+ this.getApplier = options.getApplier;
6563
+ this.verifyRetries = options.verifyRetries ?? DEFAULT_VERIFY_RETRIES;
6564
+ this.verifyRetryDelayMs = options.verifyRetryDelayMs ?? DEFAULT_VERIFY_RETRY_DELAY_MS;
6565
+ }
6566
+ /**
6567
+ * Reconcile the desired config block. Returns the outcome to report, or `null`
6568
+ * when there is nothing to report (no applier able to apply config).
6569
+ */
6570
+ async reconcile(desired) {
6571
+ const applier = this.getApplier();
6572
+ if (!applier?.setConfigRaw) {
6573
+ log$3.debug("No applier with setConfigRaw — skipping config reconcile");
6574
+ return null;
6575
+ }
6576
+ const setConfigRaw = applier.setConfigRaw.bind(applier);
6577
+ const getConfigRaw = applier.getConfigRaw?.bind(applier);
6578
+ const entries = Object.entries(desired.values).filter((e) => e[1] !== null);
6579
+ if (entries.length === 0) return {
6580
+ appliedVersion: desired.version,
6581
+ status: "applied"
6582
+ };
6583
+ const failures = [];
6584
+ for (const [key, want] of entries) try {
6585
+ if (getConfigRaw) {
6586
+ if (await getConfigRaw(key) === want) {
6587
+ log$3.debug({ key }, "Config already at desired value — no-op");
6588
+ continue;
6589
+ }
6590
+ }
6591
+ await setConfigRaw(key, want);
6592
+ if (getConfigRaw) {
6593
+ if (!await this.verify(getConfigRaw, key, want)) failures.push(`${key}: verify failed (read-back did not match)`);
6594
+ }
6595
+ } catch (err) {
6596
+ const msg = err instanceof Error ? err.message : String(err);
6597
+ failures.push(`${key}: ${msg}`);
6598
+ }
6599
+ if (failures.length > 0) {
6600
+ const joined = failures.join("; ");
6601
+ const reason = joined.length > 480 ? `${joined.slice(0, 480)}… (truncated)` : joined;
6602
+ log$3.warn({
6603
+ version: desired.version,
6604
+ reason
6605
+ }, "Config reconcile failed");
6606
+ return {
6607
+ appliedVersion: desired.version,
6608
+ status: "failed",
6609
+ reason
6610
+ };
6611
+ }
6612
+ log$3.info({
6613
+ version: desired.version,
6614
+ keys: entries.length
6615
+ }, "Config reconcile applied");
6616
+ return {
6617
+ appliedVersion: desired.version,
6618
+ status: "applied"
6619
+ };
6620
+ }
6621
+ async verify(getConfigRaw, key, want) {
6622
+ for (let attempt = 0; attempt <= this.verifyRetries; attempt++) {
6623
+ if (await getConfigRaw(key) === want) return true;
6624
+ if (attempt < this.verifyRetries && this.verifyRetryDelayMs > 0) await delay(this.verifyRetryDelayMs * 2 ** attempt);
6625
+ }
6626
+ return false;
6627
+ }
6628
+ };
6629
+ //#endregion
6479
6630
  //#region src/ipc-server.ts
6480
6631
  /**
6481
6632
  * IPC Server — Unix socket server for local plugin connections.
@@ -6823,12 +6974,12 @@ var IPCServer = class {
6823
6974
  };
6824
6975
  //#endregion
6825
6976
  //#region src/command-queue.ts
6826
- const DEFAULT_TTL_MS = 300 * 1e3;
6977
+ const DEFAULT_TTL_MS$1 = 300 * 1e3;
6827
6978
  var CommandQueue = class {
6828
6979
  queues = /* @__PURE__ */ new Map();
6829
6980
  ttlMs;
6830
6981
  gcTimer = null;
6831
- constructor(ttlMs = DEFAULT_TTL_MS) {
6982
+ constructor(ttlMs = DEFAULT_TTL_MS$1) {
6832
6983
  this.ttlMs = ttlMs;
6833
6984
  }
6834
6985
  /**
@@ -7025,6 +7176,12 @@ Type=simple
7025
7176
  ExecStart=${alfeBin} gateway daemon
7026
7177
  Restart=always
7027
7178
  RestartSec=10
7179
+ # SIGTERM only the daemon on stop/restart; it closes its MCP/runtime children
7180
+ # itself. The default (control-group) SIGTERMs the children simultaneously, so
7181
+ # they die before the daemon's orderly dispose reaches them and every planned
7182
+ # restart reports MCP "server-crash" noise to Sentry. Stragglers still get
7183
+ # SIGKILL when the stop timeout expires.
7184
+ KillMode=mixed
7028
7185
  Environment=NODE_ENV=production${root ? "\nEnvironment=HOME=/root\nWorkingDirectory=/root" : ""}
7029
7186
  ${envLines}
7030
7187
 
@@ -22838,6 +22995,73 @@ var CommandRegistry = class {
22838
22995
  }
22839
22996
  };
22840
22997
  //#endregion
22998
+ //#region src/command-dedupe.ts
22999
+ const DEFAULT_MAX_ENTRIES = 200;
23000
+ const DEFAULT_TTL_MS = 600 * 1e3;
23001
+ var CommandDedupe = class {
23002
+ entries = /* @__PURE__ */ new Map();
23003
+ maxEntries;
23004
+ ttlMs;
23005
+ now;
23006
+ constructor(opts = {}) {
23007
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
23008
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
23009
+ this.now = opts.now ?? (() => Date.now());
23010
+ }
23011
+ /**
23012
+ * Run `exec` for `commandId` exactly once within the dedupe window. A repeat
23013
+ * call with the same id (while still remembered) returns the FIRST call's
23014
+ * promise — either the in-flight execution or the settled replayable ACK — so
23015
+ * the command body never runs twice.
23016
+ */
23017
+ async run(commandId, exec) {
23018
+ this.evictExpired();
23019
+ const existing = this.entries.get(commandId);
23020
+ if (existing) return {
23021
+ ack: await existing.promise,
23022
+ duplicate: true
23023
+ };
23024
+ const promise = exec();
23025
+ this.entries.set(commandId, {
23026
+ insertedAt: this.now(),
23027
+ promise
23028
+ });
23029
+ this.evictOverCap();
23030
+ promise.catch(() => {
23031
+ this.entries.delete(commandId);
23032
+ });
23033
+ return {
23034
+ ack: await promise,
23035
+ duplicate: false
23036
+ };
23037
+ }
23038
+ /** Test/inspection helper. */
23039
+ size() {
23040
+ this.evictExpired();
23041
+ return this.entries.size;
23042
+ }
23043
+ has(commandId) {
23044
+ const e = this.entries.get(commandId);
23045
+ if (!e) return false;
23046
+ if (this.now() - e.insertedAt > this.ttlMs) {
23047
+ this.entries.delete(commandId);
23048
+ return false;
23049
+ }
23050
+ return true;
23051
+ }
23052
+ evictExpired() {
23053
+ const cutoff = this.now() - this.ttlMs;
23054
+ for (const [id, entry] of this.entries) if (entry.insertedAt <= cutoff) this.entries.delete(id);
23055
+ }
23056
+ evictOverCap() {
23057
+ while (this.entries.size > this.maxEntries) {
23058
+ const oldest = this.entries.keys().next().value;
23059
+ if (oldest === void 0) break;
23060
+ this.entries.delete(oldest);
23061
+ }
23062
+ }
23063
+ };
23064
+ //#endregion
22841
23065
  //#region src/pairing-approval.ts
22842
23066
  /**
22843
23067
  * Pairing approval poller — auto-approves pending OpenClaw scope-upgrade
@@ -23134,7 +23358,15 @@ function createMcpErrorHooks() {
23134
23358
  const throttleFor = (kind) => kind === "tool-result-error" ? resultErrorThrottle : kind === "server-crash" ? crashThrottle : errorThrottle;
23135
23359
  const detectors = /* @__PURE__ */ new Map();
23136
23360
  const flushTimers = /* @__PURE__ */ new Map();
23361
+ let shuttingDown = false;
23137
23362
  const capture = (opts) => {
23363
+ if (shuttingDown) {
23364
+ log.debug({
23365
+ server: opts.server,
23366
+ kind: opts.kind
23367
+ }, "MCP failure capture suppressed — daemon shutting down");
23368
+ return;
23369
+ }
23138
23370
  const { allow, suppressedCount } = throttleFor(opts.kind).allowErrorCapture(opts.fingerprintKey);
23139
23371
  if (!allow) {
23140
23372
  log.debug({
@@ -23206,6 +23438,9 @@ function createMcpErrorHooks() {
23206
23438
  dispose: () => {
23207
23439
  for (const timer of flushTimers.values()) clearTimeout(timer);
23208
23440
  flushTimers.clear();
23441
+ },
23442
+ beginShutdown: () => {
23443
+ shuttingDown = true;
23209
23444
  }
23210
23445
  };
23211
23446
  }
@@ -23245,6 +23480,13 @@ let runtimeProcess = null;
23245
23480
  let aiProxyUrl = null;
23246
23481
  let aiProxyRunning = false;
23247
23482
  let cloudConnected = false;
23483
+ /**
23484
+ * Dedupe for at-least-once durable command delivery (Phase B). The cloud may
23485
+ * push a command over the live WS AND re-drain the same commandId on the next
23486
+ * SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
23487
+ * so a duplicate is ACKed with the prior result without re-executing.
23488
+ */
23489
+ const commandDedupe = new CommandDedupe();
23248
23490
  let shuttingDown = false;
23249
23491
  let commandRegistry;
23250
23492
  let resolvedCliVersion;
@@ -23641,6 +23883,7 @@ async function startDaemon() {
23641
23883
  const integrationAdapter = new IntegrationManagerAdapter(integrationManager);
23642
23884
  const runtimeGate = new RuntimeGate(() => runtimeProcess);
23643
23885
  cloudClient.setIntegrationManager(integrationAdapter, runtimeGate);
23886
+ cloudClient.setConfigReconciler(new ConfigReconciler({ getApplier: () => runtimeAppliers.get(config.runtime) }));
23644
23887
  const requestRuntimeRestart = () => {
23645
23888
  logger$1.info("Runtime state changed — requesting runtime restart");
23646
23889
  runtimeGate.requestRestart();
@@ -23705,6 +23948,7 @@ async function startDaemon() {
23705
23948
  const shutdown = async (signal) => {
23706
23949
  if (shuttingDown) return;
23707
23950
  shuttingDown = true;
23951
+ mcpErrorHooks.beginShutdown();
23708
23952
  logger$1.info({ signal }, "Shutting down...");
23709
23953
  if (stopPairingApprovalPoller) {
23710
23954
  stopPairingApprovalPoller();
@@ -23767,7 +24011,34 @@ async function startDaemon() {
23767
24011
  agentId: config.agentId
23768
24012
  }, "Alfe Gateway Daemon started ✅");
23769
24013
  }
24014
+ /**
24015
+ * Single command choke point. Dedupes at-least-once durable deliveries
24016
+ * (Phase B) — a repeat commandId within the dedupe window is ACKed with the
24017
+ * prior outcome (tagged `duplicate: true`) and NOT re-executed.
24018
+ *
24019
+ * Non-idempotent, replay-ordering-hazardous commands (daemon.update /
24020
+ * daemon.restart / runtime.restart) benefit most: a re-drained restart must not
24021
+ * bounce the box a second time.
24022
+ */
23770
24023
  async function handleCloudCommand(command) {
24024
+ const { ack, duplicate } = await commandDedupe.run(command.commandId, () => executeCloudCommand(command));
24025
+ if (duplicate) {
24026
+ logger$1.info({
24027
+ commandId: command.commandId,
24028
+ command: command.command
24029
+ }, "Duplicate command — replaying prior ACK without re-executing");
24030
+ const priorResult = ack.result && typeof ack.result === "object" && !Array.isArray(ack.result) ? ack.result : { result: ack.result };
24031
+ return {
24032
+ ...ack,
24033
+ result: {
24034
+ ...priorResult,
24035
+ duplicate: true
24036
+ }
24037
+ };
24038
+ }
24039
+ return ack;
24040
+ }
24041
+ async function executeCloudCommand(command) {
23771
24042
  if (command.command === "daemon.update") {
23772
24043
  const version = command.payload?.version ?? "latest";
23773
24044
  setTimeout(() => {
@@ -28,6 +28,15 @@ declare const PROTOCOL_VERSION = 1;
28
28
  //#region src/daemon.d.ts
29
29
 
30
30
  declare function startDaemon(): Promise<void>;
31
+ /**
32
+ * Single command choke point. Dedupes at-least-once durable deliveries
33
+ * (Phase B) — a repeat commandId within the dedupe window is ACKed with the
34
+ * prior outcome (tagged `duplicate: true`) and NOT re-executed.
35
+ *
36
+ * Non-idempotent, replay-ordering-hazardous commands (daemon.update /
37
+ * daemon.restart / runtime.restart) benefit most: a re-drained restart must not
38
+ * bounce the box a second time.
39
+ */
31
40
  //#endregion
32
41
  //#region src/openclaw-version.d.ts
33
42
  /**
@@ -83,11 +92,13 @@ declare const AGENT_DAEMON_SENTRY_DSN = "https://a47f010d8d39fb4350f913492189f28
83
92
  declare const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
84
93
  /**
85
94
  * 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.
95
+ * bundler (tool errors, child crashes, stderr output). Dedicated `local-mcp`
96
+ * project for the locally-installed MCP surface: the cloud MCP Fly service
97
+ * keeps the `mcp` project (`SENTRY_DSNS.mcp`), so planned daemon restarts
98
+ * tearing down MCP children never pollute the remote service's signal.
99
+ * The `source: agent-daemon` + `agentId` tags are kept for continuity.
89
100
  */
90
- declare const AGENT_MCP_SENTRY_DSN = "https://638309eac96146a945f46b578f10c806@o4511008239452160.ingest.us.sentry.io/4511146364174336";
101
+ declare const AGENT_MCP_SENTRY_DSN = "https://b4c4b4e19135692206bacabcd12a7fd3@o4511008239452160.ingest.us.sentry.io/4511697094377472";
91
102
  /** Which surface initialised Sentry — tagged on every event. */
92
103
  type SentrySurface = "cli" | "daemon";
93
104
  interface InitAgentSentryOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
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.3.2",
30
+ "@alfe.ai/integrations": "^0.4.0",
31
31
  "@alfe.ai/mcp-bundler": "^0.3.0"
32
32
  },
33
33
  "license": "UNLICENSED",