@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 +305 -34
- package/dist/src/index.d.ts +15 -4
- package/package.json +2 -2
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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).
|
|
5380
|
-
* project
|
|
5381
|
-
*
|
|
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://
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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(() => {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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).
|
|
87
|
-
* project
|
|
88
|
-
*
|
|
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://
|
|
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.
|
|
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.
|
|
30
|
+
"@alfe.ai/integrations": "^0.4.0",
|
|
31
31
|
"@alfe.ai/mcp-bundler": "^0.3.0"
|
|
32
32
|
},
|
|
33
33
|
"license": "UNLICENSED",
|