agent-embassy 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/src/gateway/cli-copy.d.ts +1 -1
  3. package/dist/src/gateway/cli-copy.en.d.ts +3 -1
  4. package/dist/src/gateway/cli-copy.en.js +3 -0
  5. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  6. package/dist/src/gateway/cli-copy.js +2 -0
  7. package/dist/src/gateway/cli-copy.js.map +1 -1
  8. package/dist/src/gateway/cli-copy.zh-CN.d.ts +3 -1
  9. package/dist/src/gateway/cli-copy.zh-CN.js +3 -0
  10. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  11. package/dist/src/gateway/cli.d.ts +2 -2
  12. package/dist/src/gateway/cli.js +48 -2
  13. package/dist/src/gateway/cli.js.map +1 -1
  14. package/dist/src/gateway/codex-app-server.js +1 -1
  15. package/dist/src/gateway/codex-doctor.d.ts +33 -0
  16. package/dist/src/gateway/codex-doctor.js +148 -0
  17. package/dist/src/gateway/codex-doctor.js.map +1 -0
  18. package/dist/src/gateway/codex-local-transport.d.ts +5 -0
  19. package/dist/src/gateway/codex-local-transport.js +5 -0
  20. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  21. package/dist/src/gateway/control.js +40 -3
  22. package/dist/src/gateway/control.js.map +1 -1
  23. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  24. package/dist/src/gateway/dashboard-copy.en.d.ts +5 -0
  25. package/dist/src/gateway/dashboard-copy.en.js +5 -0
  26. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  27. package/dist/src/gateway/dashboard-copy.js +5 -0
  28. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  29. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +5 -0
  30. package/dist/src/gateway/dashboard-copy.zh-CN.js +5 -0
  31. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  32. package/dist/src/gateway/dashboard-model.d.ts +3 -0
  33. package/dist/src/gateway/dashboard-model.js +9 -1
  34. package/dist/src/gateway/dashboard-model.js.map +1 -1
  35. package/dist/src/gateway/dashboard.js +3 -1
  36. package/dist/src/gateway/dashboard.js.map +1 -1
  37. package/dist/src/gateway/live-dashboard-app/app.js +11 -2
  38. package/dist/src/gateway/providers.d.ts +7 -0
  39. package/dist/src/gateway/providers.js +60 -0
  40. package/dist/src/gateway/providers.js.map +1 -1
  41. package/dist/src/gateway/server.js +12 -0
  42. package/dist/src/gateway/server.js.map +1 -1
  43. package/dist/src/gateway/service.d.ts +26 -2
  44. package/dist/src/gateway/service.js +553 -104
  45. package/dist/src/gateway/service.js.map +1 -1
  46. package/dist/src/gateway/store.js +26 -12
  47. package/dist/src/gateway/store.js.map +1 -1
  48. package/dist/src/gateway/types.d.ts +9 -0
  49. package/dist/src/gateway/types.js +2 -0
  50. package/dist/src/gateway/types.js.map +1 -1
  51. package/package.json +1 -1
@@ -8,7 +8,7 @@ import { createCodexRegistrationSuccession, transitionCodexRegistrationSuccessio
8
8
  import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
9
9
  import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
10
10
  import { GatewayStore, } from "./store.js";
11
- import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, gatewayRegistrationIngressPrefixes, arePublicAvailablePeerSnapshots, directionId, parseDirection, projectGatewayPublicSnapshot, } from "./types.js";
11
+ import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, gatewayRegistrationIngressPrefixes, arePublicAvailablePeerSnapshots, directionId, parseDirection, projectGatewayPublicSnapshot, CONNECTOR_OBSERVATION_STALE_AFTER_MS, } from "./types.js";
12
12
  const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
13
13
  const CLAUDE_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
14
14
  const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
@@ -21,6 +21,8 @@ const NATIVE_HELD_THRESHOLD_MS = 1_000;
21
21
  const RUNTIME_ACTIVITY_CAPACITY = 256;
22
22
  const DELIVERY_SETTLEMENT_RETRY_MS = 250;
23
23
  const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
24
+ const CLAUDE_DISCOVERY_REFRESH_MS = 30_000;
25
+ const CODEX_DOCTOR_REFRESH_MS = 30_000;
24
26
  const BOOT_REACTIVATION_TRUST_FAILURE_CODES = new Set([
25
27
  "HOME_INVALID",
26
28
  "MANAGED_CODEX_INVALID",
@@ -127,6 +129,7 @@ export class GatewayService {
127
129
  timers;
128
130
  nativePeerCwd;
129
131
  successionGeneration;
132
+ codexDoctor;
130
133
  mutex = new KeyedMutex();
131
134
  routeBindings = new Map();
132
135
  routeStates = new Map();
@@ -177,8 +180,17 @@ export class GatewayService {
177
180
  callbackCapacity;
178
181
  deliveryCallbackReserve;
179
182
  deliveryTokenCapacity;
183
+ providerDispatchInFlightMessageIds = new Set();
184
+ connectorFreshnessDeadlineAt = new Map();
180
185
  lifecycleTimer;
186
+ lifecyclePublishPending = false;
181
187
  nextDashboardRefreshAt;
188
+ nextClaudeDiscoveryAt;
189
+ claudeDiscoveryScheduled = false;
190
+ claudeDiscoveryInFlight;
191
+ codexDoctorTimer;
192
+ codexDoctorInFlight;
193
+ codexDoctorResult;
182
194
  nextProgressWatchAt;
183
195
  control;
184
196
  /** Coarse controller mutation revision exposed by `health`. */
@@ -225,6 +237,7 @@ export class GatewayService {
225
237
  this.successionGeneration =
226
238
  options.successionGeneration ??
227
239
  createCodexRegistrationGeneration;
240
+ this.codexDoctor = options.codexDoctor;
228
241
  this.callbackCapacity = Math.max(64, options.config.limits.maxRoutes +
229
242
  options.config.limits.maxInFlightMessages * 8);
230
243
  // A callback flood must never crowd out write evidence. One selected route
@@ -273,6 +286,7 @@ export class GatewayService {
273
286
  ? {}
274
287
  : { safeErrorCode: safeCode(observation.safeErrorCode, "ADAPTER_DEGRADED") }),
275
288
  });
289
+ this.recordConnectorFreshness(adapter.identity, observation.health);
276
290
  if (observation.ownedRoute !== undefined) {
277
291
  const route = observation.ownedRoute;
278
292
  const prefix = gatewayRegistrationIngressPrefixes[adapter.identity.provider];
@@ -293,6 +307,14 @@ export class GatewayService {
293
307
  }
294
308
  assertStartActive();
295
309
  }
310
+ await this.refreshClaudeDiscovery({ incrementRevision: false }).catch(() => false);
311
+ await this.refreshCodexDoctor();
312
+ for (const [alias, binding] of this.routeBindings) {
313
+ if (binding.provider === "claude" &&
314
+ this.routeStates.get(alias) === "idle") {
315
+ bootReactivatedIdleAliases.push(alias);
316
+ }
317
+ }
296
318
  await this.recoverCodexSuccessionAfterRestartLocked();
297
319
  bootReactivatedIdleAliases.push(...(await this.reactivateRetainedCodexRoutesAfterRestartLocked()));
298
320
  assertStartActive();
@@ -313,8 +335,11 @@ export class GatewayService {
313
335
  this.control = control;
314
336
  this.running = true;
315
337
  this.nextDashboardRefreshAt = undefined;
338
+ this.nextClaudeDiscoveryAt =
339
+ this.now().getTime() + CLAUDE_DISCOVERY_REFRESH_MS;
316
340
  await this.refreshNextProgressWatchWakeLocked();
317
341
  this.scheduleLifecycleWakeLocked();
342
+ this.scheduleCodexDoctor();
318
343
  for (const alias of bootReactivatedIdleAliases) {
319
344
  this.scheduleDispatch(alias);
320
345
  }
@@ -343,6 +368,12 @@ export class GatewayService {
343
368
  const control = this.control;
344
369
  this.control = undefined;
345
370
  let closeFailed = false;
371
+ if (this.codexDoctorTimer !== undefined) {
372
+ this.timers.clearTimeout(this.codexDoctorTimer);
373
+ this.codexDoctorTimer = undefined;
374
+ }
375
+ await this.codexDoctorInFlight?.catch(() => undefined);
376
+ await this.claudeDiscoveryInFlight?.catch(() => undefined);
346
377
  if (control !== undefined) {
347
378
  try {
348
379
  await control.close();
@@ -359,6 +390,9 @@ export class GatewayService {
359
390
  this.timers.clearTimeout(this.lifecycleTimer);
360
391
  this.lifecycleTimer = undefined;
361
392
  }
393
+ this.nextClaudeDiscoveryAt = undefined;
394
+ this.lifecyclePublishPending = false;
395
+ this.connectorFreshnessDeadlineAt.clear();
362
396
  const quiesceResults = await Promise.allSettled(this.adapters.map(async (adapter) => await adapter.quiesceNativeInbound?.()));
363
397
  if (quiesceResults.some((result) => result.status === "rejected")) {
364
398
  closeFailed = true;
@@ -371,6 +405,13 @@ export class GatewayService {
371
405
  for (const messageId of [...this.messageContexts.keys()]) {
372
406
  if (retainedQueuedMessageIds.has(messageId))
373
407
  continue;
408
+ if (this.providerDispatchInFlightMessageIds.has(messageId)) {
409
+ await this.advanceDeliveryLocked(messageId, {
410
+ type: "dispatch_ambiguous",
411
+ at: this.now().getTime(),
412
+ safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
413
+ });
414
+ }
374
415
  const queued = await this.store.settleQueuedMessage({
375
416
  messageId,
376
417
  state: "cancelled",
@@ -428,6 +469,7 @@ export class GatewayService {
428
469
  this.activeDispatchByTarget.clear();
429
470
  this.scheduledDispatchTargets.clear();
430
471
  this.dispatchRunnerTargets.clear();
472
+ this.providerDispatchInFlightMessageIds.clear();
431
473
  for (const timer of this.heldRedispatchTimers.values()) {
432
474
  this.timers.clearTimeout(timer);
433
475
  }
@@ -592,6 +634,14 @@ export class GatewayService {
592
634
  const registry = this.registryEvidenceByConnector.get(`${connector.provider}\0${connector.host}`);
593
635
  return {
594
636
  ...connector,
637
+ ...(connector.provider === "codex" &&
638
+ this.codexDoctorResult !== undefined
639
+ ? {
640
+ codexDoctor: {
641
+ conditions: this.codexDoctorConditions(connector),
642
+ },
643
+ }
644
+ : {}),
595
645
  ...(connector.provider !== "claude" || registry === undefined
596
646
  ? {}
597
647
  : {
@@ -603,6 +653,70 @@ export class GatewayService {
603
653
  };
604
654
  });
605
655
  }
656
+ recordConnectorFreshness(identity, health) {
657
+ const key = `${identity.provider}\0${identity.hostId}`;
658
+ if (health === "healthy") {
659
+ this.connectorFreshnessDeadlineAt.set(key, this.now().getTime() + CONNECTOR_OBSERVATION_STALE_AFTER_MS + 1);
660
+ }
661
+ else {
662
+ this.connectorFreshnessDeadlineAt.delete(key);
663
+ }
664
+ }
665
+ codexDoctorConditions(connector) {
666
+ const attachment = this.codexDoctorResult?.conditions[0] ?? "unknown";
667
+ const stale = connector.observationAgeMs === undefined ||
668
+ connector.observationAgeMs > CONNECTOR_OBSERVATION_STALE_AFTER_MS;
669
+ if (attachment === "split_brain" || attachment === "orphaned") {
670
+ return stale ? [attachment, "observation_stale"] : [attachment];
671
+ }
672
+ if (stale)
673
+ return ["observation_stale", attachment];
674
+ return [attachment];
675
+ }
676
+ async refreshCodexDoctor() {
677
+ if (this.codexDoctor === undefined)
678
+ return false;
679
+ const next = await this.codexDoctor().catch(() => ({ conditions: ["unknown"] }));
680
+ const changed = JSON.stringify(next.conditions) !==
681
+ JSON.stringify(this.codexDoctorResult?.conditions);
682
+ this.codexDoctorResult = next;
683
+ return changed;
684
+ }
685
+ scheduleCodexDoctor() {
686
+ if (!this.running ||
687
+ this.closing ||
688
+ this.codexDoctor === undefined ||
689
+ this.codexDoctorTimer !== undefined ||
690
+ this.codexDoctorInFlight !== undefined) {
691
+ return;
692
+ }
693
+ const timer = this.timers.setTimeout(() => {
694
+ if (this.codexDoctorTimer !== timer)
695
+ return;
696
+ this.codexDoctorTimer = undefined;
697
+ const operation = this.refreshCodexDoctor()
698
+ .then(async (changed) => {
699
+ if (!changed || this.closing)
700
+ return;
701
+ await this.mutex.run("service", async () => {
702
+ this.revision += 1;
703
+ await this.publish();
704
+ });
705
+ })
706
+ .catch(() => {
707
+ this.dashboardHealthy = false;
708
+ });
709
+ this.codexDoctorInFlight = operation;
710
+ void operation.finally(() => {
711
+ if (this.codexDoctorInFlight === operation) {
712
+ this.codexDoctorInFlight = undefined;
713
+ }
714
+ this.scheduleCodexDoctor();
715
+ });
716
+ }, CODEX_DOCTOR_REFRESH_MS);
717
+ this.codexDoctorTimer = timer;
718
+ timer.unref?.();
719
+ }
606
720
  async observeSnapshot() {
607
721
  return await this.mutex.run("service", async () => {
608
722
  const changed = await this.processLifecycleLocked();
@@ -635,7 +749,11 @@ export class GatewayService {
635
749
  fingerprintSnapshot(snapshot) {
636
750
  // Closed snapshot validation bounds every array and field before this
637
751
  // serialization. Omitting generatedAt is the sole semantic exception.
638
- const { generatedAt: _generatedAt, ...semanticSnapshot } = snapshot;
752
+ const { generatedAt: _generatedAt, connectors, ...snapshotWithoutGeneratedAt } = snapshot;
753
+ const semanticSnapshot = {
754
+ ...snapshotWithoutGeneratedAt,
755
+ connectors: connectors.map(({ observationAgeMs: _observationAgeMs, ...connector }) => connector),
756
+ };
639
757
  const encoded = JSON.stringify(semanticSnapshot);
640
758
  if (Buffer.byteLength(encoded, "utf8") >
641
759
  GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET) {
@@ -901,10 +1019,10 @@ export class GatewayService {
901
1019
  this.endpointRefreshConflictPending);
902
1020
  }
903
1021
  enqueueCallbackWorkerOnly() {
904
- if (this.callbackScheduled)
1022
+ if (this.callbackScheduled || this.callbackWorker !== undefined)
905
1023
  return;
906
1024
  this.callbackScheduled = true;
907
- setImmediate(() => {
1025
+ queueMicrotask(() => {
908
1026
  this.callbackScheduled = false;
909
1027
  const worker = this.mutex.run("service", async () => {
910
1028
  await this.drainCallbackQueueLocked();
@@ -2610,7 +2728,7 @@ export class GatewayService {
2610
2728
  return JSON.stringify({
2611
2729
  inboundMode: snapshot.inboundMode,
2612
2730
  health: snapshot.health,
2613
- connectors: snapshot.connectors.map(({ lastSeenAt: _lastSeenAt, ...connector }) => connector),
2731
+ connectors: snapshot.connectors.map(({ lastSeenAt: _lastSeenAt, observationAgeMs: _observationAgeMs, ...connector }) => connector),
2614
2732
  availablePeers: snapshot.availablePeers.map(({ lastSeenAt: _lastSeenAt, ...peer }) => peer),
2615
2733
  routes: snapshot.routes.map(({ lastSeenAt: _lastSeenAt, ...route }) => route),
2616
2734
  messages: snapshot.messages,
@@ -2619,7 +2737,116 @@ export class GatewayService {
2619
2737
  truncation: snapshot.truncation,
2620
2738
  });
2621
2739
  }
2622
- async refreshClaudeDiscovery() {
2740
+ schedulePeriodicClaudeDiscovery() {
2741
+ if (this.closing ||
2742
+ this.claudeDiscoveryScheduled ||
2743
+ this.claudeDiscoveryInFlight !== undefined) {
2744
+ return;
2745
+ }
2746
+ this.claudeDiscoveryScheduled = true;
2747
+ queueMicrotask(() => {
2748
+ this.claudeDiscoveryScheduled = false;
2749
+ if (this.closing || !this.running)
2750
+ return;
2751
+ const operation = this.refreshClaudeDiscoveryPeriodically().catch(() => {
2752
+ this.dashboardHealthy = false;
2753
+ });
2754
+ this.claudeDiscoveryInFlight = operation;
2755
+ void operation.finally(() => {
2756
+ if (this.claudeDiscoveryInFlight === operation) {
2757
+ this.claudeDiscoveryInFlight = undefined;
2758
+ }
2759
+ });
2760
+ });
2761
+ }
2762
+ async refreshClaudeDiscoveryPeriodically() {
2763
+ const observations = (await Promise.all(this.adapters
2764
+ .filter((adapter) => adapter.identity.provider === "claude")
2765
+ .map(async (adapter) => {
2766
+ try {
2767
+ const snapshot = await adapter.discoverClaudePeers?.();
2768
+ return snapshot === undefined ? undefined : { adapter, snapshot };
2769
+ }
2770
+ catch {
2771
+ return undefined;
2772
+ }
2773
+ }))).filter((item) => item !== undefined);
2774
+ if (this.closing)
2775
+ return;
2776
+ const preparedRebinds = [];
2777
+ const persisted = await this.store.inspectPrivateClaudeRoutes();
2778
+ for (const route of persisted) {
2779
+ if (!route.enabled || route.state !== "stale")
2780
+ continue;
2781
+ const observation = observations.find(({ adapter, snapshot }) => snapshot.complete &&
2782
+ adapter.identity.hostId === route.binding.hostId);
2783
+ if (observation === undefined)
2784
+ continue;
2785
+ const peers = observation.snapshot.peers.filter((peer) => peer.kind === "interactive" &&
2786
+ PUBLIC_ALIAS.test(peer.alias) &&
2787
+ peer.alias.endsWith(`@${observation.adapter.identity.hostId}`));
2788
+ const candidates = peers.filter((peer) => peer.routeHandle === route.binding.routeHandle);
2789
+ const peer = candidates[0];
2790
+ if (candidates.length !== 1 ||
2791
+ peer === undefined ||
2792
+ peers.filter((item) => item.alias === peer.alias).length !== 1) {
2793
+ continue;
2794
+ }
2795
+ const candidate = { ...peer, adapter: observation.adapter };
2796
+ try {
2797
+ this.validatePersistedClaudeCandidate(candidate, route);
2798
+ await this.assertClaudeWorkspaceDisjoint(candidate.adapter, candidate.routeHandle);
2799
+ const selected = await candidate.adapter.selectRoute({
2800
+ alias: candidate.alias,
2801
+ routeHandle: candidate.routeHandle,
2802
+ });
2803
+ if (selected.routeHandle !== candidate.routeHandle) {
2804
+ throw new BridgeError("ROUTE_MISMATCH", "The peer identity changed during selection.");
2805
+ }
2806
+ preparedRebinds.push({
2807
+ candidate,
2808
+ persisted: route,
2809
+ selectedState: selected.state,
2810
+ });
2811
+ }
2812
+ catch {
2813
+ await candidate.adapter.releaseRoute?.(candidate.routeHandle).catch(() => undefined);
2814
+ }
2815
+ }
2816
+ const consumed = new Set();
2817
+ try {
2818
+ await this.mutex.run("service", async () => {
2819
+ if (this.closing || !this.running)
2820
+ return;
2821
+ const changed = await this.refreshClaudeDiscovery({
2822
+ incrementRevision: false,
2823
+ observations,
2824
+ preparedRebinds,
2825
+ consumedPreparedRebinds: consumed,
2826
+ });
2827
+ if (changed) {
2828
+ this.revision += 1;
2829
+ await this.publish();
2830
+ }
2831
+ this.scheduleLifecycleWakeLocked();
2832
+ });
2833
+ }
2834
+ finally {
2835
+ const releasable = await this.mutex.run("service", async () => preparedRebinds.filter((prepared) => {
2836
+ if (consumed.has(prepared))
2837
+ return false;
2838
+ const binding = {
2839
+ ...prepared.candidate.adapter.identity,
2840
+ routeHandle: prepared.candidate.routeHandle,
2841
+ ownerLease: stableLease("claude", prepared.candidate.routeHandle),
2842
+ };
2843
+ const alias = this.bindingAliases.get(bindingKey(binding));
2844
+ return alias === undefined || this.routeStates.get(alias) === "stale";
2845
+ }));
2846
+ await Promise.allSettled(releasable.map(async (prepared) => await prepared.candidate.adapter.releaseRoute?.(prepared.candidate.routeHandle)));
2847
+ }
2848
+ }
2849
+ async refreshClaudeDiscovery(options = {}) {
2623
2850
  const publicBefore = await this.discoveryPublicFingerprint();
2624
2851
  this.candidates.clear();
2625
2852
  const rowsByAlias = new Map();
@@ -2631,14 +2858,33 @@ export class GatewayService {
2631
2858
  peers: [],
2632
2859
  complete: false,
2633
2860
  };
2861
+ let discoverySucceeded = false;
2634
2862
  try {
2635
- discovered = (await adapter.discoverClaudePeers?.()) ?? discovered;
2863
+ const observed = options.observations?.find((item) => item.adapter === adapter);
2864
+ if (observed !== undefined) {
2865
+ discovered = observed.snapshot;
2866
+ discoverySucceeded = true;
2867
+ }
2868
+ else if (options.observations === undefined &&
2869
+ adapter.discoverClaudePeers !== undefined) {
2870
+ discovered = await adapter.discoverClaudePeers();
2871
+ discoverySucceeded = true;
2872
+ }
2636
2873
  }
2637
2874
  catch {
2638
2875
  // Registry observation is dashboard evidence, not Claude mailbox
2639
2876
  // authority. The provider still revalidates the exact target before
2640
2877
  // any write.
2641
2878
  }
2879
+ if (discoverySucceeded && discovered.complete) {
2880
+ await this.store.observeConnector({
2881
+ identity: adapter.identity,
2882
+ health: "healthy",
2883
+ protocol: adapter.protocol,
2884
+ protocolVersion: adapter.protocolVersion,
2885
+ });
2886
+ this.recordConnectorFreshness(adapter.identity, "healthy");
2887
+ }
2642
2888
  this.captureRegistryObservation(adapter, discovered.registry ?? adapter.latestRegistryObservation?.());
2643
2889
  const grouped = new Map();
2644
2890
  const byHandle = new Map();
@@ -2749,7 +2995,21 @@ export class GatewayService {
2749
2995
  const candidate = matches[0];
2750
2996
  if (candidate === undefined)
2751
2997
  continue;
2752
- await this.activatePersistedClaudeCandidate(candidate, route, "peer_identity_reobserved").catch(() => undefined);
2998
+ const prepared = options.preparedRebinds?.find((item) => item.candidate.adapter === candidate.adapter &&
2999
+ item.candidate.routeHandle === candidate.routeHandle &&
3000
+ item.persisted.binding.ownerLease === route.binding.ownerLease);
3001
+ const activated = prepared === undefined
3002
+ ? options.observations === undefined &&
3003
+ await this.activatePersistedClaudeCandidate(candidate, route, "peer_identity_reobserved").then(() => true, () => false)
3004
+ : await this.commitPreparedClaudeRebind(prepared, "peer_identity_reobserved").then(() => {
3005
+ options.consumedPreparedRebinds?.add(prepared);
3006
+ return true;
3007
+ }, () => false);
3008
+ if (activated &&
3009
+ this.running &&
3010
+ this.routeStates.get(candidate.alias) === "idle") {
3011
+ this.scheduleDispatch(candidate.alias);
3012
+ }
2753
3013
  }
2754
3014
  const rows = [];
2755
3015
  for (const [alias, row] of rowsByAlias) {
@@ -2819,7 +3079,7 @@ export class GatewayService {
2819
3079
  }
2820
3080
  this.availablePeers = rows.sort((left, right) => left.alias.localeCompare(right.alias));
2821
3081
  const changed = publicBefore !== (await this.discoveryPublicFingerprint());
2822
- if (changed)
3082
+ if (changed && options.incrementRevision !== false)
2823
3083
  this.revision += 1;
2824
3084
  return changed;
2825
3085
  }
@@ -2873,16 +3133,7 @@ export class GatewayService {
2873
3133
  return this.candidates.get(selector);
2874
3134
  }
2875
3135
  async activatePersistedClaudeCandidate(candidate, persisted, reason) {
2876
- const expectedLease = stableLease("claude", candidate.routeHandle);
2877
- if (!persisted.enabled ||
2878
- persisted.state !== "stale" ||
2879
- persisted.binding.provider !== "claude" ||
2880
- candidate.adapter.identity.provider !== "claude" ||
2881
- persisted.binding.hostId !== candidate.adapter.identity.hostId ||
2882
- persisted.binding.routeHandle !== candidate.routeHandle ||
2883
- persisted.binding.ownerLease !== expectedLease) {
2884
- throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "The persisted Claude selection does not match the exact live session identity.");
2885
- }
3136
+ this.validatePersistedClaudeCandidate(candidate, persisted);
2886
3137
  await this.assertClaudeWorkspaceDisjoint(candidate.adapter, candidate.routeHandle);
2887
3138
  try {
2888
3139
  const selected = await candidate.adapter.selectRoute({
@@ -2892,23 +3143,7 @@ export class GatewayService {
2892
3143
  if (selected.routeHandle !== candidate.routeHandle) {
2893
3144
  throw new BridgeError("ROUTE_MISMATCH", "The peer identity changed during selection.");
2894
3145
  }
2895
- const binding = {
2896
- ...candidate.adapter.identity,
2897
- routeHandle: candidate.routeHandle,
2898
- ownerLease: expectedLease,
2899
- };
2900
- await this.store.rebindStaleRoute({
2901
- alias: persisted.alias,
2902
- newAlias: candidate.alias,
2903
- currentOwnerLease: persisted.binding.ownerLease,
2904
- newBinding: binding,
2905
- reason,
2906
- state: selected.state,
2907
- });
2908
- if (persisted.alias !== candidate.alias) {
2909
- this.forgetBinding(persisted.alias);
2910
- }
2911
- this.rememberBinding(candidate.alias, binding, selected.state);
3146
+ await this.commitPreparedClaudeRebind({ candidate, persisted, selectedState: selected.state }, reason);
2912
3147
  }
2913
3148
  catch (error) {
2914
3149
  await candidate.adapter.releaseRoute?.(candidate.routeHandle).catch(() => {
@@ -2917,6 +3152,44 @@ export class GatewayService {
2917
3152
  throw error;
2918
3153
  }
2919
3154
  }
3155
+ validatePersistedClaudeCandidate(candidate, persisted) {
3156
+ const expectedLease = stableLease("claude", candidate.routeHandle);
3157
+ if (!persisted.enabled ||
3158
+ persisted.state !== "stale" ||
3159
+ persisted.binding.provider !== "claude" ||
3160
+ candidate.adapter.identity.provider !== "claude" ||
3161
+ persisted.binding.hostId !== candidate.adapter.identity.hostId ||
3162
+ persisted.binding.routeHandle !== candidate.routeHandle ||
3163
+ persisted.binding.ownerLease !== expectedLease) {
3164
+ throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "The persisted Claude selection does not match the exact live session identity.");
3165
+ }
3166
+ }
3167
+ async commitPreparedClaudeRebind(prepared, reason) {
3168
+ const { candidate, selectedState } = prepared;
3169
+ const persisted = (await this.store.inspectPrivateRoute(prepared.persisted.alias)) ??
3170
+ (await this.store.inspectPrivateRoute(candidate.alias));
3171
+ if (persisted === undefined) {
3172
+ throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "The persisted Claude selection changed before re-observation committed.");
3173
+ }
3174
+ this.validatePersistedClaudeCandidate(candidate, persisted);
3175
+ const binding = {
3176
+ ...candidate.adapter.identity,
3177
+ routeHandle: candidate.routeHandle,
3178
+ ownerLease: stableLease("claude", candidate.routeHandle),
3179
+ };
3180
+ await this.store.rebindStaleRoute({
3181
+ alias: persisted.alias,
3182
+ newAlias: candidate.alias,
3183
+ currentOwnerLease: persisted.binding.ownerLease,
3184
+ newBinding: binding,
3185
+ reason,
3186
+ state: selectedState,
3187
+ });
3188
+ if (persisted.alias !== candidate.alias) {
3189
+ this.forgetBinding(persisted.alias);
3190
+ }
3191
+ this.rememberBinding(candidate.alias, binding, selectedState);
3192
+ }
2920
3193
  async selectClaudeCandidate(candidate, persisted) {
2921
3194
  if (persisted !== undefined) {
2922
3195
  await this.activatePersistedClaudeCandidate(candidate, persisted, "peer_explicitly_reselected");
@@ -3327,6 +3600,7 @@ export class GatewayService {
3327
3600
  protocol: adapter.protocol,
3328
3601
  protocolVersion: adapter.protocolVersion,
3329
3602
  });
3603
+ this.recordConnectorFreshness(adapter.identity, "healthy");
3330
3604
  await this.store.observeRoute({
3331
3605
  binding,
3332
3606
  state,
@@ -3632,7 +3906,15 @@ export class GatewayService {
3632
3906
  unwrittenOutcome: input.unwrittenOutcome,
3633
3907
  safeErrorCode: input.safeErrorCode,
3634
3908
  };
3635
- const transition = transitionDelivery(tracker.machine, event);
3909
+ const stateAtTermination = tracker.pendingTerminalEvent === undefined &&
3910
+ this.providerDispatchInFlightMessageIds.has(messageId)
3911
+ ? transitionDelivery(tracker.machine, {
3912
+ type: "dispatch_ambiguous",
3913
+ at: observedAt,
3914
+ safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
3915
+ }).state
3916
+ : tracker.machine;
3917
+ const transition = transitionDelivery(stateAtTermination, event);
3636
3918
  const settlement = transition.effects.find((effect) => effect.type === "settle_delivery");
3637
3919
  if (settlement === undefined) {
3638
3920
  throw new BridgeError("ROUTE_TERMINATION_STATE_MISMATCH", "An affected in-flight delivery could not produce an exact terminal route settlement.", true);
@@ -3664,7 +3946,15 @@ export class GatewayService {
3664
3946
  unwrittenOutcome: input.unwrittenOutcome,
3665
3947
  safeErrorCode: input.safeErrorCode,
3666
3948
  };
3667
- const transition = transitionDelivery(tracker.machine, event);
3949
+ const stateAtTermination = tracker.pendingTerminalEvent === undefined &&
3950
+ this.providerDispatchInFlightMessageIds.has(messageId)
3951
+ ? transitionDelivery(tracker.machine, {
3952
+ type: "dispatch_ambiguous",
3953
+ at: observedAt,
3954
+ safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
3955
+ }).state
3956
+ : tracker.machine;
3957
+ const transition = transitionDelivery(stateAtTermination, event);
3668
3958
  const settlement = transition.effects.find((effect) => effect.type === "settle_delivery");
3669
3959
  if (settlement === undefined) {
3670
3960
  throw new BridgeError("PAIR_TERMINATION_STATE_MISMATCH", "An affected in-flight delivery could not produce an exact terminal pair settlement.", true);
@@ -4029,6 +4319,17 @@ export class GatewayService {
4029
4319
  async processLifecycleLocked() {
4030
4320
  const now = this.now().getTime();
4031
4321
  let changed = false;
4322
+ if (this.nextClaudeDiscoveryAt !== undefined &&
4323
+ this.nextClaudeDiscoveryAt <= now) {
4324
+ this.nextClaudeDiscoveryAt = now + CLAUDE_DISCOVERY_REFRESH_MS;
4325
+ this.schedulePeriodicClaudeDiscovery();
4326
+ }
4327
+ for (const [key, deadlineAt] of this.connectorFreshnessDeadlineAt) {
4328
+ if (deadlineAt > now)
4329
+ continue;
4330
+ this.connectorFreshnessDeadlineAt.delete(key);
4331
+ changed = true;
4332
+ }
4032
4333
  if (await this.processProgressWatchesLocked(now))
4033
4334
  changed = true;
4034
4335
  await this.drainPreDeadlineDeliveryCallbacksLocked();
@@ -4166,6 +4467,14 @@ export class GatewayService {
4166
4467
  if (this.nextProgressWatchAt !== undefined) {
4167
4468
  consider(this.nextProgressWatchAt);
4168
4469
  }
4470
+ if (this.nextClaudeDiscoveryAt !== undefined) {
4471
+ consider(this.nextClaudeDiscoveryAt);
4472
+ }
4473
+ for (const deadlineAt of this.connectorFreshnessDeadlineAt.values()) {
4474
+ consider(deadlineAt);
4475
+ }
4476
+ if (this.lifecyclePublishPending)
4477
+ consider(now);
4169
4478
  if (hasNonterminal) {
4170
4479
  this.nextDashboardRefreshAt ??= now + DELIVERY_DASHBOARD_REFRESH_MS;
4171
4480
  consider(this.nextDashboardRefreshAt);
@@ -4181,10 +4490,21 @@ export class GatewayService {
4181
4490
  this.lifecycleTimer = undefined;
4182
4491
  void this.mutex
4183
4492
  .run("service", async () => {
4184
- const changed = await this.processLifecycleLocked();
4185
- if (changed) {
4186
- this.revision += 1;
4187
- await this.publish();
4493
+ try {
4494
+ const changed = await this.processLifecycleLocked();
4495
+ if (changed) {
4496
+ this.revision += 1;
4497
+ this.lifecyclePublishPending = true;
4498
+ }
4499
+ if (this.lifecyclePublishPending) {
4500
+ await this.publish();
4501
+ this.lifecyclePublishPending = false;
4502
+ }
4503
+ this.scheduleLifecycleWakeLocked();
4504
+ }
4505
+ catch {
4506
+ this.dashboardHealthy = false;
4507
+ this.scheduleLifecycleRetryLocked();
4188
4508
  }
4189
4509
  })
4190
4510
  .catch(() => {
@@ -4194,6 +4514,23 @@ export class GatewayService {
4194
4514
  this.lifecycleTimer = timer;
4195
4515
  timer.unref?.();
4196
4516
  }
4517
+ scheduleLifecycleRetryLocked() {
4518
+ if (!this.running || this.closing || this.lifecycleTimer !== undefined) {
4519
+ return;
4520
+ }
4521
+ const timer = this.timers.setTimeout(() => {
4522
+ if (this.lifecycleTimer !== timer)
4523
+ return;
4524
+ this.lifecycleTimer = undefined;
4525
+ void this.mutex.run("service", async () => {
4526
+ this.scheduleLifecycleWakeLocked();
4527
+ }).catch(() => {
4528
+ this.dashboardHealthy = false;
4529
+ });
4530
+ }, DELIVERY_SETTLEMENT_RETRY_MS);
4531
+ this.lifecycleTimer = timer;
4532
+ timer.unref?.();
4533
+ }
4197
4534
  async deliveryStatus(token) {
4198
4535
  return await this.mutex.run("service", async () => {
4199
4536
  if (!DELIVERY_TOKEN.test(token))
@@ -4553,7 +4890,7 @@ export class GatewayService {
4553
4890
  break;
4554
4891
  }
4555
4892
  }
4556
- async awaitDispatchWithNativeHeldLocked(tracker, operation) {
4893
+ async awaitDispatchWithNativeHeld(messageId, operation) {
4557
4894
  const outcome = operation.then((value) => ({ kind: "result", value }), (error) => ({ kind: "error", error }));
4558
4895
  const unwrap = (settled) => {
4559
4896
  if (settled.kind === "error")
@@ -4561,7 +4898,7 @@ export class GatewayService {
4561
4898
  return settled.value;
4562
4899
  };
4563
4900
  while (true) {
4564
- const current = this.deliveryTrackers.get(tracker.messageId);
4901
+ const current = this.deliveryTrackers.get(messageId);
4565
4902
  if (current === undefined ||
4566
4903
  current.nativeReceipt === undefined ||
4567
4904
  current.nativeHeldSent ||
@@ -4586,14 +4923,15 @@ export class GatewayService {
4586
4923
  }
4587
4924
  // A terminal callback may have arrived before the provider's dispatch
4588
4925
  // promise resolved. Let that exact evidence win before emitting held.
4589
- await this.drainDeliveryCallbacksForMessageLocked(tracker.messageId);
4590
- const afterCallbacks = this.deliveryTrackers.get(tracker.messageId);
4591
- if (afterCallbacks === undefined ||
4592
- isTerminalDeliveryMachine(afterCallbacks.machine) ||
4593
- this.now().getTime() >= afterCallbacks.deadlineAt) {
4594
- return unwrap(await outcome);
4595
- }
4596
- await this.publishNativeHeldLocked(afterCallbacks);
4926
+ await this.mutex.run("service", async () => {
4927
+ await this.drainDeliveryCallbacksForMessageLocked(messageId);
4928
+ const afterCallbacks = this.deliveryTrackers.get(messageId);
4929
+ if (afterCallbacks !== undefined &&
4930
+ !isTerminalDeliveryMachine(afterCallbacks.machine) &&
4931
+ this.now().getTime() < afterCallbacks.deadlineAt) {
4932
+ await this.publishNativeHeldLocked(afterCallbacks);
4933
+ }
4934
+ });
4597
4935
  }
4598
4936
  }
4599
4937
  restoreQueuedMessageContextLocked(item, binding, canonicalConversationId) {
@@ -4634,7 +4972,7 @@ export class GatewayService {
4634
4972
  });
4635
4973
  return context;
4636
4974
  }
4637
- async dispatchOne(targetAlias) {
4975
+ async prepareDispatchLocked(targetAlias) {
4638
4976
  if (!this.running ||
4639
4977
  this.closing ||
4640
4978
  this.codexSuccessionDispatchFrozen.has(targetAlias) ||
@@ -4836,14 +5174,20 @@ export class GatewayService {
4836
5174
  return;
4837
5175
  }
4838
5176
  context.sourceBinding ??= { ...sourceBinding };
4839
- let result;
4840
- try {
4841
- result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
5177
+ this.scheduleLifecycleWakeLocked();
5178
+ return {
5179
+ targetAlias,
5180
+ messageId: item.messageId,
5181
+ body: item.body,
5182
+ binding: { ...binding },
5183
+ authorization: context.authorization,
5184
+ steeringWhileBusy,
5185
+ input: {
4842
5186
  sourceAlias: item.sourceAlias,
4843
5187
  sourceProvider: sourceBinding.provider,
4844
5188
  targetAlias: item.targetAlias,
4845
5189
  conversationId: context.conversationId,
4846
- binding,
5190
+ binding: { ...binding },
4847
5191
  authorization: context.authorization,
4848
5192
  messageId: item.messageId,
4849
5193
  text: item.body,
@@ -4853,21 +5197,43 @@ export class GatewayService {
4853
5197
  ...(item.steer === true && item.queuedAhead !== undefined
4854
5198
  ? { queuedAhead: item.queuedAhead }
4855
5199
  : {}),
4856
- ...(progressWatchActive
4857
- ? { progressWatchActive: true }
4858
- : {}),
4859
- }));
5200
+ ...(progressWatchActive ? { progressWatchActive: true } : {}),
5201
+ },
5202
+ };
5203
+ }
5204
+ async commitDispatchLocked(prepared, outcome) {
5205
+ const { targetAlias, messageId, body, binding, authorization, steeringWhileBusy, } = prepared;
5206
+ await this.drainDeliveryCallbacksForMessageLocked(messageId);
5207
+ if (await this.processLifecycleLocked()) {
5208
+ await this.changed();
4860
5209
  }
4861
- catch {
4862
- await this.drainDeliveryCallbacksForMessageLocked(item.messageId);
4863
- if (await this.processLifecycleLocked()) {
4864
- await this.changed();
4865
- }
4866
- if (!this.messageContexts.has(item.messageId)) {
4867
- this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
4868
- return;
4869
- }
4870
- await this.advanceDeliveryLocked(item.messageId, {
5210
+ const context = this.messageContexts.get(messageId);
5211
+ const tracker = this.deliveryTrackers.get(messageId);
5212
+ if (context === undefined || tracker === undefined) {
5213
+ this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
5214
+ return;
5215
+ }
5216
+ const currentTargetAlias = this.bindingAliases.get(context.targetBindingKey) ??
5217
+ this.bindingAliases.get(bindingKey(binding)) ??
5218
+ targetAlias;
5219
+ const currentSelectedBinding = authorization === "selected_route"
5220
+ ? this.routeBindings.get(currentTargetAlias)
5221
+ : undefined;
5222
+ const currentSelectedRoute = authorization === "selected_route"
5223
+ ? await this.store.inspectPrivateRoute(currentTargetAlias)
5224
+ : undefined;
5225
+ const authorityStillCurrent = !(context.authorization !== authorization ||
5226
+ context.targetBindingKey !== bindingKey(binding) ||
5227
+ (authorization === "selected_route" &&
5228
+ (currentSelectedBinding === undefined ||
5229
+ bindingKey(currentSelectedBinding) !== bindingKey(binding) ||
5230
+ currentSelectedRoute === undefined ||
5231
+ !currentSelectedRoute.enabled ||
5232
+ currentSelectedRoute.state === "stale" ||
5233
+ bindingKey(currentSelectedRoute.binding) !== bindingKey(binding) ||
5234
+ this.routeStates.get(currentTargetAlias) === "stale")));
5235
+ if (outcome.kind === "error") {
5236
+ await this.advanceDeliveryLocked(messageId, {
4871
5237
  type: "dispatch_ambiguous",
4872
5238
  at: this.now().getTime(),
4873
5239
  safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
@@ -4876,16 +5242,8 @@ export class GatewayService {
4876
5242
  this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
4877
5243
  return;
4878
5244
  }
4879
- await this.drainDeliveryCallbacksForMessageLocked(item.messageId);
4880
- if (await this.processLifecycleLocked()) {
4881
- await this.changed();
4882
- }
4883
- if (!this.messageContexts.has(item.messageId)) {
4884
- this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
4885
- return;
4886
- }
5245
+ const result = outcome.result;
4887
5246
  if (result.state === "deferred") {
4888
- const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
4889
5247
  const now = this.now().getTime();
4890
5248
  const deferredSafeErrorCode = safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED");
4891
5249
  const retryDelayMs = binding.provider === "claude"
@@ -4897,14 +5255,14 @@ export class GatewayService {
4897
5255
  const retryAt = binding.provider === "claude"
4898
5256
  ? Math.min(now + retryDelayMs, tracker.deadlineAt - 1)
4899
5257
  : now + retryDelayMs;
4900
- await this.advanceDeliveryLocked(item.messageId, {
5258
+ await this.advanceDeliveryLocked(messageId, {
4901
5259
  type: "dispatch_clean_prewrite_failed",
4902
5260
  at: now,
4903
5261
  retryAt,
4904
5262
  safeErrorCode: deferredSafeErrorCode,
4905
5263
  });
4906
5264
  tracker.deliverySafeErrorCode = deferredSafeErrorCode;
4907
- const postDeferred = this.deliveryTrackers.get(item.messageId)?.machine;
5265
+ const postDeferred = this.deliveryTrackers.get(messageId)?.machine;
4908
5266
  if (postDeferred === undefined ||
4909
5267
  isTerminalDeliveryMachine(postDeferred)) {
4910
5268
  await this.changed();
@@ -4915,19 +5273,20 @@ export class GatewayService {
4915
5273
  // evidence. Only the reducer's explicit queued retry phase may return
4916
5274
  // the body to the store queue.
4917
5275
  if (postDeferred.phase !== "queued") {
4918
- await this.advanceDeliveryLocked(item.messageId, {
5276
+ await this.advanceDeliveryLocked(messageId, {
4919
5277
  type: "await_terminal",
4920
5278
  at: this.now().getTime(),
4921
5279
  });
4922
5280
  await this.changed();
4923
5281
  return;
4924
5282
  }
4925
- const requeued = await this.store.requeueInFlightMessage(item.messageId, item.body, deferredSafeErrorCode);
4926
- if (this.activeDispatchByTarget.get(currentTargetAlias) === item.messageId) {
5283
+ const requeued = await this.store.requeueInFlightMessage(messageId, body, deferredSafeErrorCode);
5284
+ if (this.activeDispatchByTarget.get(currentTargetAlias) === messageId) {
4927
5285
  this.activeDispatchByTarget.delete(currentTargetAlias);
4928
5286
  }
4929
5287
  if (requeued.status === "requeued") {
4930
- if (context.authorization === "selected_route") {
5288
+ if (context.authorization === "selected_route" &&
5289
+ authorityStillCurrent) {
4931
5290
  if (binding.provider === "codex" && !steeringWhileBusy) {
4932
5291
  this.routeStates.set(currentTargetAlias, "idle");
4933
5292
  await this.store.observeRoute({
@@ -4940,6 +5299,7 @@ export class GatewayService {
4940
5299
  }
4941
5300
  await this.setNativeSourceStatus(currentTargetAlias, "waiting");
4942
5301
  }
5302
+ await this.publishNativeHeldLocked(tracker);
4943
5303
  }
4944
5304
  else if (requeued.status === "settled") {
4945
5305
  await this.applyTerminalSettlementLocked(requeued.settlement);
@@ -4947,20 +5307,30 @@ export class GatewayService {
4947
5307
  await this.changed();
4948
5308
  return;
4949
5309
  }
5310
+ if (!authorityStillCurrent) {
5311
+ await this.advanceDeliveryLocked(messageId, {
5312
+ type: "dispatch_ambiguous",
5313
+ at: this.now().getTime(),
5314
+ safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
5315
+ });
5316
+ await this.changed();
5317
+ this.scheduleDispatch(currentTargetAlias);
5318
+ return;
5319
+ }
4950
5320
  if (result.state === "pending" || result.state === "accepted") {
4951
5321
  if (result.state === "accepted" &&
4952
- (await this.acceptProviderDeliveryLocked(item.messageId))) {
5322
+ (await this.acceptProviderDeliveryLocked(messageId))) {
4953
5323
  await this.changed();
4954
5324
  }
4955
5325
  else if (result.state === "pending") {
4956
- await this.advanceDeliveryLocked(item.messageId, {
5326
+ await this.advanceDeliveryLocked(messageId, {
4957
5327
  type: "await_terminal",
4958
5328
  at: this.now().getTime(),
4959
5329
  });
4960
5330
  }
4961
5331
  }
4962
5332
  else {
4963
- await this.finishDelivery({ messageId: item.messageId, state: result.state, ...(result.safeErrorCode === undefined ? {} : { safeErrorCode: result.safeErrorCode }), ...(result.replyText === undefined ? {} : { replyText: result.replyText }) });
5333
+ await this.finishDelivery({ messageId, state: result.state, ...(result.safeErrorCode === undefined ? {} : { safeErrorCode: result.safeErrorCode }), ...(result.replyText === undefined ? {} : { replyText: result.replyText }) });
4964
5334
  }
4965
5335
  this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
4966
5336
  }
@@ -4974,27 +5344,100 @@ export class GatewayService {
4974
5344
  return;
4975
5345
  this.scheduledDispatchTargets.add(targetAlias);
4976
5346
  setImmediate(() => {
4977
- this.mutex
4978
- .run("service", async () => {
5347
+ void this.runDispatch(targetAlias).catch(() => {
5348
+ this.dashboardHealthy = false;
5349
+ });
5350
+ });
5351
+ }
5352
+ async runDispatch(targetAlias) {
5353
+ let runnerStarted = false;
5354
+ let initialScheduleConsumed = false;
5355
+ let preparedMessageId;
5356
+ let commitCompleted = false;
5357
+ try {
5358
+ const prepared = await this.mutex.run("service", async () => {
4979
5359
  this.scheduledDispatchTargets.delete(targetAlias);
5360
+ initialScheduleConsumed = true;
4980
5361
  if (this.closing ||
4981
5362
  this.codexSuccessionDispatchFrozen.has(targetAlias) ||
4982
5363
  this.codexSuccessionPoisoned.has(targetAlias)) {
4983
5364
  return;
4984
5365
  }
4985
5366
  this.dispatchRunnerTargets.add(targetAlias);
4986
- try {
4987
- await this.dispatchOne(targetAlias);
4988
- }
4989
- finally {
5367
+ runnerStarted = true;
5368
+ const candidate = await this.prepareDispatchLocked(targetAlias);
5369
+ if (candidate === undefined) {
4990
5370
  this.dispatchRunnerTargets.delete(targetAlias);
4991
5371
  this.scheduleLifecycleWakeLocked();
5372
+ runnerStarted = false;
4992
5373
  }
4993
- })
4994
- .catch(() => {
4995
- this.dashboardHealthy = false;
5374
+ else {
5375
+ preparedMessageId = candidate.messageId;
5376
+ this.providerDispatchInFlightMessageIds.add(candidate.messageId);
5377
+ }
5378
+ return candidate;
4996
5379
  });
4997
- });
5380
+ if (prepared === undefined)
5381
+ return;
5382
+ let outcome;
5383
+ try {
5384
+ const adapter = this.adapter(prepared.binding.provider, prepared.binding.hostId);
5385
+ outcome = {
5386
+ kind: "result",
5387
+ result: await this.awaitDispatchWithNativeHeld(prepared.messageId, adapter.dispatch(prepared.input)),
5388
+ };
5389
+ }
5390
+ catch {
5391
+ outcome = { kind: "error" };
5392
+ }
5393
+ await this.mutex.run("service", async () => {
5394
+ await this.commitDispatchLocked(prepared, outcome);
5395
+ commitCompleted = true;
5396
+ this.providerDispatchInFlightMessageIds.delete(prepared.messageId);
5397
+ preparedMessageId = undefined;
5398
+ this.dispatchRunnerTargets.delete(targetAlias);
5399
+ runnerStarted = false;
5400
+ this.scheduleLifecycleWakeLocked();
5401
+ });
5402
+ }
5403
+ finally {
5404
+ await this.mutex.run("service", async () => {
5405
+ if (preparedMessageId !== undefined) {
5406
+ let releaseUncertaintyMarker = commitCompleted;
5407
+ if (!commitCompleted) {
5408
+ const tracker = this.deliveryTrackers.get(preparedMessageId);
5409
+ if (tracker !== undefined &&
5410
+ !isTerminalDeliveryMachine(tracker.machine)) {
5411
+ try {
5412
+ await this.advanceDeliveryLocked(preparedMessageId, {
5413
+ type: "dispatch_ambiguous",
5414
+ at: this.now().getTime(),
5415
+ safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
5416
+ });
5417
+ await this.changed();
5418
+ releaseUncertaintyMarker = true;
5419
+ }
5420
+ catch {
5421
+ this.dashboardHealthy = false;
5422
+ }
5423
+ }
5424
+ else {
5425
+ releaseUncertaintyMarker = true;
5426
+ }
5427
+ }
5428
+ if (releaseUncertaintyMarker) {
5429
+ this.providerDispatchInFlightMessageIds.delete(preparedMessageId);
5430
+ }
5431
+ }
5432
+ if (runnerStarted) {
5433
+ this.dispatchRunnerTargets.delete(targetAlias);
5434
+ }
5435
+ if (!initialScheduleConsumed) {
5436
+ this.scheduledDispatchTargets.delete(targetAlias);
5437
+ }
5438
+ this.scheduleLifecycleWakeLocked();
5439
+ });
5440
+ }
4998
5441
  }
4999
5442
  scheduleHeldRedispatch(targetAlias) {
5000
5443
  if (this.closing || this.heldRedispatchTimers.has(targetAlias))
@@ -5316,12 +5759,16 @@ export class GatewayService {
5316
5759
  });
5317
5760
  await this.changed();
5318
5761
  await this.setNativeSourceStatus(event.targetAlias, "waiting");
5319
- // Native ingress gets one immediate dispatch opportunity. Only an
5320
- // actual queued/waiting result earns a `held` acknowledgement; an idle
5321
- // fast path can therefore settle with one terminal acknowledgement.
5322
- await this.dispatchOne(event.targetAlias);
5762
+ // Provider I/O runs outside the service mutex. The lifecycle timer emits
5763
+ // `held` only if this exact body remains nonterminal at its prompt
5764
+ // boundary; a fast provider result therefore still produces one
5765
+ // terminal acknowledgement and never freezes control-plane callbacks.
5766
+ this.scheduleDispatch(event.targetAlias);
5323
5767
  const tracker = this.deliveryTrackers.get(queued.messageId);
5324
- if (tracker?.machine.phase === "queued") {
5768
+ const routeState = this.routeStates.get(event.targetAlias);
5769
+ if (tracker?.machine.phase === "queued" &&
5770
+ routeState !== "idle" &&
5771
+ (!steer || routeState !== "busy")) {
5325
5772
  await this.publishNativeHeldLocked(tracker);
5326
5773
  }
5327
5774
  }
@@ -5462,6 +5909,7 @@ export class GatewayService {
5462
5909
  protocol: adapter.protocol,
5463
5910
  protocolVersion: adapter.protocolVersion,
5464
5911
  });
5912
+ this.recordConnectorFreshness(event.current, "healthy");
5465
5913
  const refreshedByHandle = new Map(event.routes.map((route) => [route.routeHandle, route.state]));
5466
5914
  const pendingReanchors = previousRoutes.flatMap((route) => {
5467
5915
  const state = refreshedByHandle.get(route.binding.routeHandle);
@@ -5744,6 +6192,7 @@ export class GatewayService {
5744
6192
  safeErrorCode: safeCode(event.safeErrorCode, "CONNECTOR_DEGRADED"),
5745
6193
  }),
5746
6194
  });
6195
+ this.recordConnectorFreshness(source, event.state === "stale" ? "degraded" : "healthy");
5747
6196
  if (event.state === "stale") {
5748
6197
  if (previousState !== "stale") {
5749
6198
  this.addRuntimeAlert(staleSafeCode, "error", { provider: binding.provider, host: binding.hostId, alias });