@alfe.ai/gateway 0.2.0 → 0.2.2
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/README.md +5 -0
- package/dist/health.js +192 -29
- package/dist/src/index.d.ts +12 -0
- package/package.json +15 -7
package/README.md
CHANGED
|
@@ -161,3 +161,8 @@ See `src/protocol.ts` for full type definitions.
|
|
|
161
161
|
|
|
162
162
|
- Dashboard integration management (backend wired, UI in progress)
|
|
163
163
|
- Voice migration to integration model (`packages/openclaw-voice/` exists, not yet full integration)
|
|
164
|
+
|
|
165
|
+
## Links
|
|
166
|
+
|
|
167
|
+
- 🌐 Website: <https://alfe.ai>
|
|
168
|
+
- 📚 Docs: <https://docs.alfe.ai>
|
package/dist/health.js
CHANGED
|
@@ -495,6 +495,15 @@ var AuthService = class {
|
|
|
495
495
|
getSubscriptionStatus() {
|
|
496
496
|
return this.client.request(`${this.prefix}/subscription`);
|
|
497
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Mint a short-lived, single-use Clerk sign-in token for the calling user so
|
|
500
|
+
* a native client can hand its logged-in session to the web dashboard
|
|
501
|
+
* (`${dashboard}/handoff?__clerk_ticket=<token>`). Server mints only for the
|
|
502
|
+
* authenticated user — never a caller-supplied id.
|
|
503
|
+
*/
|
|
504
|
+
createHandoffToken() {
|
|
505
|
+
return this.client.request(`${this.prefix}/handoff-token`, { method: "POST" });
|
|
506
|
+
}
|
|
498
507
|
applyForStartup(input) {
|
|
499
508
|
return this.client.request(`${this.prefix}/subscription/apply-startup`, {
|
|
500
509
|
method: "POST",
|
|
@@ -566,10 +575,8 @@ var AuthService = class {
|
|
|
566
575
|
//#region ../../packages-internal/api-client/dist/services/integrations.js
|
|
567
576
|
var IntegrationsService = class {
|
|
568
577
|
client;
|
|
569
|
-
|
|
570
|
-
constructor(client, voiceClient) {
|
|
578
|
+
constructor(client) {
|
|
571
579
|
this.client = client;
|
|
572
|
-
this.voiceClient = voiceClient;
|
|
573
580
|
}
|
|
574
581
|
listScopedInstalls(scope, scopeId) {
|
|
575
582
|
const params = new URLSearchParams({
|
|
@@ -618,8 +625,11 @@ var IntegrationsService = class {
|
|
|
618
625
|
});
|
|
619
626
|
}
|
|
620
627
|
listIntegrations(agentId, options) {
|
|
621
|
-
const
|
|
622
|
-
|
|
628
|
+
const params = new URLSearchParams();
|
|
629
|
+
if (options?.includeInherited) params.set("includeInherited", "true");
|
|
630
|
+
if (options?.effective) params.set("effective", "true");
|
|
631
|
+
const qs = params.toString();
|
|
632
|
+
return this.client.request(`/integrations/agents/${agentId}${qs ? `?${qs}` : ""}`);
|
|
623
633
|
}
|
|
624
634
|
installIntegration(agentId, data) {
|
|
625
635
|
return this.client.request(`/integrations/agents/${agentId}`, {
|
|
@@ -651,10 +661,6 @@ var IntegrationsService = class {
|
|
|
651
661
|
body: JSON.stringify({ agentId })
|
|
652
662
|
});
|
|
653
663
|
}
|
|
654
|
-
listVoices() {
|
|
655
|
-
if (this.voiceClient) return this.voiceClient.request("/api/voices");
|
|
656
|
-
return this.client.request("/integrations/voices");
|
|
657
|
-
}
|
|
658
664
|
getDiscordGuildChannels(guildId) {
|
|
659
665
|
return this.client.request(`/discord/guilds/${encodeURIComponent(guildId)}/channels`);
|
|
660
666
|
}
|
|
@@ -4483,6 +4489,15 @@ enumValues({
|
|
|
4483
4489
|
NanoClaw: "nanoclaw",
|
|
4484
4490
|
Hermes: "hermes"
|
|
4485
4491
|
});
|
|
4492
|
+
const TTS_MODELS = enumValues({
|
|
4493
|
+
ElevenLabsTurbo: "elevenlabs-turbo",
|
|
4494
|
+
ElevenLabsStandard: "elevenlabs-standard"
|
|
4495
|
+
});
|
|
4496
|
+
object({
|
|
4497
|
+
voiceId: string().regex(/^[A-Za-z0-9]{8,64}$/).optional(),
|
|
4498
|
+
ttsModel: _enum(TTS_MODELS).optional(),
|
|
4499
|
+
enabled: boolean().optional()
|
|
4500
|
+
});
|
|
4486
4501
|
enumValues({
|
|
4487
4502
|
Org: "org",
|
|
4488
4503
|
Team: "team",
|
|
@@ -4769,10 +4784,12 @@ const NotificationType = {
|
|
|
4769
4784
|
SubscriptionCancelled: "subscription.cancelled",
|
|
4770
4785
|
SubscriptionPastDue: "subscription.past_due",
|
|
4771
4786
|
BalanceThresholdWarning: "balance.threshold.warning",
|
|
4787
|
+
PlatformTierPriceIncrease: "platform.tier_price_increase",
|
|
4772
4788
|
AgentCreated: "agent.created",
|
|
4773
4789
|
AgentProvisionFailed: "agent.provision.failed",
|
|
4774
4790
|
AgentBillingSuspended: "agent.billing.suspended",
|
|
4775
4791
|
AgentDisconnectedExtended: "agent.disconnected.extended",
|
|
4792
|
+
BrowserTakeoverRequested: "browser_takeover_requested",
|
|
4776
4793
|
InviteCreated: "onboarding.invite.created",
|
|
4777
4794
|
OrgClaimed: "onboarding.org.claimed",
|
|
4778
4795
|
TeamMemberAdded: "team.member.added",
|
|
@@ -4789,7 +4806,7 @@ const RecipientStrategy = {
|
|
|
4789
4806
|
TenantAdmins: "tenant_admins",
|
|
4790
4807
|
SpecificUser: "specific_user"
|
|
4791
4808
|
};
|
|
4792
|
-
NotificationType.PaymentSucceeded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PaymentFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.TopUpCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AutoRechargeCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AutoRechargeFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.SubscriptionCreated, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.SubscriptionCancelled, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
|
|
4809
|
+
NotificationType.PaymentSucceeded, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PaymentFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.TopUpCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AutoRechargeCompleted, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AutoRechargeFailed, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.SubscriptionCreated, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.SubscriptionCancelled, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.SubscriptionPastDue, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationChannel.Sms, NotificationType.BalanceThresholdWarning, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.PlatformTierPriceIncrease, NotificationCategory.Billing, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationType.AgentCreated, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentProvisionFailed, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentBillingSuspended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.AgentDisconnectedExtended, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.BrowserTakeoverRequested, NotificationCategory.Agents, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.InviteCreated, NotificationCategory.System, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationType.OrgClaimed, NotificationCategory.System, RecipientStrategy.TenantAdmins, NotificationChannel.Email, NotificationChannel.Push, NotificationType.TeamMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.ProjectMemberAdded, NotificationCategory.Team, RecipientStrategy.SpecificUser, NotificationChannel.Email, NotificationChannel.Push, NotificationType.IntegrationInstalled, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push, NotificationType.IntegrationRemoved, NotificationCategory.Integrations, RecipientStrategy.TenantAdmins, NotificationChannel.Push;
|
|
4793
4810
|
//#endregion
|
|
4794
4811
|
//#region src/config.ts
|
|
4795
4812
|
/**
|
|
@@ -5047,6 +5064,7 @@ async function fetchAgentConfig(apiKey, apiEndpoint) {
|
|
|
5047
5064
|
const wsResult = await wsResponse.json();
|
|
5048
5065
|
const templateKey = wsResult.data?.templateKey;
|
|
5049
5066
|
const defaultModel = wsResult.data?.defaultModel ?? void 0;
|
|
5067
|
+
const installedFrom = wsResult.data?.installedFrom;
|
|
5050
5068
|
if (!templateKey) {
|
|
5051
5069
|
logger$1.debug("No templateKey in workspace response");
|
|
5052
5070
|
return defaultModel ? {
|
|
@@ -5054,8 +5072,14 @@ async function fetchAgentConfig(apiKey, apiEndpoint) {
|
|
|
5054
5072
|
files: {}
|
|
5055
5073
|
} : null;
|
|
5056
5074
|
}
|
|
5057
|
-
|
|
5058
|
-
const
|
|
5075
|
+
const pinnedVersion = installedFrom?.templateKey === templateKey ? installedFrom.version : void 0;
|
|
5076
|
+
const filesUrl = new URL(`${apiEndpoint}/templates/${encodeURIComponent(templateKey)}/files`);
|
|
5077
|
+
if (pinnedVersion !== void 0) filesUrl.searchParams.set("version", String(pinnedVersion));
|
|
5078
|
+
logger$1.debug({
|
|
5079
|
+
templateKey,
|
|
5080
|
+
version: pinnedVersion
|
|
5081
|
+
}, "Fetching template files...");
|
|
5082
|
+
const filesResponse = await fetch(filesUrl.toString(), {
|
|
5059
5083
|
method: "GET",
|
|
5060
5084
|
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
5061
5085
|
});
|
|
@@ -5067,16 +5091,18 @@ async function fetchAgentConfig(apiKey, apiEndpoint) {
|
|
|
5067
5091
|
files: {}
|
|
5068
5092
|
};
|
|
5069
5093
|
}
|
|
5070
|
-
const
|
|
5071
|
-
const fileCount = Object.keys(
|
|
5094
|
+
const files = (await filesResponse.json()).data?.files ?? {};
|
|
5095
|
+
const fileCount = Object.keys(files).length;
|
|
5072
5096
|
logger$1.debug({
|
|
5073
5097
|
templateKey,
|
|
5098
|
+
version: pinnedVersion,
|
|
5074
5099
|
fileCount
|
|
5075
5100
|
}, "Template files fetched");
|
|
5076
5101
|
return {
|
|
5077
5102
|
templateKey,
|
|
5078
5103
|
defaultModel,
|
|
5079
|
-
files
|
|
5104
|
+
files,
|
|
5105
|
+
personaFiles: Object.keys(files)
|
|
5080
5106
|
};
|
|
5081
5107
|
} catch (err) {
|
|
5082
5108
|
logger$1.debug({ err: err instanceof Error ? err.message : String(err) }, "fetchAgentConfig failed");
|
|
@@ -5231,6 +5257,79 @@ function isIPCResponse(msg) {
|
|
|
5231
5257
|
/** Current IPC protocol version */
|
|
5232
5258
|
const PROTOCOL_VERSION = 1;
|
|
5233
5259
|
//#endregion
|
|
5260
|
+
//#region src/runtime-gate.ts
|
|
5261
|
+
const log$3 = logger$1.child({ component: "RuntimeGate" });
|
|
5262
|
+
var RuntimeGate = class {
|
|
5263
|
+
depth = 0;
|
|
5264
|
+
wasRunning = false;
|
|
5265
|
+
constructor(getRuntime) {
|
|
5266
|
+
this.getRuntime = getRuntime;
|
|
5267
|
+
}
|
|
5268
|
+
/**
|
|
5269
|
+
* Suspend the runtime child. Only the outermost call (depth 0 → 1) actually
|
|
5270
|
+
* stops it; nested calls just bump the refcount. Remembers whether the child
|
|
5271
|
+
* was running so `resume()` doesn't spuriously start a runtime that was never
|
|
5272
|
+
* up (e.g. autoStartRuntime=false).
|
|
5273
|
+
*/
|
|
5274
|
+
async suspend() {
|
|
5275
|
+
if (this.depth++ > 0) return;
|
|
5276
|
+
const rp = this.getRuntime();
|
|
5277
|
+
this.wasRunning = rp?.isRunning ?? false;
|
|
5278
|
+
if (rp && this.wasRunning) {
|
|
5279
|
+
log$3.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
|
|
5280
|
+
await rp.stop();
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
/**
|
|
5284
|
+
* Resume the runtime child. Only the outermost call (depth 1 → 0) actually
|
|
5285
|
+
* restarts it, and only if it was running when we suspended. This subsumes
|
|
5286
|
+
* the old post-reconcile "restart if plugins changed" step — bringing the
|
|
5287
|
+
* runtime back is exactly what a restart-after-reconcile did.
|
|
5288
|
+
*/
|
|
5289
|
+
resume() {
|
|
5290
|
+
if (--this.depth > 0) return Promise.resolve();
|
|
5291
|
+
if (this.depth < 0) this.depth = 0;
|
|
5292
|
+
const rp = this.getRuntime();
|
|
5293
|
+
if (rp && this.wasRunning) {
|
|
5294
|
+
log$3.info("Resuming runtime after mutating reconcile");
|
|
5295
|
+
rp.resume();
|
|
5296
|
+
}
|
|
5297
|
+
this.wasRunning = false;
|
|
5298
|
+
return Promise.resolve();
|
|
5299
|
+
}
|
|
5300
|
+
/**
|
|
5301
|
+
* Out-of-band restart request (e.g. `alfe.config_set`, Hermes MCP sync).
|
|
5302
|
+
* Deferred while a suspend is active — the pending `resume()` will bring the
|
|
5303
|
+
* runtime back with fresh config, so restarting mid-suspend would just fight
|
|
5304
|
+
* the gate (and risk spawning the runtime while a CLI mutation is still
|
|
5305
|
+
* writing the DB).
|
|
5306
|
+
*/
|
|
5307
|
+
requestRestart() {
|
|
5308
|
+
if (this.depth > 0) {
|
|
5309
|
+
log$3.info("Runtime restart requested while suspended — deferring to pending resume");
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5312
|
+
const rp = this.getRuntime();
|
|
5313
|
+
if (rp) rp.restart().catch((err) => {
|
|
5314
|
+
log$3.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
|
|
5315
|
+
});
|
|
5316
|
+
}
|
|
5317
|
+
};
|
|
5318
|
+
/**
|
|
5319
|
+
* A gate that never touches the runtime — for runtimes that don't share the
|
|
5320
|
+
* single-writer SQLite hazard, or for tests. `suspend`/`resume` are no-ops and
|
|
5321
|
+
* `requestRestart` does nothing.
|
|
5322
|
+
*/
|
|
5323
|
+
var NoopRuntimeGate = class {
|
|
5324
|
+
suspend() {
|
|
5325
|
+
return Promise.resolve();
|
|
5326
|
+
}
|
|
5327
|
+
resume() {
|
|
5328
|
+
return Promise.resolve();
|
|
5329
|
+
}
|
|
5330
|
+
requestRestart() {}
|
|
5331
|
+
};
|
|
5332
|
+
//#endregion
|
|
5234
5333
|
//#region src/reconciliation.ts
|
|
5235
5334
|
const log$2 = logger$1.child({ component: "Reconciliation" });
|
|
5236
5335
|
/**
|
|
@@ -5245,13 +5344,50 @@ const MAX_ERROR_ACTIVATE_ATTEMPTS = 3;
|
|
|
5245
5344
|
var ReconciliationEngine = class {
|
|
5246
5345
|
/** Consecutive re-activation attempts for an intact error-state integration, keyed by id. */
|
|
5247
5346
|
activateAttempts = /* @__PURE__ */ new Map();
|
|
5248
|
-
|
|
5347
|
+
/**
|
|
5348
|
+
* Whether this reconcile pass has already suspended the runtime. Reset at the
|
|
5349
|
+
* start of every {@link reconcile} call. The runtime is suspended lazily on
|
|
5350
|
+
* the first mutating branch (install / reinstall / activate / removal-of-
|
|
5351
|
+
* existing) so an all-`up_to_date` pass never touches the runtime.
|
|
5352
|
+
*/
|
|
5353
|
+
suspended = false;
|
|
5354
|
+
/**
|
|
5355
|
+
* @param manager The integration manager to converge.
|
|
5356
|
+
* @param gate Runtime gate used to suspend the runtime child around
|
|
5357
|
+
* mutating branches so `openclaw` CLI subprocesses don't write
|
|
5358
|
+
* `~/.openclaw/state/openclaw.sqlite` concurrently with a live
|
|
5359
|
+
* `openclaw gateway run` (see runtime-gate.ts). Defaults to a
|
|
5360
|
+
* no-op gate for runtimes/tests that don't need protection.
|
|
5361
|
+
*/
|
|
5362
|
+
constructor(manager, gate = new NoopRuntimeGate()) {
|
|
5249
5363
|
this.manager = manager;
|
|
5364
|
+
this.gate = gate;
|
|
5365
|
+
}
|
|
5366
|
+
/**
|
|
5367
|
+
* Suspend the runtime on the first mutating branch of a reconcile pass.
|
|
5368
|
+
* Idempotent within a pass — subsequent mutating branches are covered by the
|
|
5369
|
+
* single suspend held until the `finally` in {@link reconcile}.
|
|
5370
|
+
*/
|
|
5371
|
+
async ensureSuspended() {
|
|
5372
|
+
if (!this.suspended) {
|
|
5373
|
+
this.suspended = true;
|
|
5374
|
+
await this.gate.suspend();
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
/**
|
|
5378
|
+
* Resume the runtime if this pass suspended it. Called from `reconcile()`'s
|
|
5379
|
+
* `finally`. Kept as a method (rather than an inline `if (this.suspended)`)
|
|
5380
|
+
* so the mutation performed by {@link ensureSuspended} in a different method
|
|
5381
|
+
* isn't narrowed away by control-flow analysis at the callsite.
|
|
5382
|
+
*/
|
|
5383
|
+
async resumeIfSuspended() {
|
|
5384
|
+
if (this.suspended) await this.gate.resume();
|
|
5250
5385
|
}
|
|
5251
5386
|
/**
|
|
5252
5387
|
* Reconcile local state against desired state from cloud.
|
|
5253
5388
|
*/
|
|
5254
5389
|
async reconcile(desiredIntegrations) {
|
|
5390
|
+
this.suspended = false;
|
|
5255
5391
|
const report = {
|
|
5256
5392
|
results: [],
|
|
5257
5393
|
installed: [],
|
|
@@ -5270,15 +5406,20 @@ var ReconciliationEngine = class {
|
|
|
5270
5406
|
}
|
|
5271
5407
|
const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
|
|
5272
5408
|
const desiredMap = new Map(desiredIntegrations.map((d) => [d.integrationId, d]));
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5409
|
+
try {
|
|
5410
|
+
for (const desired of desiredIntegrations) if (desired.desiredStatus === "active") await this.reconcileActive(desired, localMap.get(desired.integrationId), report);
|
|
5411
|
+
else await this.reconcileRemoved(desired.integrationId, localMap.get(desired.integrationId), report);
|
|
5412
|
+
for (const local of localIntegrations) if (!desiredMap.has(local.id)) await this.reconcileRemoved(local.id, local, report);
|
|
5413
|
+
} finally {
|
|
5414
|
+
await this.resumeIfSuspended();
|
|
5415
|
+
}
|
|
5276
5416
|
return report;
|
|
5277
5417
|
}
|
|
5278
5418
|
async reconcileActive(desired, local, report) {
|
|
5279
5419
|
const id = desired.integrationId;
|
|
5280
5420
|
try {
|
|
5281
5421
|
if (!local) {
|
|
5422
|
+
await this.ensureSuspended();
|
|
5282
5423
|
log$2.info(`Installing ${id}@${desired.version}`);
|
|
5283
5424
|
await this.manager.install(id, desired.version, desired.config, desired.customSource);
|
|
5284
5425
|
report.installed.push(id);
|
|
@@ -5293,6 +5434,7 @@ var ReconciliationEngine = class {
|
|
|
5293
5434
|
return;
|
|
5294
5435
|
}
|
|
5295
5436
|
if (local.version !== desired.version && desired.version !== "" && local.version !== "unknown") {
|
|
5437
|
+
await this.ensureSuspended();
|
|
5296
5438
|
log$2.info(`Upgrading ${id}: ${local.version} → ${desired.version}`);
|
|
5297
5439
|
if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
|
|
5298
5440
|
report.installed.push(id);
|
|
@@ -5306,6 +5448,7 @@ var ReconciliationEngine = class {
|
|
|
5306
5448
|
}
|
|
5307
5449
|
if (local.status === "error") {
|
|
5308
5450
|
if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
|
|
5451
|
+
await this.ensureSuspended();
|
|
5309
5452
|
log$2.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
|
|
5310
5453
|
this.manager.resetReinstallAttempts(id);
|
|
5311
5454
|
this.activateAttempts.delete(id);
|
|
@@ -5335,6 +5478,7 @@ var ReconciliationEngine = class {
|
|
|
5335
5478
|
});
|
|
5336
5479
|
return;
|
|
5337
5480
|
}
|
|
5481
|
+
await this.ensureSuspended();
|
|
5338
5482
|
log$2.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
|
|
5339
5483
|
this.manager.incrementReinstallAttempts(id);
|
|
5340
5484
|
try {
|
|
@@ -5367,6 +5511,7 @@ var ReconciliationEngine = class {
|
|
|
5367
5511
|
const activateAttempts = this.activateAttempts.get(id) ?? 0;
|
|
5368
5512
|
if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
|
|
5369
5513
|
this.activateAttempts.set(id, activateAttempts + 1);
|
|
5514
|
+
await this.ensureSuspended();
|
|
5370
5515
|
log$2.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
|
|
5371
5516
|
try {
|
|
5372
5517
|
if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
|
|
@@ -5408,6 +5553,7 @@ var ReconciliationEngine = class {
|
|
|
5408
5553
|
return;
|
|
5409
5554
|
}
|
|
5410
5555
|
if (local.status !== "active") {
|
|
5556
|
+
await this.ensureSuspended();
|
|
5411
5557
|
log$2.info(`Activating ${id}`);
|
|
5412
5558
|
if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
|
|
5413
5559
|
report.activated.push(id);
|
|
@@ -5419,6 +5565,7 @@ var ReconciliationEngine = class {
|
|
|
5419
5565
|
return;
|
|
5420
5566
|
}
|
|
5421
5567
|
if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
|
|
5568
|
+
await this.ensureSuspended();
|
|
5422
5569
|
log$2.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
|
|
5423
5570
|
this.manager.resetReinstallAttempts(id);
|
|
5424
5571
|
this.activateAttempts.delete(id);
|
|
@@ -5463,6 +5610,7 @@ var ReconciliationEngine = class {
|
|
|
5463
5610
|
return;
|
|
5464
5611
|
}
|
|
5465
5612
|
try {
|
|
5613
|
+
await this.ensureSuspended();
|
|
5466
5614
|
log$2.info(`Removing ${id}`);
|
|
5467
5615
|
if (local.status === "active") {
|
|
5468
5616
|
await this.manager.deactivate(id);
|
|
@@ -5550,8 +5698,16 @@ var CloudClient = class {
|
|
|
5550
5698
|
setOnReconciliationComplete(handler) {
|
|
5551
5699
|
this.onReconciliationComplete = handler;
|
|
5552
5700
|
}
|
|
5553
|
-
|
|
5554
|
-
|
|
5701
|
+
/**
|
|
5702
|
+
* Set the integration manager (and optionally the runtime gate) for
|
|
5703
|
+
* reconciliation. When set, the cloud client handles DESIRED_STATE messages
|
|
5704
|
+
* automatically. The gate is passed to the ReconciliationEngine so mutating
|
|
5705
|
+
* reconcile branches suspend the runtime child around runtime-CLI mutations
|
|
5706
|
+
* (avoids concurrent SQLite writers — see runtime-gate.ts). Resuming the
|
|
5707
|
+
* runtime after a mutating pass subsumes the old post-reconcile restart.
|
|
5708
|
+
*/
|
|
5709
|
+
setIntegrationManager(manager, gate) {
|
|
5710
|
+
this.reconciliationEngine = new ReconciliationEngine(manager, gate);
|
|
5555
5711
|
}
|
|
5556
5712
|
/**
|
|
5557
5713
|
* Start the cloud connection with auto-reconnect.
|
|
@@ -5787,7 +5943,6 @@ var CloudClient = class {
|
|
|
5787
5943
|
}, "Cloud: reconciliation complete");
|
|
5788
5944
|
const reportMsg = createReconciliationReport(report.results);
|
|
5789
5945
|
this.send(reportMsg);
|
|
5790
|
-
if ((report.installed.length > 0 || report.uninstalled.length > 0 || report.configApplied) && this.onRuntimeRestartNeeded) this.onRuntimeRestartNeeded();
|
|
5791
5946
|
this.onReconciliationComplete?.();
|
|
5792
5947
|
}
|
|
5793
5948
|
send(msg) {
|
|
@@ -21746,6 +21901,17 @@ var RuntimeProcess = class {
|
|
|
21746
21901
|
});
|
|
21747
21902
|
}
|
|
21748
21903
|
/**
|
|
21904
|
+
* Resume after a suspend (stop). `start()` alone is a no-op while stopped
|
|
21905
|
+
* because `stop()` latches `this.stopped = true`; resume clears that latch,
|
|
21906
|
+
* resets the crash backoff, and re-spawns. Used by the RuntimeGate to bring
|
|
21907
|
+
* the runtime back after a mutating reconcile branch has finished.
|
|
21908
|
+
*/
|
|
21909
|
+
resume() {
|
|
21910
|
+
this.stopped = false;
|
|
21911
|
+
this.backoffMs = BACKOFF_INITIAL_MS;
|
|
21912
|
+
this.start();
|
|
21913
|
+
}
|
|
21914
|
+
/**
|
|
21749
21915
|
* Restart the runtime process (stop then start with fresh backoff).
|
|
21750
21916
|
*/
|
|
21751
21917
|
async restart() {
|
|
@@ -22593,14 +22759,11 @@ async function startDaemon() {
|
|
|
22593
22759
|
}
|
|
22594
22760
|
});
|
|
22595
22761
|
const integrationAdapter = new IntegrationManagerAdapter(integrationManager);
|
|
22596
|
-
|
|
22762
|
+
const runtimeGate = new RuntimeGate(() => runtimeProcess);
|
|
22763
|
+
cloudClient.setIntegrationManager(integrationAdapter, runtimeGate);
|
|
22597
22764
|
const requestRuntimeRestart = () => {
|
|
22598
|
-
|
|
22599
|
-
|
|
22600
|
-
runtimeProcess.restart().catch((err) => {
|
|
22601
|
-
logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
|
|
22602
|
-
});
|
|
22603
|
-
}
|
|
22765
|
+
logger$1.info("Runtime state changed — requesting runtime restart");
|
|
22766
|
+
runtimeGate.requestRestart();
|
|
22604
22767
|
};
|
|
22605
22768
|
cloudClient.setRuntimeRestartNeededHandler(requestRuntimeRestart);
|
|
22606
22769
|
if (config.runtime === "hermes") {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -60,6 +60,18 @@ interface AgentWorkspaceConfig {
|
|
|
60
60
|
templateKey?: string;
|
|
61
61
|
defaultModel?: string;
|
|
62
62
|
files: Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* Filenames within `files` that are agent-owned persona files (SOUL.md,
|
|
65
|
+
* IDENTITY.md, AGENTS.md, USER.md, BOOTSTRAP.md, …). The consumer
|
|
66
|
+
* (`writeWorkspaceFiles`) seeds these ONLY IF ABSENT so an agent's own edits
|
|
67
|
+
* — restored by the openclaw-sync backup on a fresh VM — are never clobbered
|
|
68
|
+
* by a setup-time re-seed. Filenames NOT in this set keep overwrite-on-write
|
|
69
|
+
* semantics. The templates `/files` endpoint returns persona files
|
|
70
|
+
* exclusively today, so `fetchAgentConfig` populates this with every key; the
|
|
71
|
+
* field is threaded explicitly rather than assumed so a future workspace-file
|
|
72
|
+
* source can mix persona (seed-if-missing) and non-persona (overwrite) files.
|
|
73
|
+
*/
|
|
74
|
+
personaFiles?: string[];
|
|
63
75
|
}
|
|
64
76
|
declare const ALFE_DIR: string;
|
|
65
77
|
declare const SOCKET_PATH: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/gateway",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Alfe local gateway daemon — persistent control plane for agent integrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,14 +22,22 @@
|
|
|
22
22
|
"pino-roll": "^1.2.0",
|
|
23
23
|
"smol-toml": ">=1.6.1",
|
|
24
24
|
"ws": "^8.18.0",
|
|
25
|
-
"@alfe.ai/agent-api-client": "^0.
|
|
26
|
-
"@alfe.ai/ai-proxy-local": "^0.0.
|
|
27
|
-
"@alfe.ai/config": "^0.
|
|
28
|
-
"@alfe.ai/integration-manifest": "^0.3.
|
|
29
|
-
"@alfe.ai/integrations": "^0.2.
|
|
30
|
-
"@alfe.ai/mcp-bundler": "^0.2.
|
|
25
|
+
"@alfe.ai/agent-api-client": "^0.4.0",
|
|
26
|
+
"@alfe.ai/ai-proxy-local": "^0.0.12",
|
|
27
|
+
"@alfe.ai/config": "^0.2.0",
|
|
28
|
+
"@alfe.ai/integration-manifest": "^0.3.1",
|
|
29
|
+
"@alfe.ai/integrations": "^0.2.5",
|
|
30
|
+
"@alfe.ai/mcp-bundler": "^0.2.2"
|
|
31
31
|
},
|
|
32
32
|
"license": "UNLICENSED",
|
|
33
|
+
"homepage": "https://alfe.ai",
|
|
34
|
+
"author": "Alfe (https://alfe.ai)",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"alfe",
|
|
37
|
+
"ai-agents",
|
|
38
|
+
"agent",
|
|
39
|
+
"llm"
|
|
40
|
+
],
|
|
33
41
|
"scripts": {
|
|
34
42
|
"build": "tsdown",
|
|
35
43
|
"dev": "tsdown --watch",
|