@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 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
- voiceClient;
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 qs = options?.includeInherited ? "?includeInherited=true" : "";
622
- return this.client.request(`/integrations/agents/${agentId}${qs}`);
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
- logger$1.debug({ templateKey }, "Fetching template files...");
5058
- const filesResponse = await fetch(`${apiEndpoint}/templates/${encodeURIComponent(templateKey)}/files`, {
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 filesResult = await filesResponse.json();
5071
- const fileCount = Object.keys(filesResult.data?.files ?? {}).length;
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: filesResult.data?.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
- constructor(manager) {
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
- for (const desired of desiredIntegrations) if (desired.desiredStatus === "active") await this.reconcileActive(desired, localMap.get(desired.integrationId), report);
5274
- else await this.reconcileRemoved(desired.integrationId, localMap.get(desired.integrationId), report);
5275
- for (const local of localIntegrations) if (!desiredMap.has(local.id)) await this.reconcileRemoved(local.id, local, report);
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
- setIntegrationManager(manager) {
5554
- this.reconciliationEngine = new ReconciliationEngine(manager);
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
- cloudClient.setIntegrationManager(integrationAdapter);
22762
+ const runtimeGate = new RuntimeGate(() => runtimeProcess);
22763
+ cloudClient.setIntegrationManager(integrationAdapter, runtimeGate);
22597
22764
  const requestRuntimeRestart = () => {
22598
- if (runtimeProcess) {
22599
- logger$1.info("Runtime state changed — restarting runtime");
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") {
@@ -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.0",
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.3.0",
26
- "@alfe.ai/ai-proxy-local": "^0.0.11",
27
- "@alfe.ai/config": "^0.1.0",
28
- "@alfe.ai/integration-manifest": "^0.3.0",
29
- "@alfe.ai/integrations": "^0.2.0",
30
- "@alfe.ai/mcp-bundler": "^0.2.1"
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",