@alfe.ai/gateway 0.6.2 → 0.7.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.
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
  }
@@ -5245,6 +5270,18 @@ function createReconciliationReport(results) {
5245
5270
  };
5246
5271
  }
5247
5272
  /**
5273
+ * Create a config-only RECONCILIATION_REPORT (empty `results`) carrying just the
5274
+ * config-reconcile outcome. Sent immediately after the config pass so the
5275
+ * dashboard config chip updates without waiting for the integration passes.
5276
+ */
5277
+ function createConfigReport(config) {
5278
+ return {
5279
+ type: "RECONCILIATION_REPORT",
5280
+ results: [],
5281
+ config
5282
+ };
5283
+ }
5284
+ /**
5248
5285
  * Type guard: is this a cloud PING message?
5249
5286
  */
5250
5287
  function isCloudPing(msg) {
@@ -5266,7 +5303,7 @@ function isIPCResponse(msg) {
5266
5303
  const PROTOCOL_VERSION = 1;
5267
5304
  //#endregion
5268
5305
  //#region src/runtime-gate.ts
5269
- const log$4 = logger$1.child({ component: "RuntimeGate" });
5306
+ const log$5 = logger$1.child({ component: "RuntimeGate" });
5270
5307
  var RuntimeGate = class {
5271
5308
  depth = 0;
5272
5309
  wasRunning = false;
@@ -5284,7 +5321,7 @@ var RuntimeGate = class {
5284
5321
  const rp = this.getRuntime();
5285
5322
  this.wasRunning = rp?.isRunning ?? false;
5286
5323
  if (rp && this.wasRunning) {
5287
- log$4.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5324
+ log$5.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5288
5325
  await rp.stop();
5289
5326
  }
5290
5327
  }
@@ -5299,7 +5336,7 @@ var RuntimeGate = class {
5299
5336
  if (this.depth < 0) this.depth = 0;
5300
5337
  const rp = this.getRuntime();
5301
5338
  if (rp && this.wasRunning) {
5302
- log$4.info("Resuming runtime after mutating reconcile");
5339
+ log$5.info("Resuming runtime after mutating reconcile");
5303
5340
  rp.resume();
5304
5341
  }
5305
5342
  this.wasRunning = false;
@@ -5314,12 +5351,12 @@ var RuntimeGate = class {
5314
5351
  */
5315
5352
  requestRestart() {
5316
5353
  if (this.depth > 0) {
5317
- log$4.info("Runtime restart requested while suspended — deferring to pending resume");
5354
+ log$5.info("Runtime restart requested while suspended — deferring to pending resume");
5318
5355
  return;
5319
5356
  }
5320
5357
  const rp = this.getRuntime();
5321
5358
  if (rp) rp.restart().catch((err) => {
5322
- log$4.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5359
+ log$5.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5323
5360
  });
5324
5361
  }
5325
5362
  };
@@ -5721,7 +5758,7 @@ function captureFatal(cause) {
5721
5758
  }
5722
5759
  //#endregion
5723
5760
  //#region src/reconciliation.ts
5724
- const log$3 = logger$1.child({ component: "Reconciliation" });
5761
+ const log$4 = logger$1.child({ component: "Reconciliation" });
5725
5762
  /**
5726
5763
  * How many times reconcile will re-attempt activation of an intact integration
5727
5764
  * stuck in `error` (with no reinstall requested) before giving up and waiting
@@ -5791,7 +5828,7 @@ var ReconciliationEngine = class {
5791
5828
  try {
5792
5829
  localIntegrations = await this.manager.getInstalledIntegrations();
5793
5830
  } catch (err) {
5794
- log$3.error({ err }, "Failed to get local integrations");
5831
+ log$4.error({ err }, "Failed to get local integrations");
5795
5832
  localIntegrations = [];
5796
5833
  }
5797
5834
  const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
@@ -5810,10 +5847,10 @@ var ReconciliationEngine = class {
5810
5847
  try {
5811
5848
  if (!local) {
5812
5849
  await this.ensureSuspended();
5813
- log$3.info(`Installing ${id}@${desired.version}`);
5850
+ log$4.info(`Installing ${id}@${desired.version}`);
5814
5851
  await this.manager.install(id, desired.version, desired.config, desired.customSource);
5815
5852
  report.installed.push(id);
5816
- log$3.info(`Activating ${id}`);
5853
+ log$4.info(`Activating ${id}`);
5817
5854
  if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5818
5855
  report.activated.push(id);
5819
5856
  report.results.push({
@@ -5826,7 +5863,7 @@ var ReconciliationEngine = class {
5826
5863
  if (local.version !== desired.version && desired.version !== "" && local.version !== "unknown") {
5827
5864
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5828
5865
  await this.ensureSuspended();
5829
- log$3.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
5866
+ log$4.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
5830
5867
  this.manager.resetReinstallAttempts(id);
5831
5868
  this.activateAttempts.delete(id);
5832
5869
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5839,7 +5876,7 @@ var ReconciliationEngine = class {
5839
5876
  });
5840
5877
  return;
5841
5878
  }
5842
- log$3.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
5879
+ log$4.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
5843
5880
  try {
5844
5881
  if ((await this.manager.upgrade(id, desired.version, desired.config, desired.customSource, { onBeforeRuntimeMutation: () => this.ensureSuspended() })).configApplied) report.configApplied = true;
5845
5882
  this.manager.resetReinstallAttempts(id);
@@ -5853,7 +5890,7 @@ var ReconciliationEngine = class {
5853
5890
  });
5854
5891
  } catch (upgradeErr) {
5855
5892
  const upgradeMsg = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
5856
- log$3.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
5893
+ log$4.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
5857
5894
  captureIntegrationFailure(id, "upgrade", upgradeErr);
5858
5895
  report.errors.push({
5859
5896
  integrationId: id,
@@ -5871,7 +5908,7 @@ var ReconciliationEngine = class {
5871
5908
  if (local.status === "error") {
5872
5909
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5873
5910
  await this.ensureSuspended();
5874
- log$3.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5911
+ log$4.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5875
5912
  this.manager.resetReinstallAttempts(id);
5876
5913
  this.activateAttempts.delete(id);
5877
5914
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5887,7 +5924,7 @@ var ReconciliationEngine = class {
5887
5924
  if (!await this.manager.isInstallIntact(id)) {
5888
5925
  const attempts = this.manager.getReinstallAttempts(id);
5889
5926
  if (attempts >= 3) {
5890
- log$3.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5927
+ log$4.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5891
5928
  captureIntegrationFailure(id, "reinstall", `Max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`);
5892
5929
  report.errors.push({
5893
5930
  integrationId: id,
@@ -5902,7 +5939,7 @@ var ReconciliationEngine = class {
5902
5939
  return;
5903
5940
  }
5904
5941
  await this.ensureSuspended();
5905
- log$3.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5942
+ log$4.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5906
5943
  this.manager.incrementReinstallAttempts(id);
5907
5944
  try {
5908
5945
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5917,7 +5954,7 @@ var ReconciliationEngine = class {
5917
5954
  });
5918
5955
  } catch (reinstallErr) {
5919
5956
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
5920
- log$3.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5957
+ log$4.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5921
5958
  captureIntegrationFailure(id, "reinstall", reinstallErr);
5922
5959
  report.errors.push({
5923
5960
  integrationId: id,
@@ -5936,7 +5973,7 @@ var ReconciliationEngine = class {
5936
5973
  if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
5937
5974
  this.activateAttempts.set(id, activateAttempts + 1);
5938
5975
  await this.ensureSuspended();
5939
- log$3.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5976
+ log$4.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5940
5977
  try {
5941
5978
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
5942
5979
  this.activateAttempts.delete(id);
@@ -5949,7 +5986,7 @@ var ReconciliationEngine = class {
5949
5986
  return;
5950
5987
  } catch (reactivateErr) {
5951
5988
  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}`);
5989
+ log$4.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
5953
5990
  captureIntegrationFailure(id, "reactivate", reactivateErr);
5954
5991
  report.errors.push({
5955
5992
  integrationId: id,
@@ -5964,7 +6001,7 @@ var ReconciliationEngine = class {
5964
6001
  return;
5965
6002
  }
5966
6003
  }
5967
- log$3.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
6004
+ log$4.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5968
6005
  captureIntegrationFailure(id, "reactivate", `Integration ${id} stuck in error state — re-activation attempts exhausted`);
5969
6006
  report.errors.push({
5970
6007
  integrationId: id,
@@ -5981,7 +6018,7 @@ var ReconciliationEngine = class {
5981
6018
  if (local.status === "installing") {
5982
6019
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5983
6020
  await this.ensureSuspended();
5984
- log$3.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6021
+ log$4.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5985
6022
  this.manager.resetReinstallAttempts(id);
5986
6023
  this.activateAttempts.delete(id);
5987
6024
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5997,7 +6034,7 @@ var ReconciliationEngine = class {
5997
6034
  const attempts = this.manager.getReinstallAttempts(id);
5998
6035
  if (attempts >= 3) {
5999
6036
  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.`);
6037
+ log$4.error(`Auto-reinstall blocked for ${id} — stuck in "installing", ${String(attempts)} consecutive failures. Manual reinstall required.`);
6001
6038
  captureIntegrationFailure(id, "reinstall", errorMessage);
6002
6039
  report.errors.push({
6003
6040
  integrationId: id,
@@ -6012,7 +6049,7 @@ var ReconciliationEngine = class {
6012
6049
  return;
6013
6050
  }
6014
6051
  await this.ensureSuspended();
6015
- log$3.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
6052
+ log$4.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
6016
6053
  this.manager.incrementReinstallAttempts(id);
6017
6054
  try {
6018
6055
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6027,7 +6064,7 @@ var ReconciliationEngine = class {
6027
6064
  });
6028
6065
  } catch (reinstallErr) {
6029
6066
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
6030
- log$3.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
6067
+ log$4.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
6031
6068
  captureIntegrationFailure(id, "reinstall", reinstallErr);
6032
6069
  report.errors.push({
6033
6070
  integrationId: id,
@@ -6044,7 +6081,7 @@ var ReconciliationEngine = class {
6044
6081
  }
6045
6082
  if (local.status !== "active") {
6046
6083
  await this.ensureSuspended();
6047
- log$3.info(`Activating ${id}`);
6084
+ log$4.info(`Activating ${id}`);
6048
6085
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
6049
6086
  report.activated.push(id);
6050
6087
  report.results.push({
@@ -6056,7 +6093,7 @@ var ReconciliationEngine = class {
6056
6093
  }
6057
6094
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6058
6095
  await this.ensureSuspended();
6059
- log$3.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6096
+ log$4.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6060
6097
  this.manager.resetReinstallAttempts(id);
6061
6098
  this.activateAttempts.delete(id);
6062
6099
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6077,7 +6114,7 @@ var ReconciliationEngine = class {
6077
6114
  });
6078
6115
  } catch (err) {
6079
6116
  const message = err instanceof Error ? err.message : String(err);
6080
- log$3.error({ err }, `Error reconciling ${id}`);
6117
+ log$4.error({ err }, `Error reconciling ${id}`);
6081
6118
  captureIntegrationFailure(id, "reconcile_active", err);
6082
6119
  report.errors.push({
6083
6120
  integrationId: id,
@@ -6102,7 +6139,7 @@ var ReconciliationEngine = class {
6102
6139
  }
6103
6140
  try {
6104
6141
  await this.ensureSuspended();
6105
- log$3.info(`Removing ${id}`);
6142
+ log$4.info(`Removing ${id}`);
6106
6143
  if (local.status === "active") {
6107
6144
  await this.manager.deactivate(id);
6108
6145
  report.deactivated.push(id);
@@ -6116,7 +6153,7 @@ var ReconciliationEngine = class {
6116
6153
  });
6117
6154
  } catch (err) {
6118
6155
  const message = err instanceof Error ? err.message : String(err);
6119
- log$3.error({ err }, `Error removing ${id}`);
6156
+ log$4.error({ err }, `Error removing ${id}`);
6120
6157
  captureIntegrationFailure(id, "reconcile_removed", err);
6121
6158
  report.errors.push({
6122
6159
  integrationId: id,
@@ -6154,6 +6191,7 @@ var CloudClient = class {
6154
6191
  onRuntimeRestartNeeded = null;
6155
6192
  onReconciliationComplete = null;
6156
6193
  reconciliationEngine = null;
6194
+ configReconciler = null;
6157
6195
  reconciling = false;
6158
6196
  pendingDesiredState = null;
6159
6197
  constructor(config) {
@@ -6202,6 +6240,14 @@ var CloudClient = class {
6202
6240
  this.reconciliationEngine = new ReconciliationEngine(manager, gate);
6203
6241
  }
6204
6242
  /**
6243
+ * Set the config reconciler for the DESIRED_STATE `config` block (plan A4).
6244
+ * When set, a DESIRED_STATE carrying `config` runs a config-reconcile pass
6245
+ * BEFORE the integrations loop and reports config status immediately.
6246
+ */
6247
+ setConfigReconciler(reconciler) {
6248
+ this.configReconciler = reconciler;
6249
+ }
6250
+ /**
6205
6251
  * Start the cloud connection with auto-reconnect.
6206
6252
  */
6207
6253
  start() {
@@ -6424,6 +6470,19 @@ var CloudClient = class {
6424
6470
  }
6425
6471
  }
6426
6472
  async runReconciliation(msg) {
6473
+ if (msg.config && this.configReconciler) try {
6474
+ const configReport = await this.configReconciler.reconcile(msg.config);
6475
+ if (configReport) {
6476
+ this.send(createConfigReport(configReport));
6477
+ logger$1.info({
6478
+ version: configReport.appliedVersion,
6479
+ status: configReport.status
6480
+ }, "Cloud: sent config reconcile report");
6481
+ }
6482
+ } catch (err) {
6483
+ const message = err instanceof Error ? err.message : String(err);
6484
+ logger$1.error({ err: message }, "Cloud: config reconcile failed");
6485
+ }
6427
6486
  if (!this.reconciliationEngine) return;
6428
6487
  const report = await this.reconciliationEngine.reconcile(msg.integrations);
6429
6488
  logger$1.info({
@@ -6476,6 +6535,86 @@ var CloudClient = class {
6476
6535
  }
6477
6536
  };
6478
6537
  //#endregion
6538
+ //#region src/config-reconciler.ts
6539
+ const log$3 = logger$1.child({ component: "ConfigReconciler" });
6540
+ const DEFAULT_VERIFY_RETRIES = 3;
6541
+ const DEFAULT_VERIFY_RETRY_DELAY_MS = 750;
6542
+ const delay = (ms) => new Promise((resolve) => {
6543
+ setTimeout(resolve, ms);
6544
+ });
6545
+ var ConfigReconciler = class {
6546
+ getApplier;
6547
+ verifyRetries;
6548
+ verifyRetryDelayMs;
6549
+ constructor(options) {
6550
+ this.getApplier = options.getApplier;
6551
+ this.verifyRetries = options.verifyRetries ?? DEFAULT_VERIFY_RETRIES;
6552
+ this.verifyRetryDelayMs = options.verifyRetryDelayMs ?? DEFAULT_VERIFY_RETRY_DELAY_MS;
6553
+ }
6554
+ /**
6555
+ * Reconcile the desired config block. Returns the outcome to report, or `null`
6556
+ * when there is nothing to report (no applier able to apply config).
6557
+ */
6558
+ async reconcile(desired) {
6559
+ const applier = this.getApplier();
6560
+ if (!applier?.setConfigRaw) {
6561
+ log$3.debug("No applier with setConfigRaw — skipping config reconcile");
6562
+ return null;
6563
+ }
6564
+ const setConfigRaw = applier.setConfigRaw.bind(applier);
6565
+ const getConfigRaw = applier.getConfigRaw?.bind(applier);
6566
+ const entries = Object.entries(desired.values).filter((e) => e[1] !== null);
6567
+ if (entries.length === 0) return {
6568
+ appliedVersion: desired.version,
6569
+ status: "applied"
6570
+ };
6571
+ const failures = [];
6572
+ for (const [key, want] of entries) try {
6573
+ if (getConfigRaw) {
6574
+ if (await getConfigRaw(key) === want) {
6575
+ log$3.debug({ key }, "Config already at desired value — no-op");
6576
+ continue;
6577
+ }
6578
+ }
6579
+ await setConfigRaw(key, want);
6580
+ if (getConfigRaw) {
6581
+ if (!await this.verify(getConfigRaw, key, want)) failures.push(`${key}: verify failed (read-back did not match)`);
6582
+ }
6583
+ } catch (err) {
6584
+ const msg = err instanceof Error ? err.message : String(err);
6585
+ failures.push(`${key}: ${msg}`);
6586
+ }
6587
+ if (failures.length > 0) {
6588
+ const joined = failures.join("; ");
6589
+ const reason = joined.length > 480 ? `${joined.slice(0, 480)}… (truncated)` : joined;
6590
+ log$3.warn({
6591
+ version: desired.version,
6592
+ reason
6593
+ }, "Config reconcile failed");
6594
+ return {
6595
+ appliedVersion: desired.version,
6596
+ status: "failed",
6597
+ reason
6598
+ };
6599
+ }
6600
+ log$3.info({
6601
+ version: desired.version,
6602
+ keys: entries.length
6603
+ }, "Config reconcile applied");
6604
+ return {
6605
+ appliedVersion: desired.version,
6606
+ status: "applied"
6607
+ };
6608
+ }
6609
+ async verify(getConfigRaw, key, want) {
6610
+ for (let attempt = 0; attempt <= this.verifyRetries; attempt++) {
6611
+ if (await getConfigRaw(key) === want) return true;
6612
+ if (attempt < this.verifyRetries && this.verifyRetryDelayMs > 0) await delay(this.verifyRetryDelayMs * 2 ** attempt);
6613
+ }
6614
+ return false;
6615
+ }
6616
+ };
6617
+ //#endregion
6479
6618
  //#region src/ipc-server.ts
6480
6619
  /**
6481
6620
  * IPC Server — Unix socket server for local plugin connections.
@@ -6823,12 +6962,12 @@ var IPCServer = class {
6823
6962
  };
6824
6963
  //#endregion
6825
6964
  //#region src/command-queue.ts
6826
- const DEFAULT_TTL_MS = 300 * 1e3;
6965
+ const DEFAULT_TTL_MS$1 = 300 * 1e3;
6827
6966
  var CommandQueue = class {
6828
6967
  queues = /* @__PURE__ */ new Map();
6829
6968
  ttlMs;
6830
6969
  gcTimer = null;
6831
- constructor(ttlMs = DEFAULT_TTL_MS) {
6970
+ constructor(ttlMs = DEFAULT_TTL_MS$1) {
6832
6971
  this.ttlMs = ttlMs;
6833
6972
  }
6834
6973
  /**
@@ -22838,6 +22977,73 @@ var CommandRegistry = class {
22838
22977
  }
22839
22978
  };
22840
22979
  //#endregion
22980
+ //#region src/command-dedupe.ts
22981
+ const DEFAULT_MAX_ENTRIES = 200;
22982
+ const DEFAULT_TTL_MS = 600 * 1e3;
22983
+ var CommandDedupe = class {
22984
+ entries = /* @__PURE__ */ new Map();
22985
+ maxEntries;
22986
+ ttlMs;
22987
+ now;
22988
+ constructor(opts = {}) {
22989
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
22990
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
22991
+ this.now = opts.now ?? (() => Date.now());
22992
+ }
22993
+ /**
22994
+ * Run `exec` for `commandId` exactly once within the dedupe window. A repeat
22995
+ * call with the same id (while still remembered) returns the FIRST call's
22996
+ * promise — either the in-flight execution or the settled replayable ACK — so
22997
+ * the command body never runs twice.
22998
+ */
22999
+ async run(commandId, exec) {
23000
+ this.evictExpired();
23001
+ const existing = this.entries.get(commandId);
23002
+ if (existing) return {
23003
+ ack: await existing.promise,
23004
+ duplicate: true
23005
+ };
23006
+ const promise = exec();
23007
+ this.entries.set(commandId, {
23008
+ insertedAt: this.now(),
23009
+ promise
23010
+ });
23011
+ this.evictOverCap();
23012
+ promise.catch(() => {
23013
+ this.entries.delete(commandId);
23014
+ });
23015
+ return {
23016
+ ack: await promise,
23017
+ duplicate: false
23018
+ };
23019
+ }
23020
+ /** Test/inspection helper. */
23021
+ size() {
23022
+ this.evictExpired();
23023
+ return this.entries.size;
23024
+ }
23025
+ has(commandId) {
23026
+ const e = this.entries.get(commandId);
23027
+ if (!e) return false;
23028
+ if (this.now() - e.insertedAt > this.ttlMs) {
23029
+ this.entries.delete(commandId);
23030
+ return false;
23031
+ }
23032
+ return true;
23033
+ }
23034
+ evictExpired() {
23035
+ const cutoff = this.now() - this.ttlMs;
23036
+ for (const [id, entry] of this.entries) if (entry.insertedAt <= cutoff) this.entries.delete(id);
23037
+ }
23038
+ evictOverCap() {
23039
+ while (this.entries.size > this.maxEntries) {
23040
+ const oldest = this.entries.keys().next().value;
23041
+ if (oldest === void 0) break;
23042
+ this.entries.delete(oldest);
23043
+ }
23044
+ }
23045
+ };
23046
+ //#endregion
22841
23047
  //#region src/pairing-approval.ts
22842
23048
  /**
22843
23049
  * Pairing approval poller — auto-approves pending OpenClaw scope-upgrade
@@ -23245,6 +23451,13 @@ let runtimeProcess = null;
23245
23451
  let aiProxyUrl = null;
23246
23452
  let aiProxyRunning = false;
23247
23453
  let cloudConnected = false;
23454
+ /**
23455
+ * Dedupe for at-least-once durable command delivery (Phase B). The cloud may
23456
+ * push a command over the live WS AND re-drain the same commandId on the next
23457
+ * SERVICE_REGISTER; both hit handleCloudCommand. This remembers recent outcomes
23458
+ * so a duplicate is ACKed with the prior result without re-executing.
23459
+ */
23460
+ const commandDedupe = new CommandDedupe();
23248
23461
  let shuttingDown = false;
23249
23462
  let commandRegistry;
23250
23463
  let resolvedCliVersion;
@@ -23641,6 +23854,7 @@ async function startDaemon() {
23641
23854
  const integrationAdapter = new IntegrationManagerAdapter(integrationManager);
23642
23855
  const runtimeGate = new RuntimeGate(() => runtimeProcess);
23643
23856
  cloudClient.setIntegrationManager(integrationAdapter, runtimeGate);
23857
+ cloudClient.setConfigReconciler(new ConfigReconciler({ getApplier: () => runtimeAppliers.get(config.runtime) }));
23644
23858
  const requestRuntimeRestart = () => {
23645
23859
  logger$1.info("Runtime state changed — requesting runtime restart");
23646
23860
  runtimeGate.requestRestart();
@@ -23767,7 +23981,34 @@ async function startDaemon() {
23767
23981
  agentId: config.agentId
23768
23982
  }, "Alfe Gateway Daemon started ✅");
23769
23983
  }
23984
+ /**
23985
+ * Single command choke point. Dedupes at-least-once durable deliveries
23986
+ * (Phase B) — a repeat commandId within the dedupe window is ACKed with the
23987
+ * prior outcome (tagged `duplicate: true`) and NOT re-executed.
23988
+ *
23989
+ * Non-idempotent, replay-ordering-hazardous commands (daemon.update /
23990
+ * daemon.restart / runtime.restart) benefit most: a re-drained restart must not
23991
+ * bounce the box a second time.
23992
+ */
23770
23993
  async function handleCloudCommand(command) {
23994
+ const { ack, duplicate } = await commandDedupe.run(command.commandId, () => executeCloudCommand(command));
23995
+ if (duplicate) {
23996
+ logger$1.info({
23997
+ commandId: command.commandId,
23998
+ command: command.command
23999
+ }, "Duplicate command — replaying prior ACK without re-executing");
24000
+ const priorResult = ack.result && typeof ack.result === "object" && !Array.isArray(ack.result) ? ack.result : { result: ack.result };
24001
+ return {
24002
+ ...ack,
24003
+ result: {
24004
+ ...priorResult,
24005
+ duplicate: true
24006
+ }
24007
+ };
24008
+ }
24009
+ return ack;
24010
+ }
24011
+ async function executeCloudCommand(command) {
23771
24012
  if (command.command === "daemon.update") {
23772
24013
  const version = command.payload?.version ?? "latest";
23773
24014
  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
  /**
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.0",
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",