@alfe.ai/gateway 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/health.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { n as logger$1 } from "./logger.js";
2
+ import { a as captureFatal, c as captureRuntimeCrash, d as initAgentSentry, f as setAgentContext, l as captureRuntimeErrorOutput, o as captureIntegrationFailure, s as captureMcpFailure, u as flushSentry } from "./sentry.js";
2
3
  import { createRequire } from "node:module";
3
4
  import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
4
5
  import { execFile, execSync, spawn } from "node:child_process";
@@ -7,12 +8,12 @@ import { dirname, join } from "node:path";
7
8
  import { homedir } from "node:os";
8
9
  import pino from "pino";
9
10
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
- import { getEndpointFromToken, readConfig, resolveConfig } from "@alfe.ai/config";
11
+ import { getEndpointFromToken, readConfig } from "@alfe.ai/config";
11
12
  import crypto from "crypto";
12
13
  import { parse } from "smol-toml";
13
14
  import WebSocket from "ws";
14
15
  import { createConnection, createServer } from "node:net";
15
- import { HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, NoopOpenClawCliLock, OpenClawApplier, SerialOpenClawCliLock } from "@alfe.ai/integrations";
16
+ import { ClaudeCodeApplier, ClaudeCodeMcpSync, HermesApplier, HermesMcpSync, IntegrationManager, IntegrationManagerAdapter, McpApplier, NoopOpenClawCliLock, OpenClawApplier, SerialOpenClawCliLock } from "@alfe.ai/integrations";
16
17
  import { AgentApiClient } from "@alfe.ai/agent-api-client";
17
18
  import { Manager, McpBundler, defaultConnect } from "@alfe.ai/mcp-bundler";
18
19
  import stream, { Readable } from "stream";
@@ -551,6 +552,29 @@ var AuthService = class {
551
552
  body: JSON.stringify(input)
552
553
  });
553
554
  }
555
+ /**
556
+ * Change a personal (individual-tier) subscriber's platform plan in place on
557
+ * their existing Stripe subscription — no second Checkout, no duplicate sub,
558
+ * no double charge. Upgrades prorate immediately; downgrades apply with no
559
+ * immediate charge/refund. Returns the updated subscription.
560
+ */
561
+ changePlatformPlan(input) {
562
+ return this.client.request(`${this.prefix}/subscription/change`, {
563
+ method: "POST",
564
+ body: JSON.stringify(input)
565
+ });
566
+ }
567
+ /**
568
+ * Preview the proration cost for changing a personal (individual-tier)
569
+ * subscriber's platform plan — read-only companion to `changePlatformPlan`.
570
+ * Nothing is charged.
571
+ */
572
+ previewPlatformPlanChange(input) {
573
+ return this.client.request(`${this.prefix}/subscription/change/preview`, {
574
+ method: "POST",
575
+ body: JSON.stringify(input)
576
+ });
577
+ }
554
578
  updateSeats(seats) {
555
579
  return this.client.request(`${this.prefix}/subscription/seats`, {
556
580
  method: "POST",
@@ -687,6 +711,15 @@ var IntegrationsService = class {
687
711
  reinstallIntegration(agentId, integrationId) {
688
712
  return this.client.request(`/integrations/agents/${agentId}/${integrationId}/reinstall`, { method: "POST" });
689
713
  }
714
+ /**
715
+ * Upgrade an integration to the registry's latest version via the fast,
716
+ * diff-based daemon path (bumps `version` without `reinstallRequestedAt`).
717
+ * The agent stays online — no destructive teardown. Distinct from
718
+ * {@link reinstallIntegration}, which is the destructive repair path.
719
+ */
720
+ upgradeIntegration(agentId, integrationId) {
721
+ return this.client.request(`/integrations/agents/${agentId}/${integrationId}/upgrade`, { method: "POST" });
722
+ }
690
723
  getRegistry() {
691
724
  return this.client.request("/integrations/registry");
692
725
  }
@@ -4487,6 +4520,7 @@ enumValues({
4487
4520
  IndividualPro: "individual_pro",
4488
4521
  IndividualMax: "individual_max",
4489
4522
  OrgFree: "org_free",
4523
+ OrgStartup: "org_startup",
4490
4524
  OrgProfessional: "org_professional",
4491
4525
  OrgEnterprise: "org_enterprise"
4492
4526
  });
@@ -4523,7 +4557,8 @@ enumValues({
4523
4557
  enumValues({
4524
4558
  OpenClaw: "openclaw",
4525
4559
  NanoClaw: "nanoclaw",
4526
- Hermes: "hermes"
4560
+ Hermes: "hermes",
4561
+ ClaudeCode: "claude-code"
4527
4562
  });
4528
4563
  const TTS_MODELS = enumValues({
4529
4564
  ElevenLabsTurbo: "elevenlabs-turbo",
@@ -4664,9 +4699,10 @@ enumValues({
4664
4699
  const AnthropicModel = {
4665
4700
  Opus48: "claude-opus-4-8",
4666
4701
  Opus47: "claude-opus-4-7",
4667
- Opus: "claude-opus-4-6",
4668
- Sonnet: "claude-sonnet-4-6",
4669
- Haiku: "claude-haiku-4-5"
4702
+ Opus46: "claude-opus-4-6",
4703
+ Sonnet5: "claude-sonnet-5",
4704
+ Sonnet46: "claude-sonnet-4-6",
4705
+ Haiku45: "claude-haiku-4-5"
4670
4706
  };
4671
4707
  const ANTHROPIC_MODELS = enumValues(AnthropicModel);
4672
4708
  const OpenAIModel = {
@@ -4681,9 +4717,14 @@ const OpenAIModel = {
4681
4717
  GPT54Nano: "gpt-5.4-nano",
4682
4718
  GPT54Pro: "gpt-5.4-pro",
4683
4719
  GPT55: "gpt-5.5",
4720
+ GPT55Pro: "gpt-5.5-pro",
4721
+ GPT56Sol: "gpt-5.6-sol",
4722
+ GPT56Terra: "gpt-5.6-terra",
4723
+ GPT56Luna: "gpt-5.6-luna",
4684
4724
  O3Mini: "o3-mini",
4685
4725
  O4Mini: "o4-mini",
4686
- TextEmbedding3Small: "text-embedding-3-small"
4726
+ TextEmbedding3Small: "text-embedding-3-small",
4727
+ TextEmbedding3Large: "text-embedding-3-large"
4687
4728
  };
4688
4729
  const OPENAI_MODELS = enumValues(OpenAIModel);
4689
4730
  const DeepSeekModel = {
@@ -4707,16 +4748,25 @@ const MiniMaxModel = {
4707
4748
  M3: "MiniMax-M3",
4708
4749
  M27: "MiniMax-M2.7",
4709
4750
  M27HighSpeed: "MiniMax-M2.7-highspeed",
4710
- M25: "MiniMax-M2.5"
4751
+ M25: "MiniMax-M2.5",
4752
+ M21: "MiniMax-M2.1",
4753
+ M2: "MiniMax-M2"
4711
4754
  };
4712
4755
  const MINIMAX_MODELS = enumValues(MiniMaxModel);
4713
4756
  const MistralModel = {
4714
4757
  Large: "mistral-large-latest",
4758
+ Medium: "mistral-medium-latest",
4715
4759
  Small: "mistral-small-latest",
4716
- Codestral: "codestral-latest"
4760
+ Codestral: "codestral-latest",
4761
+ Ministral8b: "ministral-8b-latest",
4762
+ Ministral3b: "ministral-3b-latest",
4763
+ MagistralMedium: "magistral-medium-latest",
4764
+ MagistralSmall: "magistral-small-latest",
4765
+ DevstralMedium: "devstral-medium-latest"
4717
4766
  };
4718
4767
  const MISTRAL_MODELS = enumValues(MistralModel);
4719
4768
  const XAIModel = {
4769
+ Grok45: "grok-4.5",
4720
4770
  Grok43: "grok-4.3",
4721
4771
  Grok4: "grok-4",
4722
4772
  Grok41Fast: "grok-4.1-fast"
@@ -4725,10 +4775,12 @@ const XAI_MODELS = enumValues(XAIModel);
4725
4775
  const ZhipuModel = {
4726
4776
  GLM52: "glm-5.2",
4727
4777
  GLM51: "glm-5.1",
4728
- GLM51Air: "glm-5.1-air"
4778
+ GLM46: "glm-4.6",
4779
+ GLM45: "glm-4.5",
4780
+ GLM45Air: "glm-4.5-air"
4729
4781
  };
4730
4782
  const ZHIPU_MODELS = enumValues(ZhipuModel);
4731
- AnthropicModel.Opus48;
4783
+ AnthropicModel.Sonnet5;
4732
4784
  [
4733
4785
  ...ANTHROPIC_MODELS,
4734
4786
  ...OPENAI_MODELS,
@@ -4748,8 +4800,8 @@ _enum(MISTRAL_MODELS);
4748
4800
  _enum(XAI_MODELS);
4749
4801
  _enum(ZHIPU_MODELS);
4750
4802
  string().min(1);
4751
- AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus, AnthropicModel.Sonnet, AnthropicModel.Haiku, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MistralModel.Large, MistralModel.Small, MistralModel.Codestral, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM51Air;
4752
- AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus, AnthropicModel.Sonnet, AnthropicModel.Haiku, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MistralModel.Large, MistralModel.Small, MistralModel.Codestral, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM51Air;
4803
+ AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air;
4804
+ AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT4o, OpenAIModel.GPT4oMini, OpenAIModel.O3, OpenAIModel.GPT41, OpenAIModel.GPT41Mini, OpenAIModel.GPT41Nano, OpenAIModel.GPT54, OpenAIModel.GPT54Mini, OpenAIModel.GPT54Nano, OpenAIModel.GPT54Pro, OpenAIModel.GPT55, OpenAIModel.GPT55Pro, OpenAIModel.GPT56Sol, OpenAIModel.GPT56Terra, OpenAIModel.GPT56Luna, OpenAIModel.O3Mini, OpenAIModel.O4Mini, OpenAIModel.TextEmbedding3Small, OpenAIModel.TextEmbedding3Large, DeepSeekModel.Chat, DeepSeekModel.Reasoner, DeepSeekModel.V4Flash, DeepSeekModel.V4Pro, GoogleModel.Gemini35Flash, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, GoogleModel.Gemini20Flash, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, MistralModel.MagistralMedium, MistralModel.MagistralSmall, MistralModel.DevstralMedium, XAIModel.Grok45, XAIModel.Grok43, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air;
4753
4805
  enumValues({
4754
4806
  PendingChallenge: "pending_challenge",
4755
4807
  Creating: "creating",
@@ -4825,6 +4877,7 @@ const NotificationType = {
4825
4877
  AutoRechargeFailed: "auto-recharge.failed",
4826
4878
  SubscriptionCreated: "subscription.created",
4827
4879
  SubscriptionCancelled: "subscription.cancelled",
4880
+ StartupGrantEnded: "startup.grant_ended",
4828
4881
  SubscriptionPastDue: "subscription.past_due",
4829
4882
  BalanceThresholdWarning: "balance.threshold.warning",
4830
4883
  PlatformTierPriceIncrease: "platform.tier_price_increase",
@@ -4849,7 +4902,7 @@ const RecipientStrategy = {
4849
4902
  TenantAdmins: "tenant_admins",
4850
4903
  SpecificUser: "specific_user"
4851
4904
  };
4852
- 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;
4905
+ 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.StartupGrantEnded, 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;
4853
4906
  //#endregion
4854
4907
  //#region src/config.ts
4855
4908
  /**
@@ -4965,6 +5018,7 @@ function deriveGatewayWsUrl(apiEndpoint) {
4965
5018
  */
4966
5019
  function deriveAgentWorkspace(runtime, home) {
4967
5020
  if (runtime === "openclaw") return join(home, "workspace");
5021
+ if (runtime === "claude-code") return join(home, "workspace");
4968
5022
  return home;
4969
5023
  }
4970
5024
  async function loadRuntimeConfigs() {
@@ -5313,7 +5367,7 @@ function isIPCResponse(msg) {
5313
5367
  const PROTOCOL_VERSION = 1;
5314
5368
  //#endregion
5315
5369
  //#region src/runtime-gate.ts
5316
- const log$5 = logger$1.child({ component: "RuntimeGate" });
5370
+ const log$6 = logger$1.child({ component: "RuntimeGate" });
5317
5371
  var RuntimeGate = class {
5318
5372
  depth = 0;
5319
5373
  wasRunning = false;
@@ -5331,8 +5385,8 @@ var RuntimeGate = class {
5331
5385
  const rp = this.getRuntime();
5332
5386
  this.wasRunning = rp?.isRunning ?? false;
5333
5387
  if (rp && this.wasRunning) {
5334
- log$5.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5335
- await rp.stop();
5388
+ log$6.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5389
+ await rp.stopWhenIdle();
5336
5390
  }
5337
5391
  }
5338
5392
  /**
@@ -5346,7 +5400,7 @@ var RuntimeGate = class {
5346
5400
  if (this.depth < 0) this.depth = 0;
5347
5401
  const rp = this.getRuntime();
5348
5402
  if (rp && this.wasRunning) {
5349
- log$5.info("Resuming runtime after mutating reconcile");
5403
+ log$6.info("Resuming runtime after mutating reconcile");
5350
5404
  rp.resume();
5351
5405
  }
5352
5406
  this.wasRunning = false;
@@ -5361,12 +5415,12 @@ var RuntimeGate = class {
5361
5415
  */
5362
5416
  requestRestart() {
5363
5417
  if (this.depth > 0) {
5364
- log$5.info("Runtime restart requested while suspended — deferring to pending resume");
5418
+ log$6.info("Runtime restart requested while suspended — deferring to pending resume");
5365
5419
  return;
5366
5420
  }
5367
5421
  const rp = this.getRuntime();
5368
5422
  if (rp) rp.restart().catch((err) => {
5369
- log$5.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5423
+ log$6.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5370
5424
  });
5371
5425
  }
5372
5426
  };
@@ -5385,392 +5439,8 @@ var NoopRuntimeGate = class {
5385
5439
  requestRestart() {}
5386
5440
  };
5387
5441
  //#endregion
5388
- //#region src/sentry.ts
5389
- /**
5390
- * Agent-side error reporting (Sentry) for the Alfe CLI + gateway daemon.
5391
- *
5392
- * This module is the single Sentry bootstrap shared by both published
5393
- * agent-side packages — `@alfe.ai/gateway` (the daemon) and `@alfe.ai/cli`
5394
- * (which re-imports these helpers from `@alfe.ai/gateway`). It exists so
5395
- * integration-installation failures on customer/managed VMs surface centrally
5396
- * instead of silently rotting until someone SSHes into the box.
5397
- *
5398
- * Design constraints (see packages/gateway/DEVELOPING.md → "Error reporting"):
5399
- * - Errors only. No tracing/profiling (`tracesSampleRate: 0`, `sampleRate: 1`).
5400
- * - `@sentry/node` is lazy-imported so `alfe --version` doesn't pay for it.
5401
- * - Sentry can NEVER break the CLI/daemon — every entry point is try/catch'd.
5402
- * - The DSNs are baked as constants: published tarballs run on agent VMs and
5403
- * cannot read the repo's `config/config.ts`. That file's `SENTRY_DSNS`
5404
- * registry (keys `agentDaemon` + `cli`) mirrors them as the human reference.
5405
- * - Secrets are scrubbed in `beforeSend`; request/env payloads are dropped.
5406
- * - Opt-out via `ALFE_ERROR_REPORTING=0|false` or `errorReporting = false`
5407
- * in `~/.alfe/config.toml`. Default ON.
5408
- */
5409
- /**
5410
- * Baked per-surface Sentry DSNs (org `alfe-ai`). DSNs are public by design
5411
- * (they only permit event ingestion). Mirrors of `config/config.ts` →
5412
- * `SENTRY_DSNS.agentDaemon` / `SENTRY_DSNS.cli`. If you rotate a DSN, update
5413
- * both places.
5414
- */
5415
- const AGENT_DAEMON_SENTRY_DSN = "https://a47f010d8d39fb4350f913492189f283@o4511008239452160.ingest.us.sentry.io/4511679448547328";
5416
- const CLI_SENTRY_DSN = "https://82dde4631336561f2fcc89d7531623de@o4511008239452160.ingest.us.sentry.io/4511679411191808";
5417
- /**
5418
- * Dedicated project for errors emitted BY the agent runtime child process
5419
- * (OpenClaw/Hermes) — crashes, spawn failures, and error-looking output. Kept
5420
- * separate from `agent-daemon` so runtime noise never drowns daemon issues.
5421
- * Mirrors `SENTRY_DSNS.agentRuntime` in `config/config.ts`.
5422
- */
5423
- const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
5424
- /**
5425
- * MCP tool/server failures on agent VMs — reported by the daemon-hosted MCP
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.
5431
- */
5432
- const AGENT_MCP_SENTRY_DSN = "https://b4c4b4e19135692206bacabcd12a7fd3@o4511008239452160.ingest.us.sentry.io/4511697094377472";
5433
- /** Surface → Sentry project: `cli` and `agent-daemon` respectively. */
5434
- const SURFACE_DSNS = {
5435
- cli: CLI_SENTRY_DSN,
5436
- daemon: AGENT_DAEMON_SENTRY_DSN
5437
- };
5438
- /** Held after a successful init so the capture helpers can run synchronously. */
5439
- let sentry = null;
5440
- /**
5441
- * Additional bound clients (daemon surface only). The default client keeps the
5442
- * daemon's own DSN; runtime child-process events and MCP failures are routed
5443
- * to their own projects via explicitly-bound Scopes — the documented
5444
- * multi-client pattern. `null` until `initAgentSentry({ surface: "daemon" })`
5445
- * succeeds.
5446
- */
5447
- let runtimeClient = null;
5448
- let runtimeScope = null;
5449
- let mcpClient = null;
5450
- let mcpScope = null;
5451
- /**
5452
- * Map the configured API URL to a coarse environment tag. Best-effort — returns
5453
- * `"unknown"` when no config is present yet (e.g. `alfe login` pre-setup).
5454
- */
5455
- function deriveEnvironment() {
5456
- try {
5457
- const { apiUrl } = resolveConfig();
5458
- if (apiUrl.includes("api.dev.alfe.ai") || apiUrl.includes("dev.alfe.ai")) return "dev";
5459
- if (apiUrl.includes("api.test.alfe.ai")) return "test";
5460
- if (apiUrl.includes("api.demo.alfe.ai")) return "demo";
5461
- if (apiUrl.includes("api.alfe.ai")) return "prod";
5462
- } catch {}
5463
- return "unknown";
5464
- }
5465
- /** Active runtime (`openclaw`/`hermes`) from config, or undefined if unknown. */
5466
- function deriveRuntime() {
5467
- try {
5468
- return resolveConfig().runtime;
5469
- } catch {
5470
- return;
5471
- }
5472
- }
5473
- /** True when the operator has opted out of error reporting. */
5474
- function errorReportingDisabled() {
5475
- const env = process.env.ALFE_ERROR_REPORTING?.trim().toLowerCase();
5476
- if (env === "0" || env === "false") return true;
5477
- try {
5478
- if (readConfig().errorReporting === false) return true;
5479
- } catch {}
5480
- return false;
5481
- }
5482
- const KV_SECRET_RE = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:authorization|api[_-]?key|apikey|token|secret|password|passwd|bearer|credential)s?)(\s*[:=]\s*|\s+)("?)([^\s"']+)\3/gi;
5483
- const URL_QUERY_SECRET_RE = /([?&](?:[A-Za-z0-9_-]*(?:token|secret|key|signature|credential|password)|sig|code|x-amz-[a-z-]+)=)[^&\s"']+/gi;
5484
- const MIN_BARE_SECRET_LEN = 16;
5485
- const ALFE_TOKEN_RE = /alfe_(?:dev|test|demo|live)_[A-Za-z0-9._-]+/g;
5486
- const BEARER_RE = /\bbearer\s+[A-Za-z0-9._\-+/=]+/gi;
5487
- function scrubString(value) {
5488
- return value.replace(BEARER_RE, "Bearer [REDACTED]").replace(URL_QUERY_SECRET_RE, "$1[REDACTED]").replace(KV_SECRET_RE, (match, label, sep, _q, secret) => {
5489
- if (!/[:=]/.test(sep) && secret.length < MIN_BARE_SECRET_LEN) return match;
5490
- return `${label}${sep}[REDACTED]`;
5491
- }).replace(ALFE_TOKEN_RE, "alfe_[REDACTED]");
5492
- }
5493
- /** Scrub secrets from a breadcrumb's message + string data values in place. */
5494
- function scrubBreadcrumb(b) {
5495
- if (typeof b.message === "string") b.message = scrubString(b.message);
5496
- const data = b.data;
5497
- if (data) for (const k of Object.keys(data)) {
5498
- const dv = data[k];
5499
- if (typeof dv === "string") data[k] = scrubString(dv);
5500
- }
5501
- return b;
5502
- }
5503
- /**
5504
- * Scrub secrets and drop request/env payloads before an event is sent.
5505
- * Generic so it preserves the caller's exact event subtype (`beforeSend`
5506
- * receives — and must return — an `ErrorEvent`, not a widened `Event`).
5507
- */
5508
- function scrubEvent(event) {
5509
- delete event.request;
5510
- if (event.contexts) delete event.contexts.runtime;
5511
- delete event.extra;
5512
- delete event.server_name;
5513
- if (typeof event.message === "string") event.message = scrubString(event.message);
5514
- const values = event.exception?.values;
5515
- if (values) {
5516
- for (const v of values) if (typeof v.value === "string") v.value = scrubString(v.value);
5517
- }
5518
- if (event.breadcrumbs) for (const b of event.breadcrumbs) scrubBreadcrumb(b);
5519
- return event;
5520
- }
5521
- /**
5522
- * Initialise Sentry for the agent-side CLI/daemon. Idempotent, best-effort, and
5523
- * a no-op when opted out or when the baked DSN is empty. Never throws.
5524
- */
5525
- async function initAgentSentry(options) {
5526
- try {
5527
- if (sentry) return;
5528
- if (errorReportingDisabled()) return;
5529
- const dsn = SURFACE_DSNS[options.surface].trim();
5530
- if (!dsn) return;
5531
- const mod = await import("@sentry/node");
5532
- if (options.surface === "daemon") {
5533
- const buildBoundScope = (dsn, extraTags) => {
5534
- try {
5535
- if (!dsn.trim()) return null;
5536
- const client = new mod.NodeClient({
5537
- dsn: dsn.trim(),
5538
- environment: deriveEnvironment(),
5539
- ...options.release ? { release: options.release } : {},
5540
- sampleRate: 1,
5541
- tracesSampleRate: 0,
5542
- sendDefaultPii: false,
5543
- transport: mod.makeNodeTransport,
5544
- stackParser: mod.defaultStackParser,
5545
- integrations: [],
5546
- beforeSend: (event) => scrubEvent(event)
5547
- });
5548
- client.init();
5549
- const scope = new mod.Scope();
5550
- scope.setClient(client);
5551
- const runtime = deriveRuntime();
5552
- if (runtime) scope.setTag("runtime", runtime);
5553
- for (const [k, v] of Object.entries(extraTags)) scope.setTag(k, v);
5554
- return {
5555
- client,
5556
- scope
5557
- };
5558
- } catch {
5559
- return null;
5560
- }
5561
- };
5562
- if (!runtimeScope) {
5563
- const bound = buildBoundScope(AGENT_RUNTIME_SENTRY_DSN, {});
5564
- runtimeClient = bound?.client ?? null;
5565
- runtimeScope = bound?.scope ?? null;
5566
- }
5567
- if (!mcpScope) {
5568
- const bound = buildBoundScope(AGENT_MCP_SENTRY_DSN, { source: "agent-daemon" });
5569
- mcpClient = bound?.client ?? null;
5570
- mcpScope = bound?.scope ?? null;
5571
- }
5572
- }
5573
- if (mod.getClient()) {
5574
- sentry = mod;
5575
- return;
5576
- }
5577
- mod.init({
5578
- dsn,
5579
- environment: deriveEnvironment(),
5580
- ...options.release ? { release: options.release } : {},
5581
- sampleRate: 1,
5582
- tracesSampleRate: 0,
5583
- sendDefaultPii: false,
5584
- ...options.surface === "daemon" ? { integrations: (defaults) => defaults.filter((i) => i.name !== "OnUncaughtException" && i.name !== "OnUnhandledRejection") } : {},
5585
- beforeSend: (event) => scrubEvent(event),
5586
- beforeBreadcrumb: (breadcrumb) => scrubBreadcrumb(breadcrumb)
5587
- });
5588
- sentry = mod;
5589
- mod.setTag("surface", options.surface);
5590
- const runtime = deriveRuntime();
5591
- if (runtime) mod.setTag("runtime", runtime);
5592
- } catch {
5593
- sentry = null;
5594
- }
5595
- }
5596
- /**
5597
- * Attach the resolved agent identity to all subsequent events. Called by the
5598
- * daemon once `loadDaemonConfig()` has resolved `agentId`/`runtime`.
5599
- */
5600
- function setAgentContext(context) {
5601
- if (!sentry) return;
5602
- try {
5603
- for (const scope of [
5604
- sentry,
5605
- runtimeScope,
5606
- mcpScope
5607
- ]) {
5608
- if (!scope) continue;
5609
- if (context.agentId) scope.setTag("agentId", context.agentId);
5610
- if (context.runtime) scope.setTag("runtime", context.runtime);
5611
- }
5612
- } catch {}
5613
- }
5614
- /**
5615
- * Capture an integration lifecycle failure (install/activate/reinstall/remove)
5616
- * with `{ integration, phase }` tags. Accepts an `Error` (captured with stack)
5617
- * or a plain message string (captured as an error-level message). No-op when
5618
- * Sentry is not initialised.
5619
- */
5620
- function captureIntegrationFailure(integration, phase, cause) {
5621
- if (!sentry) return;
5622
- try {
5623
- if (cause instanceof Error) sentry.captureException(cause, { tags: {
5624
- integration,
5625
- phase
5626
- } });
5627
- else sentry.captureMessage(String(cause), {
5628
- level: "error",
5629
- tags: {
5630
- integration,
5631
- phase
5632
- }
5633
- });
5634
- } catch {}
5635
- }
5636
- /**
5637
- * Capture a generic CLI failure with a `{ context }` tag. The CLI counterpart
5638
- * to `captureIntegrationFailure` — used by `exitWithError` at the handled-error
5639
- * `process.exit()` sites in `@alfe.ai/cli`'s commands, which otherwise report
5640
- * nothing (the error was caught, never thrown). Accepts an `Error` (captured
5641
- * with stack), a defined non-Error value (captured as an error-level message),
5642
- * or nothing (the `context` string itself becomes the message). No-op when
5643
- * Sentry is not initialised. Never throws.
5644
- */
5645
- function captureCliFailure(context, cause) {
5646
- if (!sentry) return;
5647
- try {
5648
- if (cause instanceof Error) sentry.captureException(cause, { tags: { context } });
5649
- else if (cause === void 0) sentry.captureMessage(context, { level: "error" });
5650
- else sentry.captureMessage(String(cause), {
5651
- level: "error",
5652
- tags: { context }
5653
- });
5654
- } catch {}
5655
- }
5656
- /**
5657
- * Capture a runtime child-process crash (non-graceful exit) or spawn failure
5658
- * into the dedicated `agent-runtime` project. Recent output travels as
5659
- * breadcrumbs — NEVER `extra`, which `scrubEvent` (the runtime client's
5660
- * `beforeSend`) deletes. Throttling is the caller's job (`CaptureThrottle` in
5661
- * `runtime-output-monitor.ts`). No-op when the runtime client is not
5662
- * initialised. Never throws.
5663
- */
5664
- function captureRuntimeCrash(opts) {
5665
- if (!runtimeScope) return;
5666
- try {
5667
- const scope = runtimeScope.clone();
5668
- for (const { stream, line } of opts.recentOutput) scope.addBreadcrumb({
5669
- category: `runtime.${stream}`,
5670
- level: stream === "stderr" ? "warning" : "info",
5671
- message: line
5672
- });
5673
- const crashKey = opts.spawnError ? `spawn:${opts.spawnError.code ?? "error"}` : String(opts.code ?? opts.signal ?? "unknown");
5674
- scope.setFingerprint([
5675
- "runtime-crash",
5676
- opts.runtime,
5677
- crashKey
5678
- ]);
5679
- scope.setTags({
5680
- runtime: opts.runtime,
5681
- exitCode: String(opts.code),
5682
- signal: String(opts.signal),
5683
- crashesSuppressed: String(opts.crashesSuppressed ?? 0)
5684
- });
5685
- if (opts.spawnError) scope.captureException(opts.spawnError);
5686
- else scope.captureMessage(`Runtime ${opts.runtime} crashed (code=${String(opts.code)}, signal=${String(opts.signal)})`, "error");
5687
- } catch {}
5688
- }
5689
- /**
5690
- * Capture an error-looking block of runtime output (stack trace, Python
5691
- * traceback, or ERROR-level log line) detected while the child is running.
5692
- * The block lines travel as breadcrumbs; the head line is the message.
5693
- * Throttling/dedupe is the caller's job. No-op when Sentry is not initialised.
5694
- * Never throws.
5695
- */
5696
- function captureRuntimeErrorOutput(opts) {
5697
- if (!runtimeScope) return;
5698
- try {
5699
- const scope = runtimeScope.clone();
5700
- for (const line of opts.lines) scope.addBreadcrumb({
5701
- category: `runtime.${opts.stream}`,
5702
- level: "error",
5703
- message: line
5704
- });
5705
- scope.setFingerprint([
5706
- "runtime-output",
5707
- opts.runtime,
5708
- opts.kind,
5709
- opts.fingerprintKey
5710
- ]);
5711
- scope.setTags({
5712
- runtime: opts.runtime,
5713
- stream: opts.stream,
5714
- kind: opts.kind,
5715
- suppressedCount: String(opts.suppressedCount ?? 0)
5716
- });
5717
- scope.captureMessage(opts.lines[0].slice(0, 300), "error");
5718
- } catch {}
5719
- }
5720
- /**
5721
- * Capture an MCP failure (tool error, server crash, or error-looking stderr
5722
- * output) into the `mcp` project via the daemon's bound MCP client. Throttling
5723
- * is the caller's job (`mcp-error-capture.ts`). No-op when the MCP client is
5724
- * not initialised. Never throws.
5725
- */
5726
- function captureMcpFailure(opts) {
5727
- if (!mcpScope) return;
5728
- try {
5729
- const scope = mcpScope.clone();
5730
- for (const line of opts.lines ?? []) scope.addBreadcrumb({
5731
- category: `mcp.${opts.server}`,
5732
- level: "error",
5733
- message: line
5734
- });
5735
- scope.setFingerprint([
5736
- "agent-mcp",
5737
- opts.server,
5738
- opts.kind,
5739
- ...opts.tool ? [opts.tool] : []
5740
- ]);
5741
- scope.setTags({
5742
- server: opts.server,
5743
- kind: opts.kind,
5744
- ...opts.tool ? { tool: opts.tool } : {},
5745
- suppressedCount: String(opts.suppressedCount ?? 0)
5746
- });
5747
- scope.captureMessage(`MCP ${opts.server}${opts.tool ? `.${opts.tool}` : ""} ${opts.kind}: ${opts.message.slice(0, 300)}`, "error");
5748
- } catch {}
5749
- }
5750
- /** Flush queued events (small timeout) so short-lived commands deliver them. */
5751
- async function flushSentry(timeoutMs = 2e3) {
5752
- for (const client of [runtimeClient, mcpClient]) try {
5753
- if (client) await client.flush(timeoutMs);
5754
- } catch {}
5755
- if (!sentry) return;
5756
- try {
5757
- await sentry.flush(timeoutMs);
5758
- } catch {}
5759
- }
5760
- /**
5761
- * Capture a fatal error — for the CLI's top-level catch. Does NOT flush: the
5762
- * caller's `finally { await flushSentry() }` delivers it (flushing here too
5763
- * doubled the worst-case exit delay on a failing command).
5764
- */
5765
- function captureFatal(cause) {
5766
- if (!sentry) return;
5767
- try {
5768
- sentry.captureException(cause);
5769
- } catch {}
5770
- }
5771
- //#endregion
5772
5442
  //#region src/reconciliation.ts
5773
- const log$4 = logger$1.child({ component: "Reconciliation" });
5443
+ const log$5 = logger$1.child({ component: "Reconciliation" });
5774
5444
  /**
5775
5445
  * How many times reconcile will re-attempt activation of an intact integration
5776
5446
  * stuck in `error` (with no reinstall requested) before giving up and waiting
@@ -5840,7 +5510,7 @@ var ReconciliationEngine = class {
5840
5510
  try {
5841
5511
  localIntegrations = await this.manager.getInstalledIntegrations();
5842
5512
  } catch (err) {
5843
- log$4.error({ err }, "Failed to get local integrations");
5513
+ log$5.error({ err }, "Failed to get local integrations");
5844
5514
  localIntegrations = [];
5845
5515
  }
5846
5516
  const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
@@ -5859,10 +5529,10 @@ var ReconciliationEngine = class {
5859
5529
  try {
5860
5530
  if (!local) {
5861
5531
  await this.ensureSuspended();
5862
- log$4.info(`Installing ${id}@${desired.version}`);
5532
+ log$5.info(`Installing ${id}@${desired.version}`);
5863
5533
  await this.manager.install(id, desired.version, desired.config, desired.customSource);
5864
5534
  report.installed.push(id);
5865
- log$4.info(`Activating ${id}`);
5535
+ log$5.info(`Activating ${id}`);
5866
5536
  if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
5867
5537
  report.activated.push(id);
5868
5538
  report.results.push({
@@ -5875,7 +5545,7 @@ var ReconciliationEngine = class {
5875
5545
  if (local.version !== desired.version && desired.version !== "" && local.version !== "unknown") {
5876
5546
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5877
5547
  await this.ensureSuspended();
5878
- log$4.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
5548
+ log$5.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
5879
5549
  this.manager.resetReinstallAttempts(id);
5880
5550
  this.activateAttempts.delete(id);
5881
5551
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5888,7 +5558,7 @@ var ReconciliationEngine = class {
5888
5558
  });
5889
5559
  return;
5890
5560
  }
5891
- log$4.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
5561
+ log$5.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
5892
5562
  try {
5893
5563
  if ((await this.manager.upgrade(id, desired.version, desired.config, desired.customSource, { onBeforeRuntimeMutation: () => this.ensureSuspended() })).configApplied) report.configApplied = true;
5894
5564
  this.manager.resetReinstallAttempts(id);
@@ -5902,7 +5572,7 @@ var ReconciliationEngine = class {
5902
5572
  });
5903
5573
  } catch (upgradeErr) {
5904
5574
  const upgradeMsg = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
5905
- log$4.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
5575
+ log$5.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
5906
5576
  captureIntegrationFailure(id, "upgrade", upgradeErr);
5907
5577
  report.errors.push({
5908
5578
  integrationId: id,
@@ -5920,7 +5590,7 @@ var ReconciliationEngine = class {
5920
5590
  if (local.status === "error") {
5921
5591
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
5922
5592
  await this.ensureSuspended();
5923
- log$4.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5593
+ log$5.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5924
5594
  this.manager.resetReinstallAttempts(id);
5925
5595
  this.activateAttempts.delete(id);
5926
5596
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5936,7 +5606,7 @@ var ReconciliationEngine = class {
5936
5606
  if (!await this.manager.isInstallIntact(id)) {
5937
5607
  const attempts = this.manager.getReinstallAttempts(id);
5938
5608
  if (attempts >= 3) {
5939
- log$4.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5609
+ log$5.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
5940
5610
  captureIntegrationFailure(id, "reinstall", `Max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`);
5941
5611
  report.errors.push({
5942
5612
  integrationId: id,
@@ -5951,7 +5621,7 @@ var ReconciliationEngine = class {
5951
5621
  return;
5952
5622
  }
5953
5623
  await this.ensureSuspended();
5954
- log$4.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5624
+ log$5.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
5955
5625
  this.manager.incrementReinstallAttempts(id);
5956
5626
  try {
5957
5627
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -5966,7 +5636,7 @@ var ReconciliationEngine = class {
5966
5636
  });
5967
5637
  } catch (reinstallErr) {
5968
5638
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
5969
- log$4.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5639
+ log$5.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
5970
5640
  captureIntegrationFailure(id, "reinstall", reinstallErr);
5971
5641
  report.errors.push({
5972
5642
  integrationId: id,
@@ -5985,7 +5655,7 @@ var ReconciliationEngine = class {
5985
5655
  if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
5986
5656
  this.activateAttempts.set(id, activateAttempts + 1);
5987
5657
  await this.ensureSuspended();
5988
- log$4.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5658
+ log$5.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
5989
5659
  try {
5990
5660
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
5991
5661
  this.activateAttempts.delete(id);
@@ -5998,7 +5668,7 @@ var ReconciliationEngine = class {
5998
5668
  return;
5999
5669
  } catch (reactivateErr) {
6000
5670
  const reactivateMsg = reactivateErr instanceof Error ? reactivateErr.message : String(reactivateErr);
6001
- log$4.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
5671
+ log$5.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
6002
5672
  captureIntegrationFailure(id, "reactivate", reactivateErr);
6003
5673
  report.errors.push({
6004
5674
  integrationId: id,
@@ -6013,7 +5683,7 @@ var ReconciliationEngine = class {
6013
5683
  return;
6014
5684
  }
6015
5685
  }
6016
- log$4.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
5686
+ log$5.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
6017
5687
  captureIntegrationFailure(id, "reactivate", `Integration ${id} stuck in error state — re-activation attempts exhausted`);
6018
5688
  report.errors.push({
6019
5689
  integrationId: id,
@@ -6030,7 +5700,7 @@ var ReconciliationEngine = class {
6030
5700
  if (local.status === "installing") {
6031
5701
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6032
5702
  await this.ensureSuspended();
6033
- log$4.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5703
+ log$5.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6034
5704
  this.manager.resetReinstallAttempts(id);
6035
5705
  this.activateAttempts.delete(id);
6036
5706
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6046,7 +5716,7 @@ var ReconciliationEngine = class {
6046
5716
  const attempts = this.manager.getReinstallAttempts(id);
6047
5717
  if (attempts >= 3) {
6048
5718
  const errorMessage = `Stale installing state — max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`;
6049
- log$4.error(`Auto-reinstall blocked for ${id} — stuck in "installing", ${String(attempts)} consecutive failures. Manual reinstall required.`);
5719
+ log$5.error(`Auto-reinstall blocked for ${id} — stuck in "installing", ${String(attempts)} consecutive failures. Manual reinstall required.`);
6050
5720
  captureIntegrationFailure(id, "reinstall", errorMessage);
6051
5721
  report.errors.push({
6052
5722
  integrationId: id,
@@ -6061,7 +5731,7 @@ var ReconciliationEngine = class {
6061
5731
  return;
6062
5732
  }
6063
5733
  await this.ensureSuspended();
6064
- log$4.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
5734
+ log$5.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
6065
5735
  this.manager.incrementReinstallAttempts(id);
6066
5736
  try {
6067
5737
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6076,7 +5746,7 @@ var ReconciliationEngine = class {
6076
5746
  });
6077
5747
  } catch (reinstallErr) {
6078
5748
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
6079
- log$4.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
5749
+ log$5.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
6080
5750
  captureIntegrationFailure(id, "reinstall", reinstallErr);
6081
5751
  report.errors.push({
6082
5752
  integrationId: id,
@@ -6093,7 +5763,7 @@ var ReconciliationEngine = class {
6093
5763
  }
6094
5764
  if (local.status !== "active") {
6095
5765
  await this.ensureSuspended();
6096
- log$4.info(`Activating ${id}`);
5766
+ log$5.info(`Activating ${id}`);
6097
5767
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
6098
5768
  report.activated.push(id);
6099
5769
  report.results.push({
@@ -6105,7 +5775,7 @@ var ReconciliationEngine = class {
6105
5775
  }
6106
5776
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6107
5777
  await this.ensureSuspended();
6108
- log$4.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
5778
+ log$5.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6109
5779
  this.manager.resetReinstallAttempts(id);
6110
5780
  this.activateAttempts.delete(id);
6111
5781
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6126,7 +5796,7 @@ var ReconciliationEngine = class {
6126
5796
  });
6127
5797
  } catch (err) {
6128
5798
  const message = err instanceof Error ? err.message : String(err);
6129
- log$4.error({ err }, `Error reconciling ${id}`);
5799
+ log$5.error({ err }, `Error reconciling ${id}`);
6130
5800
  captureIntegrationFailure(id, "reconcile_active", err);
6131
5801
  report.errors.push({
6132
5802
  integrationId: id,
@@ -6151,7 +5821,7 @@ var ReconciliationEngine = class {
6151
5821
  }
6152
5822
  try {
6153
5823
  await this.ensureSuspended();
6154
- log$4.info(`Removing ${id}`);
5824
+ log$5.info(`Removing ${id}`);
6155
5825
  if (local.status === "active") {
6156
5826
  await this.manager.deactivate(id);
6157
5827
  report.deactivated.push(id);
@@ -6165,7 +5835,7 @@ var ReconciliationEngine = class {
6165
5835
  });
6166
5836
  } catch (err) {
6167
5837
  const message = err instanceof Error ? err.message : String(err);
6168
- log$4.error({ err }, `Error removing ${id}`);
5838
+ log$5.error({ err }, `Error removing ${id}`);
6169
5839
  captureIntegrationFailure(id, "reconcile_removed", err);
6170
5840
  report.errors.push({
6171
5841
  integrationId: id,
@@ -6548,7 +6218,7 @@ var CloudClient = class {
6548
6218
  };
6549
6219
  //#endregion
6550
6220
  //#region src/config-reconciler.ts
6551
- const log$3 = logger$1.child({ component: "ConfigReconciler" });
6221
+ const log$4 = logger$1.child({ component: "ConfigReconciler" });
6552
6222
  const DEFAULT_VERIFY_RETRIES = 3;
6553
6223
  const DEFAULT_VERIFY_RETRY_DELAY_MS = 750;
6554
6224
  const delay = (ms) => new Promise((resolve) => {
@@ -6570,7 +6240,7 @@ var ConfigReconciler = class {
6570
6240
  async reconcile(desired) {
6571
6241
  const applier = this.getApplier();
6572
6242
  if (!applier?.setConfigRaw) {
6573
- log$3.debug("No applier with setConfigRaw — skipping config reconcile");
6243
+ log$4.debug("No applier with setConfigRaw — skipping config reconcile");
6574
6244
  return null;
6575
6245
  }
6576
6246
  const setConfigRaw = applier.setConfigRaw.bind(applier);
@@ -6584,7 +6254,7 @@ var ConfigReconciler = class {
6584
6254
  for (const [key, want] of entries) try {
6585
6255
  if (getConfigRaw) {
6586
6256
  if (await getConfigRaw(key) === want) {
6587
- log$3.debug({ key }, "Config already at desired value — no-op");
6257
+ log$4.debug({ key }, "Config already at desired value — no-op");
6588
6258
  continue;
6589
6259
  }
6590
6260
  }
@@ -6594,12 +6264,19 @@ var ConfigReconciler = class {
6594
6264
  }
6595
6265
  } catch (err) {
6596
6266
  const msg = err instanceof Error ? err.message : String(err);
6267
+ if (getConfigRaw && await this.verify(getConfigRaw, key, want)) {
6268
+ log$4.warn({
6269
+ key,
6270
+ err: msg
6271
+ }, "config set errored but value landed — treating as applied");
6272
+ continue;
6273
+ }
6597
6274
  failures.push(`${key}: ${msg}`);
6598
6275
  }
6599
6276
  if (failures.length > 0) {
6600
6277
  const joined = failures.join("; ");
6601
6278
  const reason = joined.length > 480 ? `${joined.slice(0, 480)}… (truncated)` : joined;
6602
- log$3.warn({
6279
+ log$4.warn({
6603
6280
  version: desired.version,
6604
6281
  reason
6605
6282
  }, "Config reconcile failed");
@@ -6609,7 +6286,7 @@ var ConfigReconciler = class {
6609
6286
  reason
6610
6287
  };
6611
6288
  }
6612
- log$3.info({
6289
+ log$4.info({
6613
6290
  version: desired.version,
6614
6291
  keys: entries.length
6615
6292
  }, "Config reconcile applied");
@@ -7080,6 +6757,14 @@ var CommandQueue = class {
7080
6757
  */
7081
6758
  const LAUNCHD_LABEL = "ai.alfe.gateway";
7082
6759
  const SYSTEMD_SERVICE = "alfe-gateway";
6760
+ /**
6761
+ * On-disk path for the boot-time self-heal guard script (Linux only).
6762
+ * Written next to the systemd unit at setup time and invoked via
6763
+ * `ExecStartPre=` — see `writeGuardScript` and `generateSystemdUnit`.
6764
+ */
6765
+ function getGuardScriptPath() {
6766
+ return isRootUser() ? "/usr/local/lib/alfe/alfe-cli-guard.sh" : join(homedir(), ".alfe", "alfe-cli-guard.sh");
6767
+ }
7083
6768
  function isRootUser() {
7084
6769
  return process.getuid?.() === 0;
7085
6770
  }
@@ -7155,16 +6840,108 @@ function generateLaunchdPlist() {
7155
6840
  </plist>`;
7156
6841
  }
7157
6842
  /**
6843
+ * Boot-time self-heal guard (Linux only).
6844
+ *
6845
+ * Runs as an `ExecStartPre=` BEFORE the daemon launches, OUTSIDE the
6846
+ * (possibly-broken) `alfe` binary. It exists for the case defense #1
6847
+ * (verify-before-exit in `upgrade.ts`) structurally cannot cover: the daemon
6848
+ * is KILLED during `npm install -g @alfe.ai/cli` (reboot / OOM), leaving the
6849
+ * package with deps but no `dist/` and a dangling `/usr/bin/alfe` symlink.
6850
+ * systemd then execs a dangling binary forever → 203/EXEC crash-loop, a
6851
+ * permanent brick with no automatic recovery (real prod incident,
6852
+ * ~13,570 restarts over 2 days).
6853
+ *
6854
+ * The guard:
6855
+ * - checks the CLI is intact: the symlink target (`dist/index.js`) exists AND
6856
+ * `alfe --version` exits 0;
6857
+ * - if broken, reinstalls `@alfe.ai/cli@$ALFE_CLI_VERSION` (the exact version
6858
+ * baked into the unit's `Environment=`; falls back to `@latest` when
6859
+ * unset), so the daemon then execs a working binary;
6860
+ * - is idempotent + fast (no-op when healthy) and NEVER wedges boot: any of
6861
+ * its own failures log + `exit 0` so a human can still SSH in.
6862
+ *
6863
+ * POSIX sh (dash-safe): the box may not have bash. Keep the two intactness
6864
+ * checks in lockstep with `verifyCliInstall` in `verify-cli-install.ts`.
6865
+ */
6866
+ /** @internal exported for unit tests; not re-exported from the barrel. */
6867
+ function generateGuardScript() {
6868
+ const version = process.env.ALFE_CLI_VERSION;
6869
+ return `#!/bin/sh
6870
+ # Alfe CLI boot-time self-heal guard. Auto-generated by 'alfe setup' — do not edit.
6871
+ # Repairs an interrupted 'npm install -g @alfe.ai/cli' before the daemon starts,
6872
+ # so a mid-install reboot can't brick the agent with a 203/EXEC crash-loop.
6873
+ # Must never wedge boot: every failure path logs and exits 0.
6874
+ set -u
6875
+
6876
+ TARGET='${version && version.length > 0 ? `@alfe.ai/cli@${version}` : "@alfe.ai/cli@latest"}'
6877
+ log() { echo "[alfe-cli-guard] $*" >&2; }
6878
+
6879
+ # Resolve the alfe bin and its real target (dist/index.js). readlink -f follows
6880
+ # the whole symlink chain; fall back to the bin path itself if unavailable.
6881
+ BIN="$(command -v alfe 2>/dev/null || true)"
6882
+ if [ -n "$BIN" ]; then
6883
+ ENTRY="$(readlink -f "$BIN" 2>/dev/null || echo "$BIN")"
6884
+ else
6885
+ ENTRY=""
6886
+ fi
6887
+
6888
+ healthy=1
6889
+ if [ -z "$ENTRY" ] || [ ! -f "$ENTRY" ]; then
6890
+ healthy=0
6891
+ log "CLI entry missing (bin=\${BIN:-<none>} entry=\${ENTRY:-<none>})"
6892
+ elif ! alfe --version >/dev/null 2>&1; then
6893
+ healthy=0
6894
+ log "alfe --version failed"
6895
+ fi
6896
+
6897
+ if [ "$healthy" -eq 1 ]; then
6898
+ exit 0
6899
+ fi
6900
+
6901
+ log "CLI install looks broken — reinstalling $TARGET"
6902
+ # Blow away a partial tree first so npm re-materialises dist/ cleanly. Best-effort.
6903
+ GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
6904
+ if [ -n "$GLOBAL_ROOT" ] && [ -d "$GLOBAL_ROOT/@alfe.ai/cli" ]; then
6905
+ rm -rf "$GLOBAL_ROOT/@alfe.ai/cli" 2>/dev/null || true
6906
+ fi
6907
+
6908
+ # Bound the reinstall so a slow/unreachable registry at boot can't stall the
6909
+ # ExecStartPre up to systemd's DefaultTimeoutStartSec (~90s). Two layers:
6910
+ # - npm --fetch-timeout/--fetch-retries cap per-request/hung-socket time;
6911
+ # - a wall-clock 'timeout' wrapper caps the whole install when present
6912
+ # ('timeout' isn't guaranteed, so fall back to a bare install).
6913
+ # stdout->stderr ('>&2') so npm's chatter lands in the journal, not on fd1.
6914
+ if command -v timeout >/dev/null 2>&1; then
6915
+ timeout 120 npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
6916
+ rc=$?
6917
+ else
6918
+ npm install -g "$TARGET" --fetch-timeout=60000 --fetch-retries=2 >&2
6919
+ rc=$?
6920
+ fi
6921
+ if [ "$rc" -eq 0 ]; then
6922
+ log "reinstall succeeded"
6923
+ else
6924
+ log "reinstall FAILED (rc=$rc) — daemon start may still fail; a human can SSH in to repair"
6925
+ fi
6926
+
6927
+ # Always exit 0: even a failed repair must not block the unit from attempting
6928
+ # ExecStart (which then fails visibly) rather than wedging silently in pre-start.
6929
+ exit 0
6930
+ `;
6931
+ }
6932
+ /**
7158
6933
  * Generate a systemd unit for Linux.
7159
6934
  * Root users get a system-level unit; non-root get a user-level unit.
7160
6935
  */
6936
+ /** @internal exported for unit tests; not re-exported from the barrel. */
7161
6937
  function generateSystemdUnit() {
7162
6938
  const alfeBin = getAlfeBinPath();
7163
6939
  const root = isRootUser();
7164
6940
  const envLines = [
7165
6941
  "ALFE_MANAGED",
7166
6942
  "ALFE_API_KEY",
7167
- "LOG_LEVEL"
6943
+ "LOG_LEVEL",
6944
+ "ALFE_CLI_VERSION"
7168
6945
  ].filter((key) => process.env[key]).map((key) => `Environment=${key}=${process.env[key] ?? ""}`).join("\n");
7169
6946
  return `[Unit]
7170
6947
  Description=Alfe Gateway Daemon
@@ -7173,6 +6950,7 @@ Wants=network-online.target
7173
6950
 
7174
6951
  [Service]
7175
6952
  Type=simple
6953
+ ExecStartPre=-/bin/sh ${getGuardScriptPath()}
7176
6954
  ExecStart=${alfeBin} gateway daemon
7177
6955
  Restart=always
7178
6956
  RestartSec=10
@@ -7189,6 +6967,19 @@ ${envLines}
7189
6967
  WantedBy=${root ? "multi-user.target" : "default.target"}`;
7190
6968
  }
7191
6969
  /**
6970
+ * Write the boot-time self-heal guard script to disk (Linux only) and make it
6971
+ * executable. Called from `installSystemd` before the unit is written.
6972
+ */
6973
+ async function writeGuardScript() {
6974
+ const guardPath = getGuardScriptPath();
6975
+ await mkdir(dirname(guardPath), { recursive: true });
6976
+ await writeFile(guardPath, generateGuardScript(), {
6977
+ encoding: "utf-8",
6978
+ mode: 493
6979
+ });
6980
+ logger$1.info({ path: guardPath }, "Wrote CLI self-heal guard script");
6981
+ }
6982
+ /**
7192
6983
  * Install the service unit for the current platform.
7193
6984
  */
7194
6985
  async function installService() {
@@ -7250,6 +7041,7 @@ async function installSystemd() {
7250
7041
  const dir = root ? "/etc/systemd/system" : join(homedir(), ".config", "systemd", "user");
7251
7042
  const ctl = root ? "systemctl" : "systemctl --user";
7252
7043
  if (!root) await mkdir(dir, { recursive: true });
7044
+ await writeGuardScript();
7253
7045
  await writeFile(unitPath, generateSystemdUnit(), "utf-8");
7254
7046
  logger$1.info({
7255
7047
  path: unitPath,
@@ -7272,6 +7064,9 @@ async function uninstallSystemd() {
7272
7064
  try {
7273
7065
  await unlink(unitPath);
7274
7066
  } catch {}
7067
+ try {
7068
+ await unlink(getGuardScriptPath());
7069
+ } catch {}
7275
7070
  try {
7276
7071
  execSync(`${ctl} daemon-reload`, { stdio: "pipe" });
7277
7072
  } catch {}
@@ -22585,13 +22380,21 @@ var CaptureThrottle = class {
22585
22380
  * Spawns the runtime binary, pipes stdout/stderr to the daemon logger,
22586
22381
  * and restarts on crash with exponential backoff.
22587
22382
  */
22588
- const log$2 = createLogger("RuntimeProcess");
22383
+ const log$3 = createLogger("RuntimeProcess");
22589
22384
  /** Quiet-period flush for a pending multi-line error block (e.g. a traceback
22590
22385
  * followed by silence — no closing line ever arrives to complete it). */
22591
22386
  const DETECTOR_FLUSH_DEBOUNCE_MS = 1500;
22592
22387
  const BACKOFF_INITIAL_MS = 1e3;
22593
22388
  const BACKOFF_MAX_MS = 3e4;
22594
22389
  const STABLE_UPTIME_MS = 6e4;
22390
+ /**
22391
+ * Max time a NON-crash restart waits for an in-flight chat turn to finish
22392
+ * before it forces the stop anyway. A turn that outlives this is almost
22393
+ * certainly wedged; a real reply streams in far less. Long enough to cover a
22394
+ * normal multi-tool turn, short enough that a stuck turn doesn't block a
22395
+ * platform-pushed `daemon.update` indefinitely.
22396
+ */
22397
+ const DRAIN_GRACE_MS = 45e3;
22595
22398
  var RuntimeProcess = class {
22596
22399
  child = null;
22597
22400
  stopped = false;
@@ -22602,10 +22405,20 @@ var RuntimeProcess = class {
22602
22405
  detector = new ErrorLineDetector();
22603
22406
  throttle = new CaptureThrottle(STABLE_UPTIME_MS);
22604
22407
  detectorFlushTimer = null;
22408
+ turnActivityProbe = null;
22605
22409
  constructor(options) {
22606
22410
  this.options = options;
22607
22411
  }
22608
22412
  /**
22413
+ * Attach the turn-activity probe used by drained restarts. The daemon calls
22414
+ * this after the IPC server is up (plugins register there). Without it,
22415
+ * {@link stopWhenIdle} and drained {@link restart} fall back to an immediate
22416
+ * stop — safe, just not turn-aware.
22417
+ */
22418
+ setTurnActivityProbe(probe) {
22419
+ this.turnActivityProbe = probe;
22420
+ }
22421
+ /**
22609
22422
  * Resolve the binary command and args for a given runtime type.
22610
22423
  */
22611
22424
  resolveCommand() {
@@ -22622,6 +22435,10 @@ var RuntimeProcess = class {
22622
22435
  command: "hermes",
22623
22436
  args: ["gateway", "run"]
22624
22437
  };
22438
+ case "claude-code": return {
22439
+ command: "alfe-claude-host",
22440
+ args: ["run"]
22441
+ };
22625
22442
  default: throw new Error(`Unsupported runtime: ${this.options.runtime}`);
22626
22443
  }
22627
22444
  }
@@ -22632,7 +22449,7 @@ var RuntimeProcess = class {
22632
22449
  if (this.stopped) return;
22633
22450
  const { command, args } = this.resolveCommand();
22634
22451
  this.lastStartTime = Date.now();
22635
- log$2.info({
22452
+ log$3.info({
22636
22453
  runtime: this.options.runtime,
22637
22454
  command,
22638
22455
  args,
@@ -22653,7 +22470,7 @@ var RuntimeProcess = class {
22653
22470
  this.child.stdout?.on("data", (data) => {
22654
22471
  const lines = data.toString().trim().split("\n");
22655
22472
  for (const line of lines) {
22656
- log$2.info({
22473
+ log$3.info({
22657
22474
  runtime: this.options.runtime,
22658
22475
  stream: "stdout"
22659
22476
  }, line);
@@ -22663,7 +22480,7 @@ var RuntimeProcess = class {
22663
22480
  this.child.stderr?.on("data", (data) => {
22664
22481
  const lines = data.toString().trim().split("\n");
22665
22482
  for (const line of lines) {
22666
- log$2.warn({
22483
+ log$3.warn({
22667
22484
  runtime: this.options.runtime,
22668
22485
  stream: "stderr"
22669
22486
  }, line);
@@ -22673,7 +22490,7 @@ var RuntimeProcess = class {
22673
22490
  this.child.on("exit", (code, signal) => {
22674
22491
  this.child = null;
22675
22492
  if (this.stopped) {
22676
- log$2.info({
22493
+ log$3.info({
22677
22494
  runtime: this.options.runtime,
22678
22495
  code,
22679
22496
  signal
@@ -22681,7 +22498,7 @@ var RuntimeProcess = class {
22681
22498
  return;
22682
22499
  }
22683
22500
  if (code === 0 && signal == null) {
22684
- log$2.info({
22501
+ log$3.info({
22685
22502
  runtime: this.options.runtime,
22686
22503
  code
22687
22504
  }, "Runtime exited gracefully — restarting");
@@ -22691,7 +22508,7 @@ var RuntimeProcess = class {
22691
22508
  }, 500);
22692
22509
  return;
22693
22510
  }
22694
- log$2.warn({
22511
+ log$3.warn({
22695
22512
  runtime: this.options.runtime,
22696
22513
  code,
22697
22514
  signal,
@@ -22712,7 +22529,7 @@ var RuntimeProcess = class {
22712
22529
  recentOutput: this.ringBuffer.snapshot(),
22713
22530
  crashesSuppressed: crash.suppressedCount
22714
22531
  });
22715
- else log$2.debug({
22532
+ else log$3.debug({
22716
22533
  runtime: this.options.runtime,
22717
22534
  suppressed: crash.suppressedCount
22718
22535
  }, "Crash capture suppressed by throttle");
@@ -22724,7 +22541,7 @@ var RuntimeProcess = class {
22724
22541
  this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
22725
22542
  });
22726
22543
  this.child.on("error", (err) => {
22727
- log$2.error({
22544
+ log$3.error({
22728
22545
  runtime: this.options.runtime,
22729
22546
  err: err.message
22730
22547
  }, "Runtime process error");
@@ -22763,7 +22580,7 @@ var RuntimeProcess = class {
22763
22580
  this.emitErrorBlock(this.detector.flush());
22764
22581
  }, DETECTOR_FLUSH_DEBOUNCE_MS);
22765
22582
  } catch (err) {
22766
- log$2.debug({
22583
+ log$3.debug({
22767
22584
  runtime: this.options.runtime,
22768
22585
  err: err instanceof Error ? err.message : String(err)
22769
22586
  }, "Runtime output observation failed");
@@ -22774,7 +22591,7 @@ var RuntimeProcess = class {
22774
22591
  if (!block) return;
22775
22592
  const { allow, suppressedCount } = this.throttle.allowErrorCapture(block.fingerprintKey);
22776
22593
  if (!allow) {
22777
- log$2.debug({
22594
+ log$3.debug({
22778
22595
  runtime: this.options.runtime,
22779
22596
  kind: block.kind,
22780
22597
  suppressed: suppressedCount
@@ -22808,7 +22625,7 @@ var RuntimeProcess = class {
22808
22625
  if (!child) return;
22809
22626
  return new Promise((resolve) => {
22810
22627
  const killTimer = setTimeout(() => {
22811
- log$2.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
22628
+ log$3.warn({ runtime: this.options.runtime }, "Runtime did not exit in time — sending SIGKILL");
22812
22629
  child.kill("SIGKILL");
22813
22630
  }, 5e3);
22814
22631
  child.on("exit", () => {
@@ -22819,6 +22636,54 @@ var RuntimeProcess = class {
22819
22636
  });
22820
22637
  }
22821
22638
  /**
22639
+ * Stop the runtime, but first wait for any in-flight chat turn to finish
22640
+ * (up to a grace deadline). This is the entry point for NON-crash restarts —
22641
+ * a mid-turn SIGTERM turns a healthy reply into an "unknown error" for the
22642
+ * user, so a planned stop drains first.
22643
+ *
22644
+ * Semantics:
22645
+ * - No probe attached, or probe reports idle up-front → stops immediately
22646
+ * (no added latency for the common case).
22647
+ * - A turn is in flight → polls the probe every {@link DRAIN_POLL_INTERVAL_MS}
22648
+ * until it reports idle, then stops.
22649
+ * - The turn never ends within `graceMs` → logs and forces the stop anyway
22650
+ * (a wedged turn must not block a platform-pushed restart forever).
22651
+ *
22652
+ * Crash restarts must NOT call this — the child is already dead/broken and
22653
+ * the probe would just burn the grace window. They keep using {@link stop}.
22654
+ */
22655
+ async stopWhenIdle(opts) {
22656
+ const probe = opts?.probe ?? this.turnActivityProbe;
22657
+ const graceMs = opts?.graceMs ?? 45e3;
22658
+ if (!this.child || !probe) {
22659
+ await this.stop();
22660
+ return;
22661
+ }
22662
+ const deadline = Date.now() + graceMs;
22663
+ let deferred = false;
22664
+ for (;;) {
22665
+ if (!await probe.isTurnActive()) break;
22666
+ if (Date.now() >= deadline) {
22667
+ log$3.warn({
22668
+ runtime: this.options.runtime,
22669
+ graceMs
22670
+ }, "Runtime still mid-turn at drain grace deadline — forcing stop");
22671
+ break;
22672
+ }
22673
+ if (!deferred) {
22674
+ deferred = true;
22675
+ log$3.info({
22676
+ runtime: this.options.runtime,
22677
+ graceMs
22678
+ }, "Runtime is mid-turn — deferring planned stop until idle");
22679
+ }
22680
+ await new Promise((resolve) => setTimeout(resolve, 500));
22681
+ if (!this.isRunning) return;
22682
+ }
22683
+ if (deferred) log$3.info({ runtime: this.options.runtime }, "Runtime idle — proceeding with planned stop");
22684
+ await this.stop();
22685
+ }
22686
+ /**
22822
22687
  * Resume after a suspend (stop). `start()` alone is a no-op while stopped
22823
22688
  * because `stop()` latches `this.stopped = true`; resume clears that latch,
22824
22689
  * resets the crash backoff, and re-spawns. Used by the RuntimeGate to bring
@@ -22831,10 +22696,20 @@ var RuntimeProcess = class {
22831
22696
  }
22832
22697
  /**
22833
22698
  * Restart the runtime process (stop then start with fresh backoff).
22699
+ *
22700
+ * This is a PLANNED restart (`runtime.restart`, post-upgrade cycle, gate
22701
+ * request), so by default it drains any in-flight chat turn before stopping
22702
+ * — see {@link stopWhenIdle}. Pass `{ drain: false }` to force an immediate
22703
+ * stop (e.g. when the caller has already established the child is unhealthy).
22834
22704
  */
22835
- async restart() {
22836
- log$2.info({ runtime: this.options.runtime }, "Restarting runtime...");
22837
- await this.stop();
22705
+ async restart(opts) {
22706
+ const drain = opts?.drain ?? true;
22707
+ log$3.info({
22708
+ runtime: this.options.runtime,
22709
+ drain
22710
+ }, "Restarting runtime...");
22711
+ if (drain) await this.stopWhenIdle();
22712
+ else await this.stop();
22838
22713
  this.stopped = false;
22839
22714
  this.backoffMs = BACKOFF_INITIAL_MS;
22840
22715
  this.start();
@@ -22847,6 +22722,78 @@ var RuntimeProcess = class {
22847
22722
  }
22848
22723
  };
22849
22724
  //#endregion
22725
+ //#region src/turn-activity.ts
22726
+ /**
22727
+ * Turn-activity probe — lets a NON-crash runtime restart wait for the runtime
22728
+ * to finish any in-flight chat turn before the child is SIGTERM'd.
22729
+ *
22730
+ * ## Why this exists
22731
+ *
22732
+ * Every non-crash restart path (`runtime.restart`, `daemon.update`,
22733
+ * `daemon.restart`, the RuntimeGate resume after a mutating reconcile) used to
22734
+ * route straight into {@link RuntimeProcess.stop}, which is SIGTERM →
22735
+ * SIGKILL-after-5s with zero turn-awareness. A platform-pushed `daemon.update`
22736
+ * that landed mid-turn killed the runtime child while it was streaming a reply,
22737
+ * and the user saw an "unknown error" for a turn that was otherwise healthy.
22738
+ *
22739
+ * The fix is a small "is a turn active?" signal that the daemon can consult
22740
+ * before a *planned* stop, deferring the SIGTERM until the runtime reports idle
22741
+ * (or a grace deadline elapses). Crash restarts do NOT consult it — the child is
22742
+ * already dead or wedged, so there is nothing to drain.
22743
+ *
22744
+ * ## The runtime-side contract (the boundary)
22745
+ *
22746
+ * The runtime's activity is a property of the chat plugin's active-turn state,
22747
+ * which lives INSIDE the runtime child (e.g. `openclaw-chat`'s per-turn run
22748
+ * gate). The daemon cannot see it directly. The daemon reaches it over the same
22749
+ * IPC socket the plugin already uses to register:
22750
+ *
22751
+ * daemon → plugin request: { method: 'runtime.activity' }
22752
+ * plugin → daemon response: { ok: true, payload: { active: boolean } }
22753
+ *
22754
+ * `runtime.activity` is on the daemon→plugin method allowlist (see
22755
+ * protocol.ts). A plugin answers `active: true` while it is dispatching a turn
22756
+ * and `active: false` otherwise. This is the ONE thing the runtime must provide;
22757
+ * everything else in the drain lives on the gateway side.
22758
+ *
22759
+ * ## Fail-open to idle
22760
+ *
22761
+ * If NO connected plugin answers `runtime.activity` (older plugin build, the
22762
+ * chat plugin isn't loaded, the request times out), the probe reports **idle**.
22763
+ * A planned restart must never hang forever waiting on a signal that will never
22764
+ * arrive — the grace deadline is a backstop, but fail-open-to-idle means the
22765
+ * common "nothing to drain" case restarts immediately with no added latency.
22766
+ */
22767
+ const log$2 = logger$1.child({ component: "TurnActivity" });
22768
+ /** The IPC method the daemon calls on plugins to ask "is a turn active?". */
22769
+ const RUNTIME_ACTIVITY_METHOD = "runtime.activity";
22770
+ /**
22771
+ * IPC-backed probe. Broadcasts `runtime.activity` to every registered plugin
22772
+ * and reports active if ANY plugin claims an in-flight turn. Fails open to idle
22773
+ * on any error / timeout / non-answer so a planned restart is never wedged.
22774
+ */
22775
+ var IpcTurnActivityProbe = class {
22776
+ constructor(getIpcServer, probeTimeoutMs = 2e3) {
22777
+ this.getIpcServer = getIpcServer;
22778
+ this.probeTimeoutMs = probeTimeoutMs;
22779
+ }
22780
+ async isTurnActive() {
22781
+ const ipc = this.getIpcServer();
22782
+ if (!ipc || ipc.registeredCount === 0) return false;
22783
+ try {
22784
+ const responses = await ipc.broadcastRequest(RUNTIME_ACTIVITY_METHOD, {}, this.probeTimeoutMs);
22785
+ for (const [pluginId, res] of responses) if (res.ok && res.payload != null && typeof res.payload === "object" && res.payload.active === true) {
22786
+ log$2.debug({ pluginId }, "Plugin reports an in-flight turn");
22787
+ return true;
22788
+ }
22789
+ return false;
22790
+ } catch (err) {
22791
+ log$2.debug({ err: err instanceof Error ? err.message : String(err) }, "Turn-activity probe failed — assuming idle");
22792
+ return false;
22793
+ }
22794
+ }
22795
+ };
22796
+ //#endregion
22850
22797
  //#region src/command-registry.ts
22851
22798
  /**
22852
22799
  * Command Registry — maps integration command names to local handler files.
@@ -23475,8 +23422,16 @@ let mcpManagerRef = null;
23475
23422
  * and for managed/self-hosted runtimes other than hermes.
23476
23423
  */
23477
23424
  let hermesMcpSync = null;
23425
+ /**
23426
+ * claude-code-only MCP store consumer. Mirrors the runtime-agnostic MCP store
23427
+ * into the host's `--mcp-config` JSON (~/.claude-code/mcp-config.json), injecting
23428
+ * ALFE_API_KEY into every stdio server's env. Null for every other runtime
23429
+ * (never constructed) — same shape/role as {@link hermesMcpSync}.
23430
+ */
23431
+ let claudeCodeMcpSync = null;
23478
23432
  let aiProxyServer = null;
23479
23433
  let runtimeProcess = null;
23434
+ let turnActivityProbe = null;
23480
23435
  let aiProxyUrl = null;
23481
23436
  let aiProxyRunning = false;
23482
23437
  let cloudConnected = false;
@@ -23587,6 +23542,10 @@ const RUNTIME_VERSION_COMMANDS = {
23587
23542
  hermes: {
23588
23543
  command: "hermes",
23589
23544
  args: ["version"]
23545
+ },
23546
+ "claude-code": {
23547
+ command: "alfe-claude-host",
23548
+ args: ["--version"]
23590
23549
  }
23591
23550
  };
23592
23551
  /**
@@ -23770,6 +23729,7 @@ async function startDaemon() {
23770
23729
  logger$1.debug({ socketPath: config.socketPath }, "Starting IPC server...");
23771
23730
  ipcServer = new IPCServer(config.socketPath);
23772
23731
  ipcServer.setRequestHandler(handlePluginRequest);
23732
+ turnActivityProbe = new IpcTurnActivityProbe(() => ipcServer);
23773
23733
  try {
23774
23734
  await ipcServer.start();
23775
23735
  logger$1.debug("IPC server started");
@@ -23824,6 +23784,13 @@ async function startDaemon() {
23824
23784
  runtime: name,
23825
23785
  home: runtimeCfg.workspace
23826
23786
  }, "Registered Hermes runtime applier");
23787
+ } else if (name === "claude-code") {
23788
+ runtimeAppliers.set(name, new ClaudeCodeApplier({ home: runtimeCfg.workspace }));
23789
+ logger$1.info({
23790
+ runtime: name,
23791
+ home: runtimeCfg.workspace,
23792
+ agentWorkspace: runtimeCfg.agentWorkspace
23793
+ }, "Registered Claude Code runtime applier");
23827
23794
  } else logger$1.warn({ runtime: name }, "Unknown runtime type — skipping");
23828
23795
  runtimeAppliersRef = runtimeAppliers;
23829
23796
  const integrationsService = new IntegrationsService(new AlfeApiClient({
@@ -23842,7 +23809,7 @@ async function startDaemon() {
23842
23809
  idleTtlMs: 0,
23843
23810
  ...mcpErrorHooks
23844
23811
  }, { connect: defaultConnect });
23845
- if (config.runtime === "hermes") logger$1.info("Hermes runtime daemon MCP bundler left idle (store mirrored to config.yaml by HermesMcpSync)");
23812
+ if (config.runtime === "hermes" || config.runtime === "claude-code") logger$1.info({ runtime: config.runtime }, "MCP store mirrored into runtime config by the runtime MCP sync — daemon MCP bundler left idle");
23846
23813
  else {
23847
23814
  await mcpManager.loadIntoBundler(mcpBundler);
23848
23815
  const warmBundler = (reason) => {
@@ -23901,6 +23868,18 @@ async function startDaemon() {
23901
23868
  hermesMcpSync.start();
23902
23869
  }
23903
23870
  }
23871
+ if (config.runtime === "claude-code") {
23872
+ const ccCfg = config.runtimes[config.runtime];
23873
+ if (ccCfg) {
23874
+ claudeCodeMcpSync = new ClaudeCodeMcpSync({
23875
+ manager: mcpManager,
23876
+ home: ccCfg.workspace,
23877
+ apiKey: config.apiKey,
23878
+ requestRestart: requestRuntimeRestart
23879
+ });
23880
+ claudeCodeMcpSync.start();
23881
+ }
23882
+ }
23904
23883
  cloudClient.setOnReconciliationComplete(() => {
23905
23884
  try {
23906
23885
  const activeCommands = integrationManager.getActiveCommands();
@@ -23933,6 +23912,7 @@ async function startDaemon() {
23933
23912
  workspace: runtimeCfg.workspace,
23934
23913
  env: {}
23935
23914
  });
23915
+ runtimeProcess.setTurnActivityProbe(turnActivityProbe);
23936
23916
  runtimeProcess.start();
23937
23917
  logger$1.debug("Runtime process started");
23938
23918
  if (config.runtime === "openclaw") {
@@ -23963,6 +23943,10 @@ async function startDaemon() {
23963
23943
  hermesMcpSync.stop();
23964
23944
  hermesMcpSync = null;
23965
23945
  }
23946
+ if (claudeCodeMcpSync) {
23947
+ claudeCodeMcpSync.stop();
23948
+ claudeCodeMcpSync = null;
23949
+ }
23966
23950
  if (mcpBundler) {
23967
23951
  logger$1.debug("Stopping MCP bundler...");
23968
23952
  await mcpBundler.dispose();
@@ -24038,11 +24022,46 @@ async function handleCloudCommand(command) {
24038
24022
  }
24039
24023
  return ack;
24040
24024
  }
24025
+ /**
24026
+ * Wait for any in-flight chat turn to finish before a daemon PROCESS exit
24027
+ * (daemon.update / daemon.restart). Those paths don't stop the runtime via
24028
+ * `RuntimeProcess.stop` — they exit the daemon, and systemd/launchd cgroup-kill
24029
+ * the runtime child underneath. That kill is just as turn-destroying as a
24030
+ * SIGTERM, so we drain here too. Grace-deadline backstopped; fails open to
24031
+ * "proceed" so a wedged turn can't strand a platform-pushed update.
24032
+ */
24033
+ async function drainRuntimeBeforeExit(reason) {
24034
+ const probe = turnActivityProbe;
24035
+ if (!probe || !runtimeProcess?.isRunning) return;
24036
+ const deadline = Date.now() + DRAIN_GRACE_MS;
24037
+ let deferred = false;
24038
+ while (Date.now() < deadline) {
24039
+ let active;
24040
+ try {
24041
+ active = await probe.isTurnActive();
24042
+ } catch {
24043
+ return;
24044
+ }
24045
+ if (!active) {
24046
+ if (deferred) logger$1.info({ reason }, "Runtime idle — proceeding with planned daemon exit");
24047
+ return;
24048
+ }
24049
+ if (!deferred) {
24050
+ deferred = true;
24051
+ logger$1.info({ reason }, "Runtime is mid-turn — deferring planned daemon exit until idle");
24052
+ }
24053
+ await new Promise((resolve) => setTimeout(resolve, 500));
24054
+ }
24055
+ logger$1.warn({
24056
+ reason,
24057
+ graceMs: DRAIN_GRACE_MS
24058
+ }, "Runtime still mid-turn at drain grace deadline — proceeding with daemon exit");
24059
+ }
24041
24060
  async function executeCloudCommand(command) {
24042
24061
  if (command.command === "daemon.update") {
24043
24062
  const version = command.payload?.version ?? "latest";
24044
24063
  setTimeout(() => {
24045
- import("./upgrade.js").then(({ upgradeAndExit }) => upgradeAndExit(version)).catch((err) => {
24064
+ drainRuntimeBeforeExit("daemon.update").then(() => import("./upgrade.js")).then(({ upgradeAndExit }) => upgradeAndExit(version)).catch((err) => {
24046
24065
  logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Upgrade failed");
24047
24066
  process.exit(1);
24048
24067
  });
@@ -24060,7 +24079,9 @@ async function executeCloudCommand(command) {
24060
24079
  if (command.command === "daemon.restart") {
24061
24080
  logger$1.info("Restart requested — exiting for systemd restart");
24062
24081
  setTimeout(() => {
24063
- process.exit(0);
24082
+ drainRuntimeBeforeExit("daemon.restart").then(() => {
24083
+ process.exit(0);
24084
+ });
24064
24085
  }, 500);
24065
24086
  return {
24066
24087
  type: "COMMAND_ACK",
@@ -24437,17 +24458,41 @@ function handleMcpListServers(manager = mcpManagerRef) {
24437
24458
  message: "MCP manager not initialized"
24438
24459
  }
24439
24460
  };
24461
+ const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
24462
+ const byName = new Map(statuses.map((s) => [s.name, s]));
24440
24463
  return {
24441
24464
  ok: true,
24442
- payload: { servers: manager.listServers() }
24465
+ payload: { servers: manager.listServers().map(({ id, entry }) => ({
24466
+ id,
24467
+ entry,
24468
+ status: byName.get(id) ?? {
24469
+ name: id,
24470
+ connected: false,
24471
+ toolCount: 0,
24472
+ consecutiveFailures: 0
24473
+ }
24474
+ })) }
24443
24475
  };
24444
24476
  }
24445
24477
  /**
24446
- * Register a new MCP server in the alfe bundler store on behalf of the
24447
- * agent. Owned as `'manual'` so the agent can later remove it without
24448
- * an expectedOwner conflict — matches what `alfe mcp add` does from the
24449
- * CLI. The daemon's bundler will pick the change up via the manager's
24450
- * store watcher and spawn the child on the next tool-factory call.
24478
+ * How long the add-confirm probe waits for the freshly-added server to
24479
+ * connect before replying. This is effectively the whole budget for
24480
+ * `bundler.warmServer`: the `Manager.warmServer` pre-reconcile is cheap here
24481
+ * because `addServer` already queued the Connection object into the store +
24482
+ * bundler before we probe, so `reconcileBundler()` finds no diff and returns
24483
+ * fast. Kept well under the plugin caller's 30s IPC timeout. A slower server
24484
+ * still connects in the background and surfaces on the next
24485
+ * `alfe_mcp_list_tools` — the probe just reports what it saw within the window.
24486
+ */
24487
+ const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
24488
+ /**
24489
+ * Register a new MCP server in the alfe bundler store on behalf of the agent,
24490
+ * then CONFIRM the connect before replying so the agent learns whether the
24491
+ * server actually works. Owned as `'manual'` so the agent can later remove it
24492
+ * without an expectedOwner conflict — matches what `alfe mcp add` does from the
24493
+ * CLI. Registration is durable regardless of the probe outcome: a probe
24494
+ * failure (or a runtime with no daemon bundler, e.g. hermes) still returns
24495
+ * `ok` with `connected: false`; the store watcher / runtime picks the entry up.
24451
24496
  */
24452
24497
  async function handleMcpAddServer(params, manager = mcpManagerRef) {
24453
24498
  if (!manager) return {
@@ -24478,10 +24523,6 @@ async function handleMcpAddServer(params, manager = mcpManagerRef) {
24478
24523
  id: p.id,
24479
24524
  owner: "manual"
24480
24525
  });
24481
- return {
24482
- ok: true,
24483
- payload: { id: p.id }
24484
- };
24485
24526
  } catch (err) {
24486
24527
  const message = err instanceof Error ? err.message : String(err);
24487
24528
  logger$1.warn({
@@ -24496,6 +24537,24 @@ async function handleMcpAddServer(params, manager = mcpManagerRef) {
24496
24537
  }
24497
24538
  };
24498
24539
  }
24540
+ let status = null;
24541
+ if (manager.warmServer) try {
24542
+ status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
24543
+ } catch (err) {
24544
+ logger$1.warn({
24545
+ id: p.id,
24546
+ err: err instanceof Error ? err.message : String(err)
24547
+ }, "mcp.add_server warm probe threw");
24548
+ }
24549
+ return {
24550
+ ok: true,
24551
+ payload: {
24552
+ id: p.id,
24553
+ connected: status?.connected ?? false,
24554
+ toolCount: status?.toolCount ?? 0,
24555
+ ...status?.lastError !== void 0 ? { error: status.lastError } : {}
24556
+ }
24557
+ };
24499
24558
  }
24500
24559
  function buildServerConfig(p) {
24501
24560
  if (typeof p.command === "string" && p.command.length > 0) {
@@ -24721,4 +24780,4 @@ function formatDuration(ms) {
24721
24780
  return `${String(Math.round(seconds / 3600))}h`;
24722
24781
  }
24723
24782
  //#endregion
24724
- export { PID_PATH as C, resolveAgentIdentity as D, loadDaemonConfig as E, PINNED_OPENCLAW_VERSION as O, ALFE_DIR as S, fetchAgentConfig as T, captureRuntimeErrorOutput as _, installService as a, setAgentContext as b, uninstallService as c, AGENT_RUNTIME_SENTRY_DSN as d, captureCliFailure as f, captureRuntimeCrash as g, captureMcpFailure as h, checkExistingDaemon as i, AGENT_DAEMON_SENTRY_DSN as l, captureIntegrationFailure as m, queryDaemonHealth as n, startService as o, captureFatal as p, startDaemon as r, stopExistingDaemon as s, formatHealthReport as t, AGENT_MCP_SENTRY_DSN as u, flushSentry as v, SOCKET_PATH as w, PROTOCOL_VERSION as x, initAgentSentry as y };
24783
+ export { installService as a, uninstallService as c, PID_PATH as d, SOCKET_PATH as f, PINNED_OPENCLAW_VERSION as g, resolveAgentIdentity as h, checkExistingDaemon as i, PROTOCOL_VERSION as l, loadDaemonConfig as m, queryDaemonHealth as n, startService as o, fetchAgentConfig as p, startDaemon as r, stopExistingDaemon as s, formatHealthReport as t, ALFE_DIR as u };