@alfe.ai/gateway 0.10.5 → 0.10.6

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
@@ -4349,6 +4349,7 @@ function isClerkReverificationResponse(value) {
4349
4349
  //#endregion
4350
4350
  //#region ../../packages-internal/types/dist/models.js
4351
4351
  const AnthropicModel = {
4352
+ Opus55: "claude-opus-5-5",
4352
4353
  Opus5: "claude-opus-5",
4353
4354
  Opus48: "claude-opus-4-8",
4354
4355
  Opus47: "claude-opus-4-7",
@@ -4359,6 +4360,7 @@ const AnthropicModel = {
4359
4360
  };
4360
4361
  const ANTHROPIC_MODELS = enumValues(AnthropicModel);
4361
4362
  const OpenAIModel = {
4363
+ GPT6Astra: "gpt-6-astra",
4362
4364
  GPT4o: "gpt-4o",
4363
4365
  GPT4oMini: "gpt-4o-mini",
4364
4366
  O3: "o3",
@@ -4381,20 +4383,22 @@ const OpenAIModel = {
4381
4383
  };
4382
4384
  const OPENAI_MODELS = enumValues(OpenAIModel);
4383
4385
  const DeepSeekModel = {
4384
- Chat: "deepseek-chat",
4385
- Reasoner: "deepseek-reasoner",
4386
- V4Flash: "deepseek-v4-flash",
4387
- V4Pro: "deepseek-v4-pro"
4386
+ Flash: "deepseek-flash",
4387
+ V4Pro: "deepseek-v4-pro",
4388
+ V4Flash: "deepseek-v4-flash"
4388
4389
  };
4389
4390
  const DEEPSEEK_MODELS = enumValues(DeepSeekModel);
4390
4391
  const GoogleModel = {
4392
+ Gemini38Flash: "gemini-3.8-flash",
4393
+ Gemini37Flash: "gemini-3.7-flash",
4394
+ Gemini36Flash: "gemini-3.6-flash",
4391
4395
  Gemini35Flash: "gemini-3.5-flash",
4396
+ Gemini35FlashLite: "gemini-3.5-flash-lite",
4392
4397
  Gemini31Pro: "gemini-3.1-pro-preview",
4393
4398
  Gemini31FlashLite: "gemini-3.1-flash-lite",
4394
4399
  Gemini25Pro: "gemini-2.5-pro",
4395
4400
  Gemini25Flash: "gemini-2.5-flash",
4396
- Gemini25FlashLite: "gemini-2.5-flash-lite",
4397
- Gemini20Flash: "gemini-2.0-flash"
4401
+ Gemini25FlashLite: "gemini-2.5-flash-lite"
4398
4402
  };
4399
4403
  const GOOGLE_MODELS = enumValues(GoogleModel);
4400
4404
  const MiniMaxModel = {
@@ -4412,20 +4416,23 @@ const MistralModel = {
4412
4416
  Small: "mistral-small-latest",
4413
4417
  Codestral: "codestral-latest",
4414
4418
  Ministral8b: "ministral-8b-latest",
4415
- Ministral3b: "ministral-3b-latest",
4416
- MagistralMedium: "magistral-medium-latest",
4417
- MagistralSmall: "magistral-small-latest",
4418
- DevstralMedium: "devstral-medium-latest"
4419
+ Ministral3b: "ministral-3b-latest"
4419
4420
  };
4420
4421
  const MISTRAL_MODELS = enumValues(MistralModel);
4421
4422
  const XAIModel = {
4423
+ Grok47: "grok-4.7",
4424
+ Grok46: "grok-4.6",
4422
4425
  Grok45: "grok-4.5",
4423
4426
  Grok43: "grok-4.3",
4427
+ GrokBuild01: "grok-build-0.1",
4424
4428
  Grok4: "grok-4",
4425
4429
  Grok41Fast: "grok-4.1-fast"
4426
4430
  };
4427
4431
  const XAI_MODELS = enumValues(XAIModel);
4428
4432
  const ZhipuModel = {
4433
+ GLM53: "glm-5.3",
4434
+ GLM53Flash: "glm-5.3-flash",
4435
+ GLM53FlashX: "glm-5.3-flashx",
4429
4436
  GLM52: "glm-5.2",
4430
4437
  GLM51: "glm-5.1",
4431
4438
  GLM46: "glm-4.6",
@@ -4441,6 +4448,8 @@ const MoonshotModel = {
4441
4448
  };
4442
4449
  const MOONSHOT_MODELS = enumValues(MoonshotModel);
4443
4450
  const QwenModel = {
4451
+ Qwen38Max: "qwen3.8-max",
4452
+ Qwen38Flash: "qwen3.8-flash",
4444
4453
  Qwen37Max: "qwen3.7-max",
4445
4454
  Qwen37Plus: "qwen3.7-plus",
4446
4455
  Qwen36Flash: "qwen3.6-flash",
@@ -4471,7 +4480,7 @@ _enum(ZHIPU_MODELS);
4471
4480
  _enum(MOONSHOT_MODELS);
4472
4481
  _enum(QWEN_MODELS);
4473
4482
  string().trim().min(1);
4474
- AnthropicModel.Opus5, 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, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4483
+ AnthropicModel.Opus55, AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT6Astra, 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.Flash, DeepSeekModel.V4Pro, DeepSeekModel.V4Flash, GoogleModel.Gemini38Flash, GoogleModel.Gemini37Flash, GoogleModel.Gemini36Flash, GoogleModel.Gemini35Flash, GoogleModel.Gemini35FlashLite, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, XAIModel.Grok47, XAIModel.Grok46, XAIModel.Grok45, XAIModel.Grok43, XAIModel.GrokBuild01, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM53, ZhipuModel.GLM53Flash, ZhipuModel.GLM53FlashX, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen38Max, QwenModel.Qwen38Flash, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4475
4484
  /**
4476
4485
  * Alfe-hosted open-weight models are promoted dynamically only in regions
4477
4486
  * whose private data plane exists. Keep them out of ALL_MODELS so the standard
@@ -4531,7 +4540,7 @@ const AWS_PRIVATE_MODEL_PRICING = {
4531
4540
  [BedrockModel.TitanEmbedTextV2]: { input: .026004 }
4532
4541
  };
4533
4542
  Object.fromEntries(AWS_PRIVATE_BEDROCK_CHAT_MODELS.map((modelId) => [`alfe-bedrock-${modelId}`, AWS_PRIVATE_MODEL_PRICING[modelId]]));
4534
- AnthropicModel.Opus5, 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, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4543
+ AnthropicModel.Opus55, AnthropicModel.Opus5, AnthropicModel.Opus48, AnthropicModel.Opus47, AnthropicModel.Opus46, AnthropicModel.Sonnet5, AnthropicModel.Sonnet46, AnthropicModel.Haiku45, OpenAIModel.GPT6Astra, 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.Flash, DeepSeekModel.V4Pro, DeepSeekModel.V4Flash, GoogleModel.Gemini38Flash, GoogleModel.Gemini37Flash, GoogleModel.Gemini36Flash, GoogleModel.Gemini35Flash, GoogleModel.Gemini35FlashLite, GoogleModel.Gemini31Pro, GoogleModel.Gemini31FlashLite, GoogleModel.Gemini25Pro, GoogleModel.Gemini25Flash, GoogleModel.Gemini25FlashLite, MiniMaxModel.M3, MiniMaxModel.M27, MiniMaxModel.M27HighSpeed, MiniMaxModel.M25, MiniMaxModel.M21, MiniMaxModel.M2, MistralModel.Large, MistralModel.Medium, MistralModel.Small, MistralModel.Codestral, MistralModel.Ministral8b, MistralModel.Ministral3b, XAIModel.Grok47, XAIModel.Grok46, XAIModel.Grok45, XAIModel.Grok43, XAIModel.GrokBuild01, XAIModel.Grok4, XAIModel.Grok41Fast, ZhipuModel.GLM53, ZhipuModel.GLM53Flash, ZhipuModel.GLM53FlashX, ZhipuModel.GLM52, ZhipuModel.GLM51, ZhipuModel.GLM46, ZhipuModel.GLM45, ZhipuModel.GLM45Air, MoonshotModel.K3, MoonshotModel.K26, MoonshotModel.K27Code, MoonshotModel.K27CodeHighSpeed, QwenModel.Qwen38Max, QwenModel.Qwen38Flash, QwenModel.Qwen37Max, QwenModel.Qwen37Plus, QwenModel.Qwen36Flash, QwenModel.Qwen35Flash;
4535
4544
  enumValues({
4536
4545
  Standard: "standard",
4537
4546
  AwsPrivate: "aws_private"
@@ -4746,7 +4755,8 @@ var AlfeApiClient = class {
4746
4755
  ok: false,
4747
4756
  error: errorMessage ?? (typeof body === "string" ? body : `API error: ${String(res.status)}`),
4748
4757
  status: res.status,
4749
- ...typeof errBody?.code === "string" ? { code: errBody.code } : {}
4758
+ ...typeof errBody?.code === "string" ? { code: errBody.code } : {},
4759
+ ...Array.isArray(errBody?.ineligibleUserIds) && errBody.ineligibleUserIds.every((id) => typeof id === "string") ? { ineligibleUserIds: [...errBody.ineligibleUserIds] } : {}
4750
4760
  }
4751
4761
  };
4752
4762
  }
@@ -5854,13 +5864,17 @@ function isIPCResponse(msg) {
5854
5864
  const PROTOCOL_VERSION = 1;
5855
5865
  //#endregion
5856
5866
  //#region src/runtime-gate.ts
5857
- const log$5 = logger.child({ component: "RuntimeGate" });
5867
+ const log$6 = logger.child({ component: "RuntimeGate" });
5858
5868
  var RuntimeGate = class {
5859
5869
  depth = 0;
5860
5870
  wasRunning = false;
5861
5871
  exclusive = false;
5862
5872
  /** Brackets parked behind an exclusive hold (or an exclusive request parked behind open brackets). */
5863
5873
  waiters = [];
5874
+ /** `suspendExclusive()` callers currently parked behind open brackets. */
5875
+ parkedExclusive = 0;
5876
+ /** The daemon is holding the runtime's FIRST start (fresh managed box); see {@link deferFirstStart}. */
5877
+ firstStartDeferred = false;
5864
5878
  constructor(getRuntime) {
5865
5879
  this.getRuntime = getRuntime;
5866
5880
  }
@@ -5872,6 +5886,9 @@ var RuntimeGate = class {
5872
5886
  get isExclusive() {
5873
5887
  return this.exclusive;
5874
5888
  }
5889
+ get hasParkedExclusive() {
5890
+ return this.parkedExclusive > 0;
5891
+ }
5875
5892
  waitFor(ready) {
5876
5893
  if (ready()) return Promise.resolve();
5877
5894
  return new Promise((resolve) => {
@@ -5894,26 +5911,33 @@ var RuntimeGate = class {
5894
5911
  * up (e.g. autoStartRuntime=false). Waits while an exclusive bracket is open.
5895
5912
  */
5896
5913
  async suspend() {
5897
- if (this.exclusive) log$5.info("Mutating reconcile parked behind an exclusive runtime hold (upgrade in flight)");
5914
+ if (this.exclusive) log$6.info("Mutating reconcile parked behind an exclusive runtime hold (upgrade in flight)");
5898
5915
  while (this.exclusive) await this.waitFor(() => !this.exclusive);
5899
5916
  await this.enter();
5900
5917
  }
5901
5918
  async suspendExclusive() {
5902
- if (this.exclusive || this.depth !== 0) log$5.info({
5919
+ if (this.exclusive || this.depth !== 0) log$6.info({
5903
5920
  depth: this.depth,
5904
5921
  exclusive: this.exclusive
5905
5922
  }, "Exclusive runtime hold parked behind open brackets (reconcile in flight)");
5906
- while (this.exclusive || this.depth !== 0) await this.waitFor(() => !this.exclusive && this.depth === 0);
5907
- log$5.info("Exclusive runtime hold acquired");
5923
+ if (this.exclusive || this.depth !== 0) {
5924
+ this.parkedExclusive += 1;
5925
+ try {
5926
+ while (this.exclusive || this.depth !== 0) await this.waitFor(() => !this.exclusive && this.depth === 0);
5927
+ } finally {
5928
+ this.parkedExclusive -= 1;
5929
+ }
5930
+ }
5931
+ log$6.info("Exclusive runtime hold acquired");
5908
5932
  this.exclusive = true;
5909
5933
  await this.enter();
5910
5934
  }
5911
5935
  async enter() {
5912
5936
  if (this.depth++ > 0) return;
5913
5937
  const rp = this.getRuntime();
5914
- this.wasRunning = rp?.isRunning ?? false;
5938
+ this.wasRunning = rp?.isActive ?? false;
5915
5939
  if (rp && this.wasRunning) {
5916
- log$5.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5940
+ log$6.info("Suspending runtime for mutating reconcile (avoid concurrent SQLite writers)");
5917
5941
  await rp.stopWhenIdle();
5918
5942
  }
5919
5943
  }
@@ -5928,7 +5952,7 @@ var RuntimeGate = class {
5928
5952
  if (this.depth < 0) this.depth = 0;
5929
5953
  const rp = this.getRuntime();
5930
5954
  if (rp && this.wasRunning) {
5931
- log$5.info("Resuming runtime after mutating reconcile");
5955
+ log$6.info("Resuming runtime after mutating reconcile");
5932
5956
  rp.resume();
5933
5957
  }
5934
5958
  this.wasRunning = false;
@@ -5937,6 +5961,32 @@ var RuntimeGate = class {
5937
5961
  return Promise.resolve();
5938
5962
  }
5939
5963
  /**
5964
+ * Start a runtime that has not been started yet (daemon boot, deferred first
5965
+ * start). Never spawns while a bracket is open: the start is folded into the
5966
+ * pending outermost `resume()` instead, so a first start can't land in the
5967
+ * middle of a mutating reconcile or an exclusive upgrade.
5968
+ */
5969
+ requestStart() {
5970
+ this.firstStartDeferred = false;
5971
+ const rp = this.getRuntime();
5972
+ if (!rp) return;
5973
+ if (this.depth > 0) {
5974
+ log$6.info("Runtime start requested while suspended — deferring to pending resume");
5975
+ this.wasRunning = true;
5976
+ return;
5977
+ }
5978
+ rp.start();
5979
+ }
5980
+ /**
5981
+ * Mark the runtime's first start as deferred (fresh managed box, see
5982
+ * first-runtime-start.ts). Until {@link requestStart} runs, an out-of-band
5983
+ * {@link requestRestart} of the never-started runtime is folded into that
5984
+ * pending start instead of spawning it early.
5985
+ */
5986
+ deferFirstStart() {
5987
+ this.firstStartDeferred = true;
5988
+ }
5989
+ /**
5940
5990
  * Out-of-band restart request (e.g. `alfe.config_set`, Hermes MCP sync).
5941
5991
  * Deferred while a suspend is active — the pending `resume()` will bring the
5942
5992
  * runtime back with fresh config, so restarting mid-suspend would just fight
@@ -5945,12 +5995,16 @@ var RuntimeGate = class {
5945
5995
  */
5946
5996
  requestRestart() {
5947
5997
  if (this.depth > 0) {
5948
- log$5.info("Runtime restart requested while suspended — deferring to pending resume");
5998
+ log$6.info("Runtime restart requested while suspended — deferring to pending resume");
5949
5999
  return;
5950
6000
  }
5951
6001
  const rp = this.getRuntime();
6002
+ if (rp && this.firstStartDeferred && !rp.isActive) {
6003
+ log$6.info("Runtime restart requested before the deferred first start — folding into that start");
6004
+ return;
6005
+ }
5952
6006
  if (rp) rp.restart().catch((err) => {
5953
- log$5.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
6007
+ log$6.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to restart runtime");
5954
6008
  });
5955
6009
  }
5956
6010
  };
@@ -5973,7 +6027,7 @@ var NoopRuntimeGate = class {
5973
6027
  };
5974
6028
  //#endregion
5975
6029
  //#region src/reconciliation.ts
5976
- const log$4 = logger.child({ component: "Reconciliation" });
6030
+ const log$5 = logger.child({ component: "Reconciliation" });
5977
6031
  /**
5978
6032
  * How many times reconcile will re-attempt activation of an intact integration
5979
6033
  * stuck in `error` (with no reinstall requested) before giving up and waiting
@@ -5994,6 +6048,13 @@ var ReconciliationEngine = class {
5994
6048
  */
5995
6049
  suspended = false;
5996
6050
  /**
6051
+ * A suspension carried from the previous pass because the caller already
6052
+ * had another DESIRED_STATE queued (see {@link ReconcileOptions.holdSuspension}).
6053
+ * The next pass adopts it instead of resuming → re-suspending the runtime;
6054
+ * {@link releaseHeldSuspension} gives it back once the queue is drained.
6055
+ */
6056
+ heldSuspension = false;
6057
+ /**
5997
6058
  * @param manager The integration manager to converge.
5998
6059
  * @param gate Runtime gate used to suspend the runtime child around
5999
6060
  * mutating branches so `openclaw` CLI subprocesses don't write
@@ -6022,14 +6083,43 @@ var ReconciliationEngine = class {
6022
6083
  * so the mutation performed by {@link ensureSuspended} in a different method
6023
6084
  * isn't narrowed away by control-flow analysis at the callsite.
6024
6085
  */
6025
- async resumeIfSuspended() {
6026
- if (this.suspended) await this.gate.resume();
6086
+ async resumeIfSuspended(opts) {
6087
+ if (!this.suspended) return;
6088
+ if (opts?.holdSuspension?.()) {
6089
+ log$5.info("Holding runtime suspension across queued reconcile pass");
6090
+ if (this.gate.hasParkedExclusive) log$5.warn("Exclusive runtime hold (upgrade) is parked behind a suspension held for queued reconcile passes");
6091
+ this.heldSuspension = true;
6092
+ return;
6093
+ }
6094
+ await this.gate.resume();
6095
+ }
6096
+ /**
6097
+ * Resume a suspension held across passes by {@link ReconcileOptions.holdSuspension}.
6098
+ * The caller MUST call this once its queue of passes is drained (success or
6099
+ * failure), otherwise the runtime stays suspended. No-op when nothing is held.
6100
+ */
6101
+ async releaseHeldSuspension() {
6102
+ if (!this.heldSuspension) return;
6103
+ this.heldSuspension = false;
6104
+ await this.gate.resume();
6105
+ }
6106
+ /** True while a suspension is carried between passes (tests/diagnostics). */
6107
+ get isHoldingSuspension() {
6108
+ return this.heldSuspension;
6027
6109
  }
6028
6110
  /**
6029
6111
  * Reconcile local state against desired state from cloud.
6030
6112
  */
6031
- async reconcile(desiredIntegrations) {
6032
- this.suspended = false;
6113
+ async reconcile(desiredIntegrations, opts) {
6114
+ this.suspended = this.heldSuspension;
6115
+ this.heldSuspension = false;
6116
+ try {
6117
+ return await this.reconcilePass(desiredIntegrations);
6118
+ } finally {
6119
+ await this.resumeIfSuspended(opts);
6120
+ }
6121
+ }
6122
+ async reconcilePass(desiredIntegrations) {
6033
6123
  const report = {
6034
6124
  results: [],
6035
6125
  installed: [],
@@ -6043,7 +6133,7 @@ var ReconciliationEngine = class {
6043
6133
  try {
6044
6134
  localIntegrations = await this.manager.getInstalledIntegrations();
6045
6135
  } catch (err) {
6046
- log$4.error({ err }, "Failed to get local integrations");
6136
+ log$5.error({ err }, "Failed to get local integrations");
6047
6137
  captureIntegrationFailure("_reconciliation", "read_local_state", err);
6048
6138
  const errorMessage = "Failed to read local integration state; reconciliation skipped";
6049
6139
  const affectedIds = desiredIntegrations.length > 0 ? desiredIntegrations.map((desired) => desired.integrationId) : ["*"];
@@ -6061,34 +6151,30 @@ var ReconciliationEngine = class {
6061
6151
  }
6062
6152
  const localMap = new Map(localIntegrations.map((i) => [i.id, i]));
6063
6153
  const desiredMap = new Map(desiredIntegrations.map((d) => [d.integrationId, d]));
6064
- try {
6065
- for (const desired of desiredIntegrations) if (desired.desiredStatus === "active") {
6066
- const local = localMap.get(desired.integrationId);
6067
- if (desired.connectionAuthorityVersion !== void 0 && local?.connectionAuthorityPendingVersion !== void 0 && local.status === "error" && (local.version === desired.version || desired.version === "" || local.version === "unknown") && !(desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) && await this.manager.isInstallIntact(desired.integrationId)) report.results.push({
6154
+ for (const desired of desiredIntegrations) if (desired.desiredStatus === "active") {
6155
+ const local = localMap.get(desired.integrationId);
6156
+ if (desired.connectionAuthorityVersion !== void 0 && local?.connectionAuthorityPendingVersion !== void 0 && local.status === "error" && (local.version === desired.version || desired.version === "" || local.version === "unknown") && !(desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) && await this.manager.isInstallIntact(desired.integrationId)) report.results.push({
6157
+ integrationId: desired.integrationId,
6158
+ action: "activated",
6159
+ actualStatus: "active"
6160
+ });
6161
+ else await this.reconcileActive(desired, local, report);
6162
+ const result = report.results.find((item) => item.integrationId === desired.integrationId);
6163
+ if (result?.actualStatus === "active" && desired.connectionAuthorityVersion !== void 0 && (local?.connectionAuthorityPendingVersion !== void 0 || desired.connectionAuthorityVersion !== local?.connectionAuthorityVersion)) try {
6164
+ await this.manager.refreshConnections(desired.integrationId, desired.connectionAuthorityVersion, { onBeforeRuntimeMutation: () => this.ensureSuspended() });
6165
+ } catch (err) {
6166
+ captureIntegrationFailure(desired.integrationId, "refresh_connections", err);
6167
+ const error = err instanceof Error ? err.message : String(err);
6168
+ report.errors.push({
6068
6169
  integrationId: desired.integrationId,
6069
- action: "activated",
6070
- actualStatus: "active"
6170
+ error
6071
6171
  });
6072
- else await this.reconcileActive(desired, local, report);
6073
- const result = report.results.find((item) => item.integrationId === desired.integrationId);
6074
- if (result?.actualStatus === "active" && desired.connectionAuthorityVersion !== void 0 && (local?.connectionAuthorityPendingVersion !== void 0 || desired.connectionAuthorityVersion !== local?.connectionAuthorityVersion)) try {
6075
- await this.manager.refreshConnections(desired.integrationId, desired.connectionAuthorityVersion, { onBeforeRuntimeMutation: () => this.ensureSuspended() });
6076
- } catch (err) {
6077
- captureIntegrationFailure(desired.integrationId, "refresh_connections", err);
6078
- const error = err instanceof Error ? err.message : String(err);
6079
- report.errors.push({
6080
- integrationId: desired.integrationId,
6081
- error
6082
- });
6083
- result.action = "error";
6084
- result.actualStatus = "error";
6085
- result.errorMessage = error;
6086
- }
6087
- } else await this.reconcileRemoved(desired.integrationId, localMap.get(desired.integrationId), report);
6088
- for (const local of localIntegrations) if (!desiredMap.has(local.id)) await this.reconcileRemoved(local.id, local, report);
6089
- } finally {
6090
- await this.resumeIfSuspended();
6091
- }
6172
+ result.action = "error";
6173
+ result.actualStatus = "error";
6174
+ result.errorMessage = error;
6175
+ }
6176
+ } else await this.reconcileRemoved(desired.integrationId, localMap.get(desired.integrationId), report);
6177
+ for (const local of localIntegrations) if (!desiredMap.has(local.id)) await this.reconcileRemoved(local.id, local, report);
6092
6178
  return report;
6093
6179
  }
6094
6180
  async reconcileActive(desired, local, report) {
@@ -6096,10 +6182,10 @@ var ReconciliationEngine = class {
6096
6182
  try {
6097
6183
  if (!local) {
6098
6184
  await this.ensureSuspended();
6099
- log$4.info(`Installing ${id}@${desired.version}`);
6185
+ log$5.info(`Installing ${id}@${desired.version}`);
6100
6186
  await this.manager.install(id, desired.version, desired.config, desired.customSource);
6101
6187
  report.installed.push(id);
6102
- log$4.info(`Activating ${id}`);
6188
+ log$5.info(`Activating ${id}`);
6103
6189
  if ((await this.manager.activate(id)).configApplied) report.configApplied = true;
6104
6190
  report.activated.push(id);
6105
6191
  report.results.push({
@@ -6112,7 +6198,7 @@ var ReconciliationEngine = class {
6112
6198
  if (local.version !== desired.version && desired.version !== "" && local.version !== "unknown") {
6113
6199
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6114
6200
  await this.ensureSuspended();
6115
- log$4.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
6201
+ log$5.info(`Reinstalling ${id} on version change (reinstall requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt}): ${local.version} → ${desired.version}`);
6116
6202
  this.manager.resetReinstallAttempts(id);
6117
6203
  this.activateAttempts.delete(id);
6118
6204
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource, { reinstallRequestedAt: desired.reinstallRequestedAt })).configApplied) report.configApplied = true;
@@ -6125,7 +6211,7 @@ var ReconciliationEngine = class {
6125
6211
  });
6126
6212
  return;
6127
6213
  }
6128
- log$4.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
6214
+ log$5.info(`Upgrading ${id} in place: ${local.version} → ${desired.version}`);
6129
6215
  try {
6130
6216
  if ((await this.manager.upgrade(id, desired.version, desired.config, desired.customSource, { onBeforeRuntimeMutation: () => this.ensureSuspended() })).configApplied) report.configApplied = true;
6131
6217
  this.manager.resetReinstallAttempts(id);
@@ -6139,7 +6225,7 @@ var ReconciliationEngine = class {
6139
6225
  });
6140
6226
  } catch (upgradeErr) {
6141
6227
  const upgradeMsg = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
6142
- log$4.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
6228
+ log$5.error({ err: upgradeErr }, `Upgrade failed for ${id}`);
6143
6229
  captureIntegrationFailure(id, "upgrade", upgradeErr);
6144
6230
  report.errors.push({
6145
6231
  integrationId: id,
@@ -6157,7 +6243,7 @@ var ReconciliationEngine = class {
6157
6243
  if (local.status === "error") {
6158
6244
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6159
6245
  await this.ensureSuspended();
6160
- log$4.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6246
+ log$5.info(`Reinstalling ${id} from error state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6161
6247
  this.manager.resetReinstallAttempts(id);
6162
6248
  this.activateAttempts.delete(id);
6163
6249
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource, { reinstallRequestedAt: desired.reinstallRequestedAt })).configApplied) report.configApplied = true;
@@ -6173,7 +6259,7 @@ var ReconciliationEngine = class {
6173
6259
  if (!await this.manager.isInstallIntact(id)) {
6174
6260
  const attempts = this.manager.getReinstallAttempts(id);
6175
6261
  if (attempts >= 3) {
6176
- log$4.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
6262
+ log$5.error(`Auto-reinstall blocked for ${id} — ${String(attempts)} consecutive failures. Manual reinstall required.`);
6177
6263
  captureIntegrationFailure(id, "reinstall", `Max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`);
6178
6264
  report.errors.push({
6179
6265
  integrationId: id,
@@ -6188,7 +6274,7 @@ var ReconciliationEngine = class {
6188
6274
  return;
6189
6275
  }
6190
6276
  await this.ensureSuspended();
6191
- log$4.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
6277
+ log$5.info(`Auto-reinstalling ${id} — install directory is corrupted or missing (attempt ${String(attempts + 1)}/3)`);
6192
6278
  this.manager.incrementReinstallAttempts(id);
6193
6279
  try {
6194
6280
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6203,7 +6289,7 @@ var ReconciliationEngine = class {
6203
6289
  });
6204
6290
  } catch (reinstallErr) {
6205
6291
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
6206
- log$4.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
6292
+ log$5.error({ err: reinstallErr }, `Auto-reinstall failed for ${id}`);
6207
6293
  captureIntegrationFailure(id, "reinstall", reinstallErr);
6208
6294
  report.errors.push({
6209
6295
  integrationId: id,
@@ -6222,7 +6308,7 @@ var ReconciliationEngine = class {
6222
6308
  if (activateAttempts < MAX_ERROR_ACTIVATE_ATTEMPTS) {
6223
6309
  this.activateAttempts.set(id, activateAttempts + 1);
6224
6310
  await this.ensureSuspended();
6225
- log$4.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
6311
+ log$5.info(`Re-activating ${id} from error state (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)})`);
6226
6312
  try {
6227
6313
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
6228
6314
  this.activateAttempts.delete(id);
@@ -6235,7 +6321,7 @@ var ReconciliationEngine = class {
6235
6321
  return;
6236
6322
  } catch (reactivateErr) {
6237
6323
  const reactivateMsg = reactivateErr instanceof Error ? reactivateErr.message : String(reactivateErr);
6238
- log$4.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
6324
+ log$5.warn(`Re-activation of ${id} from error state failed (attempt ${String(activateAttempts + 1)}/${String(MAX_ERROR_ACTIVATE_ATTEMPTS)}): ${reactivateMsg}`);
6239
6325
  captureIntegrationFailure(id, "reactivate", reactivateErr);
6240
6326
  report.errors.push({
6241
6327
  integrationId: id,
@@ -6250,7 +6336,7 @@ var ReconciliationEngine = class {
6250
6336
  return;
6251
6337
  }
6252
6338
  }
6253
- log$4.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
6339
+ log$5.warn(`Integration ${id} is in error state — re-activation exhausted, waiting for reinstall request`);
6254
6340
  captureIntegrationFailure(id, "reactivate", `Integration ${id} stuck in error state — re-activation attempts exhausted`);
6255
6341
  report.errors.push({
6256
6342
  integrationId: id,
@@ -6267,7 +6353,7 @@ var ReconciliationEngine = class {
6267
6353
  if (local.status === "installing") {
6268
6354
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6269
6355
  await this.ensureSuspended();
6270
- log$4.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6356
+ log$5.info(`Reinstalling ${id} from stale installing state (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6271
6357
  this.manager.resetReinstallAttempts(id);
6272
6358
  this.activateAttempts.delete(id);
6273
6359
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource, { reinstallRequestedAt: desired.reinstallRequestedAt })).configApplied) report.configApplied = true;
@@ -6283,7 +6369,7 @@ var ReconciliationEngine = class {
6283
6369
  const attempts = this.manager.getReinstallAttempts(id);
6284
6370
  if (attempts >= 3) {
6285
6371
  const errorMessage = `Stale installing state — max auto-reinstall attempts (${String(attempts)}) reached — manual reinstall required`;
6286
- log$4.error(`Auto-reinstall blocked for ${id} — stuck in "installing", ${String(attempts)} consecutive failures. Manual reinstall required.`);
6372
+ log$5.error(`Auto-reinstall blocked for ${id} — stuck in "installing", ${String(attempts)} consecutive failures. Manual reinstall required.`);
6287
6373
  captureIntegrationFailure(id, "reinstall", errorMessage);
6288
6374
  report.errors.push({
6289
6375
  integrationId: id,
@@ -6298,7 +6384,7 @@ var ReconciliationEngine = class {
6298
6384
  return;
6299
6385
  }
6300
6386
  await this.ensureSuspended();
6301
- log$4.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
6387
+ log$5.info(`Recovering ${id} from stale installing state via reinstall (attempt ${String(attempts + 1)}/3)`);
6302
6388
  this.manager.incrementReinstallAttempts(id);
6303
6389
  try {
6304
6390
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource)).configApplied) report.configApplied = true;
@@ -6313,7 +6399,7 @@ var ReconciliationEngine = class {
6313
6399
  });
6314
6400
  } catch (reinstallErr) {
6315
6401
  const reinstallMsg = reinstallErr instanceof Error ? reinstallErr.message : String(reinstallErr);
6316
- log$4.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
6402
+ log$5.error({ err: reinstallErr }, `Reinstall from stale installing state failed for ${id}`);
6317
6403
  captureIntegrationFailure(id, "reinstall", reinstallErr);
6318
6404
  report.errors.push({
6319
6405
  integrationId: id,
@@ -6330,7 +6416,7 @@ var ReconciliationEngine = class {
6330
6416
  }
6331
6417
  if (local.status !== "active") {
6332
6418
  await this.ensureSuspended();
6333
- log$4.info(`Activating ${id}`);
6419
+ log$5.info(`Activating ${id}`);
6334
6420
  if ((await this.manager.activate(id, { forcePlugins: true })).configApplied) report.configApplied = true;
6335
6421
  report.activated.push(id);
6336
6422
  report.results.push({
@@ -6342,7 +6428,7 @@ var ReconciliationEngine = class {
6342
6428
  }
6343
6429
  if (desired.reinstallRequestedAt && local.installedAt && desired.reinstallRequestedAt > local.installedAt) {
6344
6430
  await this.ensureSuspended();
6345
- log$4.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6431
+ log$5.info(`Reinstall requested for ${id} (requested: ${desired.reinstallRequestedAt}, installed: ${local.installedAt})`);
6346
6432
  this.manager.resetReinstallAttempts(id);
6347
6433
  this.activateAttempts.delete(id);
6348
6434
  if ((await this.manager.reinstall(id, desired.version, desired.config, desired.customSource, { reinstallRequestedAt: desired.reinstallRequestedAt })).configApplied) report.configApplied = true;
@@ -6363,7 +6449,7 @@ var ReconciliationEngine = class {
6363
6449
  });
6364
6450
  } catch (err) {
6365
6451
  const message = err instanceof Error ? err.message : String(err);
6366
- log$4.error({ err }, `Error reconciling ${id}`);
6452
+ log$5.error({ err }, `Error reconciling ${id}`);
6367
6453
  captureIntegrationFailure(id, "reconcile_active", err);
6368
6454
  report.errors.push({
6369
6455
  integrationId: id,
@@ -6388,7 +6474,7 @@ var ReconciliationEngine = class {
6388
6474
  }
6389
6475
  try {
6390
6476
  await this.ensureSuspended();
6391
- log$4.info(`Removing ${id}`);
6477
+ log$5.info(`Removing ${id}`);
6392
6478
  if (local.status === "active") {
6393
6479
  await this.manager.deactivate(id);
6394
6480
  report.deactivated.push(id);
@@ -6402,7 +6488,7 @@ var ReconciliationEngine = class {
6402
6488
  });
6403
6489
  } catch (err) {
6404
6490
  const message = err instanceof Error ? err.message : String(err);
6405
- log$4.error({ err }, `Error removing ${id}`);
6491
+ log$5.error({ err }, `Error removing ${id}`);
6406
6492
  captureIntegrationFailure(id, "reconcile_removed", err);
6407
6493
  report.errors.push({
6408
6494
  integrationId: id,
@@ -6440,6 +6526,7 @@ var CloudClient = class {
6440
6526
  onConnectionChange = null;
6441
6527
  onRuntimeRestartNeeded = null;
6442
6528
  onReconciliationComplete = null;
6529
+ onReconciliationSettled = null;
6443
6530
  reconciliationEngine = null;
6444
6531
  configReconciler = null;
6445
6532
  reconciling = false;
@@ -6479,6 +6566,16 @@ var CloudClient = class {
6479
6566
  this.onReconciliationComplete = handler;
6480
6567
  }
6481
6568
  /**
6569
+ * Set a callback for when a DESIRED_STATE burst has fully SETTLED: the
6570
+ * received snapshot and every queued one have run (each may have succeeded
6571
+ * or failed) and any runtime suspension held across them was released.
6572
+ * Fires on success AND failure — the daemon's deferred first runtime start
6573
+ * hangs off it, and a failed reconcile must not strand the runtime.
6574
+ */
6575
+ setOnReconciliationSettled(handler) {
6576
+ this.onReconciliationSettled = handler;
6577
+ }
6578
+ /**
6482
6579
  * Set the integration manager (and optionally the runtime gate) for
6483
6580
  * reconciliation. When set, the cloud client handles DESIRED_STATE messages
6484
6581
  * automatically. The gate is passed to the ReconciliationEngine so mutating
@@ -6725,20 +6822,43 @@ var CloudClient = class {
6725
6822
  const message = err instanceof Error ? err.message : String(err);
6726
6823
  logger.error({ err: message }, "Cloud: reconciliation failed");
6727
6824
  } finally {
6728
- while (this.pendingDesiredState) {
6729
- const next = this.pendingDesiredState;
6730
- this.pendingDesiredState = null;
6731
- logger.info({ count: next.integrations.length }, "Cloud: processing queued DESIRED_STATE");
6825
+ for (;;) {
6826
+ while (this.pendingDesiredState) {
6827
+ const next = this.pendingDesiredState;
6828
+ this.pendingDesiredState = null;
6829
+ logger.info({ count: next.integrations.length }, "Cloud: processing queued DESIRED_STATE");
6830
+ try {
6831
+ await this.runReconciliation(next);
6832
+ } catch (err) {
6833
+ const message = err instanceof Error ? err.message : String(err);
6834
+ logger.error({ err: message }, "Cloud: queued reconciliation failed");
6835
+ }
6836
+ }
6732
6837
  try {
6733
- await this.runReconciliation(next);
6838
+ await this.reconciliationEngine.releaseHeldSuspension();
6734
6839
  } catch (err) {
6735
6840
  const message = err instanceof Error ? err.message : String(err);
6736
- logger.error({ err: message }, "Cloud: queued reconciliation failed");
6841
+ logger.error({ err: message }, "Cloud: failed to resume runtime after queued reconciliation");
6737
6842
  }
6843
+ if (!this.hasQueuedDesiredState()) break;
6738
6844
  }
6739
6845
  this.reconciling = false;
6846
+ try {
6847
+ this.onReconciliationSettled?.();
6848
+ } catch (err) {
6849
+ const message = err instanceof Error ? err.message : String(err);
6850
+ logger.error({ err: message }, "Cloud: reconciliation-settled handler failed");
6851
+ }
6740
6852
  }
6741
6853
  }
6854
+ /**
6855
+ * Read through a method so control-flow narrowing from the drain loop above
6856
+ * doesn't treat `pendingDesiredState` as still null across an `await` (a
6857
+ * snapshot can be queued by another message while we're suspended).
6858
+ */
6859
+ hasQueuedDesiredState() {
6860
+ return this.pendingDesiredState !== null;
6861
+ }
6742
6862
  async runReconciliation(msg) {
6743
6863
  if (msg.config && this.configReconciler) try {
6744
6864
  const configReport = await this.configReconciler.reconcile(msg.config);
@@ -6754,7 +6874,7 @@ var CloudClient = class {
6754
6874
  logger.error({ err: message }, "Cloud: config reconcile failed");
6755
6875
  }
6756
6876
  if (!this.reconciliationEngine) return;
6757
- const report = await this.reconciliationEngine.reconcile(msg.integrations);
6877
+ const report = await this.reconciliationEngine.reconcile(msg.integrations, { holdSuspension: () => this.hasQueuedDesiredState() });
6758
6878
  logger.info({
6759
6879
  installed: report.installed.length,
6760
6880
  activated: report.activated.length,
@@ -6817,7 +6937,7 @@ var CloudClient = class {
6817
6937
  };
6818
6938
  //#endregion
6819
6939
  //#region src/config-reconciler.ts
6820
- const log$3 = logger.child({ component: "ConfigReconciler" });
6940
+ const log$4 = logger.child({ component: "ConfigReconciler" });
6821
6941
  const DEFAULT_VERIFY_RETRIES = 3;
6822
6942
  const DEFAULT_VERIFY_RETRY_DELAY_MS = 750;
6823
6943
  const delay = (ms) => new Promise((resolve) => {
@@ -6839,7 +6959,7 @@ var ConfigReconciler = class {
6839
6959
  async reconcile(desired) {
6840
6960
  const applier = this.getApplier();
6841
6961
  if (!applier?.setConfigRaw) {
6842
- log$3.debug("No applier with setConfigRaw — skipping config reconcile");
6962
+ log$4.debug("No applier with setConfigRaw — skipping config reconcile");
6843
6963
  return null;
6844
6964
  }
6845
6965
  const setConfigRaw = applier.setConfigRaw.bind(applier);
@@ -6857,13 +6977,13 @@ var ConfigReconciler = class {
6857
6977
  try {
6858
6978
  current = await getConfigRaw(key);
6859
6979
  } catch (err) {
6860
- log$3.warn({
6980
+ log$4.warn({
6861
6981
  key,
6862
6982
  err: err instanceof Error ? err.message : String(err)
6863
6983
  }, "Config diff read failed — attempting the write");
6864
6984
  }
6865
6985
  if (current === want) {
6866
- log$3.debug({ key }, "Config already at desired value — no-op");
6986
+ log$4.debug({ key }, "Config already at desired value — no-op");
6867
6987
  continue;
6868
6988
  }
6869
6989
  }
@@ -6878,7 +6998,7 @@ var ConfigReconciler = class {
6878
6998
  if (getConfigRaw) {
6879
6999
  const outcome = await this.verify(getConfigRaw, key, want);
6880
7000
  if (outcome === "matched") {
6881
- log$3.warn({
7001
+ log$4.warn({
6882
7002
  key,
6883
7003
  err: msg
6884
7004
  }, "config set errored but value landed — treating as applied");
@@ -6894,7 +7014,7 @@ var ConfigReconciler = class {
6894
7014
  if (failures.length > 0) {
6895
7015
  const joined = failures.join("; ");
6896
7016
  const reason = joined.length > 480 ? `${joined.slice(0, 480)}… (truncated)` : joined;
6897
- log$3.warn({
7017
+ log$4.warn({
6898
7018
  version: desired.version,
6899
7019
  reason
6900
7020
  }, "Config reconcile failed");
@@ -6905,13 +7025,13 @@ var ConfigReconciler = class {
6905
7025
  };
6906
7026
  }
6907
7027
  if (inconclusiveKeys.length > 0) {
6908
- log$3.warn({
7028
+ log$4.warn({
6909
7029
  version: desired.version,
6910
7030
  keys: inconclusiveKeys
6911
7031
  }, "Config reconcile verification was inconclusive — leaving version pending");
6912
7032
  return null;
6913
7033
  }
6914
- log$3.info({
7034
+ log$4.info({
6915
7035
  version: desired.version,
6916
7036
  keys: entries.length
6917
7037
  }, "Config reconcile applied");
@@ -6928,7 +7048,7 @@ var ConfigReconciler = class {
6928
7048
  if (current === want) return "matched";
6929
7049
  if (current !== void 0) observedValue = true;
6930
7050
  } catch (err) {
6931
- log$3.debug({
7051
+ log$4.debug({
6932
7052
  key,
6933
7053
  err: err instanceof Error ? err.message : String(err)
6934
7054
  }, "Config verify read failed");
@@ -8945,7 +9065,7 @@ function reportAvailableCommands(deps) {
8945
9065
  * arrive — the grace deadline is a backstop, but fail-open-to-idle means the
8946
9066
  * common "nothing to drain" case restarts immediately with no added latency.
8947
9067
  */
8948
- const log$2 = logger.child({ component: "TurnActivity" });
9068
+ const log$3 = logger.child({ component: "TurnActivity" });
8949
9069
  /** The IPC method the daemon calls on plugins to ask "is a turn active?". */
8950
9070
  const RUNTIME_ACTIVITY_METHOD = "runtime.activity";
8951
9071
  /**
@@ -8964,17 +9084,122 @@ var IpcTurnActivityProbe = class {
8964
9084
  try {
8965
9085
  const responses = await ipc.broadcastRequest(RUNTIME_ACTIVITY_METHOD, {}, this.probeTimeoutMs);
8966
9086
  for (const [pluginId, res] of responses) if (res.ok && res.payload != null && typeof res.payload === "object" && res.payload.active === true) {
8967
- log$2.debug({ pluginId }, "Plugin reports an in-flight turn");
9087
+ log$3.debug({ pluginId }, "Plugin reports an in-flight turn");
8968
9088
  return true;
8969
9089
  }
8970
9090
  return false;
8971
9091
  } catch (err) {
8972
- log$2.debug({ err: err instanceof Error ? err.message : String(err) }, "Turn-activity probe failed — assuming idle");
9092
+ log$3.debug({ err: err instanceof Error ? err.message : String(err) }, "Turn-activity probe failed — assuming idle");
8973
9093
  return false;
8974
9094
  }
8975
9095
  }
8976
9096
  };
8977
9097
  //#endregion
9098
+ //#region src/first-runtime-start.ts
9099
+ /**
9100
+ * Deferred first runtime start for fresh managed boxes.
9101
+ *
9102
+ * On a brand-new managed VM the first DESIRED_STATE always installs the core
9103
+ * integrations, and every mutating reconcile suspends the runtime through the
9104
+ * RuntimeGate. Starting the runtime at daemon boot therefore buys nothing: it
9105
+ * boots (and runs its first-boot migrations) only to be stopped ~seconds later
9106
+ * by that first reconcile, then started again after it. Deferring the first
9107
+ * start until the first reconcile burst settles removes that whole cycle.
9108
+ *
9109
+ * The deferral is released by whichever comes first:
9110
+ * - the cloud client's "reconciliation settled" hook (success OR failure),
9111
+ * - a fallback timer armed when the cloud client first REGISTERS
9112
+ * ({@link DeferredRuntimeStart.armFallback}), so slow boot/connect time
9113
+ * doesn't eat the window the first DESIRED_STATE needs;
9114
+ * - a longer boot ceiling measured from daemon boot, so a daemon that never
9115
+ * registers (gateway down, registration failing) still brings the runtime up.
9116
+ *
9117
+ * Only fresh managed boxes defer (managed mode AND no integrations in local
9118
+ * state). Self-hosted daemons and restarts of an already-provisioned box start
9119
+ * the runtime immediately, exactly as before.
9120
+ */
9121
+ const log$2 = logger.child({ component: "FirstRuntimeStart" });
9122
+ /** How long a fresh box waits, after first registering with the cloud, for its first reconcile to settle. */
9123
+ const FIRST_START_FALLBACK_MS = 18e4;
9124
+ /** Hard ceiling from daemon boot, for a daemon that never registers with the cloud. */
9125
+ const FIRST_START_BOOT_CEILING_MS = 6e5;
9126
+ /**
9127
+ * Fresh managed box ⇔ managed mode AND local integration state is readable and
9128
+ * empty. An unreadable state is NOT treated as fresh — fall back to the old
9129
+ * start-immediately behaviour rather than delay a box we know nothing about.
9130
+ */
9131
+ function shouldDeferFirstRuntimeStart(input) {
9132
+ return input.managed && input.installedIntegrationCount === 0;
9133
+ }
9134
+ var DeferredRuntimeStart = class {
9135
+ released = false;
9136
+ cancelled = false;
9137
+ startFn = null;
9138
+ fallbackArmed = false;
9139
+ fallbackTimer = null;
9140
+ ceilingTimer;
9141
+ /** Arms the boot ceiling immediately; the fallback waits for {@link armFallback}. */
9142
+ constructor(fallbackMs = FIRST_START_FALLBACK_MS, bootCeilingMs = FIRST_START_BOOT_CEILING_MS) {
9143
+ this.fallbackMs = fallbackMs;
9144
+ this.ceilingTimer = setTimeout(() => {
9145
+ this.ceilingTimer = null;
9146
+ this.release("boot-ceiling");
9147
+ }, bootCeilingMs);
9148
+ }
9149
+ /**
9150
+ * Start the fallback clock. Called when the cloud client registers; only
9151
+ * the FIRST call arms it (a reconnect must not push the deadline out).
9152
+ */
9153
+ armFallback() {
9154
+ if (this.released || this.cancelled || this.fallbackArmed) return;
9155
+ this.fallbackArmed = true;
9156
+ this.fallbackTimer = setTimeout(() => {
9157
+ this.fallbackTimer = null;
9158
+ this.release("fallback-timeout");
9159
+ }, this.fallbackMs);
9160
+ }
9161
+ /** True once released (by either path). */
9162
+ get isReleased() {
9163
+ return this.released;
9164
+ }
9165
+ /**
9166
+ * Provide the start action once the runtime process exists. If the deferral
9167
+ * was already released (a reconcile settled before the runtime object was
9168
+ * built), starts right away.
9169
+ */
9170
+ attach(start) {
9171
+ if (this.cancelled) return;
9172
+ this.startFn = start;
9173
+ if (this.released) start();
9174
+ }
9175
+ /** Release the deferral. Idempotent — only the first call starts the runtime. */
9176
+ release(reason) {
9177
+ if (this.released || this.cancelled) return;
9178
+ this.released = true;
9179
+ this.clearTimers();
9180
+ log$2.info({
9181
+ reason,
9182
+ fallbackMs: this.fallbackMs
9183
+ }, "Releasing deferred first runtime start");
9184
+ this.startFn?.();
9185
+ }
9186
+ /** Daemon shutdown: never start the runtime after this. */
9187
+ cancel() {
9188
+ this.cancelled = true;
9189
+ this.clearTimers();
9190
+ }
9191
+ clearTimers() {
9192
+ if (this.fallbackTimer) {
9193
+ clearTimeout(this.fallbackTimer);
9194
+ this.fallbackTimer = null;
9195
+ }
9196
+ if (this.ceilingTimer) {
9197
+ clearTimeout(this.ceilingTimer);
9198
+ this.ceilingTimer = null;
9199
+ }
9200
+ }
9201
+ };
9202
+ //#endregion
8978
9203
  //#region src/command-registry.ts
8979
9204
  /**
8980
9205
  * Command Registry — maps integration command names to local handler files.
@@ -9656,6 +9881,8 @@ let stopPairingApprovalPoller = null;
9656
9881
  let runtimeAppliersRef = null;
9657
9882
  let openclawCliLockRef = null;
9658
9883
  let runtimeGateRef = null;
9884
+ /** Pending deferred first runtime start (fresh managed box only). */
9885
+ let firstRuntimeStart = null;
9659
9886
  const LEGACY_CONFIG_SET_KEYS = new Set(["agents.defaults.model"]);
9660
9887
  const MAX_LEGACY_CONFIG_VALUE_LENGTH = 16 * 1024;
9661
9888
  async function startDaemon() {
@@ -9744,6 +9971,7 @@ async function startDaemon() {
9744
9971
  cloudClient.setConnectionChangeHandler((connected) => {
9745
9972
  cloudConnected = connected;
9746
9973
  logger.info({ connected }, "Cloud connection state changed");
9974
+ if (connected) firstRuntimeStart?.armFallback();
9747
9975
  ipcServer?.broadcastEvent("cloud.status", { connected });
9748
9976
  });
9749
9977
  const openclawCliLock = new SerialOpenClawCliLock();
@@ -9874,6 +10102,27 @@ async function startDaemon() {
9874
10102
  cloudClient
9875
10103
  });
9876
10104
  });
10105
+ if (config.autoStartRuntime && config.runtime) {
10106
+ let installedIntegrationCount;
10107
+ try {
10108
+ installedIntegrationCount = integrationManager.list().length;
10109
+ } catch (err) {
10110
+ logger.warn({ err: err instanceof Error ? err.message : String(err) }, "Could not read local integration state — starting runtime immediately");
10111
+ installedIntegrationCount = null;
10112
+ }
10113
+ if (shouldDeferFirstRuntimeStart({
10114
+ managed,
10115
+ installedIntegrationCount
10116
+ })) {
10117
+ logger.info("Fresh managed box — deferring first runtime start until the first reconcile settles");
10118
+ const deferral = new DeferredRuntimeStart();
10119
+ firstRuntimeStart = deferral;
10120
+ runtimeGate.deferFirstStart();
10121
+ cloudClient.setOnReconciliationSettled(() => {
10122
+ deferral.release("reconcile-settled");
10123
+ });
10124
+ }
10125
+ }
9877
10126
  cloudClient.start();
9878
10127
  logger.debug("Cloud client started");
9879
10128
  if (!config.autoStartRuntime && config.runtime) logger.warn({ runtime: config.runtime }, "Runtime configured but auto_start_runtime=false — runtime will not be spawned; in-runtime plugins (chat/console/sync) will be offline until you start it manually");
@@ -9892,8 +10141,15 @@ async function startDaemon() {
9892
10141
  env: config.runtime === "claude-code" && config.defaultModel ? { ALFE_DEFAULT_MODEL: config.defaultModel } : {}
9893
10142
  });
9894
10143
  runtimeProcess.setTurnActivityProbe(turnActivityProbe);
9895
- runtimeProcess.start();
9896
- logger.debug("Runtime process started");
10144
+ if (firstRuntimeStart) {
10145
+ firstRuntimeStart.attach(() => {
10146
+ runtimeGate.requestStart();
10147
+ });
10148
+ logger.debug("Runtime process created — first start deferred");
10149
+ } else {
10150
+ runtimeGate.requestStart();
10151
+ logger.debug("Runtime process started");
10152
+ }
9897
10153
  if (config.runtime === "openclaw") {
9898
10154
  stopPairingApprovalPoller = startPairingApprovalPoller({
9899
10155
  log: logger,
@@ -9913,9 +10169,10 @@ async function startDaemon() {
9913
10169
  stopPairingApprovalPoller();
9914
10170
  stopPairingApprovalPoller = null;
9915
10171
  }
10172
+ firstRuntimeStart?.cancel();
9916
10173
  if (runtimeProcess) {
9917
10174
  logger.debug("Stopping runtime process...");
9918
- await runtimeProcess.stop();
10175
+ await runtimeProcess.shutdown();
9919
10176
  logger.debug("Runtime process stopped");
9920
10177
  }
9921
10178
  if (hermesMcpSync) {
@@ -15413,6 +15413,14 @@ const BACKOFF_INITIAL_MS = 1e3;
15413
15413
  const BACKOFF_MAX_MS = 3e4;
15414
15414
  const STABLE_UPTIME_MS = 6e4;
15415
15415
  /**
15416
+ * Output markers (lower-cased substrings) that distinguish the planned exit-78
15417
+ * from any other EX_CONFIG. 2026.9.5 raises the same startup race two ways:
15418
+ * "OpenClaw migration inputs changed during startup", and
15419
+ * `throwStartupMigrationGuardRejected`'s "OpenClaw startup migrations were
15420
+ * skipped because the selected config changed during startup… Retry startup".
15421
+ */
15422
+ const MIGRATION_RESTART_MARKERS = ["migration inputs changed during startup", "selected config changed during startup"];
15423
+ /**
15416
15424
  * Alfe owns supervision and version rollout; OpenClaw must not self-manage.
15417
15425
  *
15418
15426
  * `OPENCLAW_NODE_UPDATE_RESPAWNED=1` also disables 2026.9.x's node-runtime
@@ -15446,6 +15454,17 @@ var RuntimeProcess = class {
15446
15454
  backoffMs = BACKOFF_INITIAL_MS;
15447
15455
  lastStartTime = 0;
15448
15456
  restartTimer = null;
15457
+ /** Whether the CURRENT run printed one of {@link MIGRATION_RESTART_MARKERS}. Reset on every spawn. */
15458
+ sawMigrationInputsChanged = false;
15459
+ /** Consecutive exit-78 migration restarts treated as planned (see {@link MAX_CONSECUTIVE_MIGRATION_RESTARTS}). */
15460
+ consecutiveMigrationRestarts = 0;
15461
+ /**
15462
+ * Daemon shutdown latch. Once set, `start()` / `resume()` / `restart()` are
15463
+ * permanent no-ops: an in-flight reconcile's final gate resume (or a held
15464
+ * suspension released by the drain loop) must not respawn the runtime after
15465
+ * shutdown stopped it.
15466
+ */
15467
+ shutdownLatched = false;
15449
15468
  ringBuffer = new OutputRingBuffer();
15450
15469
  detector = new ErrorLineDetector();
15451
15470
  throttle = new CaptureThrottle(STABLE_UPTIME_MS);
@@ -15492,9 +15511,10 @@ var RuntimeProcess = class {
15492
15511
  * Start the runtime process.
15493
15512
  */
15494
15513
  start() {
15495
- if (this.stopped || this.child !== null) return;
15514
+ if (this.stopped || this.shutdownLatched || this.child !== null) return;
15496
15515
  const { command, args } = this.resolveCommand();
15497
15516
  this.lastStartTime = Date.now();
15517
+ this.sawMigrationInputsChanged = false;
15498
15518
  log.info({
15499
15519
  runtime: this.options.runtime,
15500
15520
  command,
@@ -15555,8 +15575,18 @@ var RuntimeProcess = class {
15555
15575
  }, "Runtime stopped (expected)");
15556
15576
  return;
15557
15577
  }
15558
- if (!spawnError && code === 0 && signal == null) {
15559
- log.info({
15578
+ const uptime = Date.now() - this.lastStartTime;
15579
+ if (uptime >= 6e4) this.consecutiveMigrationRestarts = 0;
15580
+ const isGraceful = !spawnError && code === 0 && signal == null;
15581
+ const isPlannedMigrationRestart = !spawnError && signal == null && code === 78 && this.sawMigrationInputsChanged && this.consecutiveMigrationRestarts < 1;
15582
+ if (isGraceful || isPlannedMigrationRestart) {
15583
+ if (isPlannedMigrationRestart) {
15584
+ this.consecutiveMigrationRestarts += 1;
15585
+ log.info({
15586
+ runtime: this.options.runtime,
15587
+ code
15588
+ }, "Runtime asked for a restart after its startup migration inputs changed — restarting (planned, not a crash)");
15589
+ } else log.info({
15560
15590
  runtime: this.options.runtime,
15561
15591
  code
15562
15592
  }, "Runtime exited gracefully — restarting");
@@ -15573,7 +15603,6 @@ var RuntimeProcess = class {
15573
15603
  spawnError: spawnError?.message,
15574
15604
  backoffMs: this.backoffMs
15575
15605
  }, "Runtime crashed — scheduling restart with backoff");
15576
- const uptime = Date.now() - this.lastStartTime;
15577
15606
  if (this.detectorFlushTimer) {
15578
15607
  clearTimeout(this.detectorFlushTimer);
15579
15608
  this.detectorFlushTimer = null;
@@ -15619,6 +15648,10 @@ var RuntimeProcess = class {
15619
15648
  stream,
15620
15649
  line
15621
15650
  });
15651
+ if (!this.sawMigrationInputsChanged) {
15652
+ const lower = line.toLowerCase();
15653
+ if (MIGRATION_RESTART_MARKERS.some((marker) => lower.includes(marker))) this.sawMigrationInputsChanged = true;
15654
+ }
15622
15655
  for (const block of this.detector.feed(stream, line)) this.emitErrorBlock(block);
15623
15656
  if (this.detectorFlushTimer) {
15624
15657
  clearTimeout(this.detectorFlushTimer);
@@ -15690,6 +15723,14 @@ var RuntimeProcess = class {
15690
15723
  });
15691
15724
  }
15692
15725
  /**
15726
+ * Final stop for daemon shutdown: latches so nothing can start the runtime
15727
+ * again (see {@link shutdownLatched}), then stops it.
15728
+ */
15729
+ async shutdown() {
15730
+ this.shutdownLatched = true;
15731
+ await this.stop();
15732
+ }
15733
+ /**
15693
15734
  * Stop the runtime, but first wait for any in-flight chat turn to finish
15694
15735
  * (up to a grace deadline). This is the entry point for NON-crash restarts —
15695
15736
  * a mid-turn SIGTERM turns a healthy reply into an "unknown error" for the
@@ -15744,8 +15785,10 @@ var RuntimeProcess = class {
15744
15785
  * the runtime back after a mutating reconcile branch has finished.
15745
15786
  */
15746
15787
  resume() {
15788
+ if (this.shutdownLatched) return;
15747
15789
  this.stopped = false;
15748
15790
  this.backoffMs = BACKOFF_INITIAL_MS;
15791
+ this.consecutiveMigrationRestarts = 0;
15749
15792
  this.start();
15750
15793
  }
15751
15794
  /**
@@ -15766,6 +15809,7 @@ var RuntimeProcess = class {
15766
15809
  else await this.stop();
15767
15810
  this.stopped = false;
15768
15811
  this.backoffMs = BACKOFF_INITIAL_MS;
15812
+ this.consecutiveMigrationRestarts = 0;
15769
15813
  this.start();
15770
15814
  }
15771
15815
  /**
@@ -15774,6 +15818,18 @@ var RuntimeProcess = class {
15774
15818
  get isRunning() {
15775
15819
  return this.child !== null && !this.child.killed;
15776
15820
  }
15821
+ /**
15822
+ * Whether the runtime is live OR about to be: a child exists, or a restart
15823
+ * timer (crash backoff / graceful / migration restart) is pending and will
15824
+ * spawn one. This — not {@link isRunning} — is what the RuntimeGate must
15825
+ * check before a mutation: during the restart window the child is null but
15826
+ * the pending timer would fire `start()` mid-mutation, putting a second
15827
+ * SQLite writer on ~/.openclaw/state. `stop()` cancels the timer, and the
15828
+ * gate's `resume()` brings the runtime back.
15829
+ */
15830
+ get isActive() {
15831
+ return !this.stopped && (this.child !== null || this.restartTimer !== null);
15832
+ }
15777
15833
  };
15778
15834
  //#endregion
15779
15835
  export { ErrorLineDetector as a, createLogger as c, PINNED_OPENCLAW_VERSION as d, parseOpenClawVersion as f, __require as h, CaptureThrottle as i, OPENCLAW_MANAGED_NODE_MAJOR as l, __commonJSMin as m, OPENCLAW_EXTERNAL_SUPERVISOR_ENV as n, STRICT_ERROR_CAPTURE_PROFILE as o, supportsOpenClawNode as p, RuntimeProcess as r, toFingerprintKey as s, DRAIN_GRACE_MS as t, OPENCLAW_NODE_REQUIREMENT as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "@alfe.ai/ai-proxy-local": "^0.0.17",
28
28
  "@alfe.ai/config": "^0.4.1",
29
29
  "@alfe.ai/integration-manifest": "^0.4.2",
30
- "@alfe.ai/integrations": "^0.6.11",
30
+ "@alfe.ai/integrations": "^0.6.12",
31
31
  "@alfe.ai/mcp-bundler": "^0.4.2"
32
32
  },
33
33
  "license": "UNLICENSED",