agent-embassy 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/CONTRIBUTING.md +69 -6
  3. package/README.md +8 -5
  4. package/README.zh-CN.md +6 -4
  5. package/SECURITY.md +152 -27
  6. package/dist/src/gateway/claude-peer.d.ts +7 -4
  7. package/dist/src/gateway/claude-peer.js +80 -36
  8. package/dist/src/gateway/claude-peer.js.map +1 -1
  9. package/dist/src/gateway/claude-runtime.d.ts +4 -0
  10. package/dist/src/gateway/claude-runtime.js +50 -39
  11. package/dist/src/gateway/claude-runtime.js.map +1 -1
  12. package/dist/src/gateway/cli-copy.d.ts +1 -1
  13. package/dist/src/gateway/cli-copy.en.d.ts +1 -1
  14. package/dist/src/gateway/cli-copy.en.js +1 -1
  15. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  16. package/dist/src/gateway/cli-copy.js +1 -1
  17. package/dist/src/gateway/cli-copy.js.map +1 -1
  18. package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
  19. package/dist/src/gateway/cli-copy.zh-CN.js +1 -1
  20. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  21. package/dist/src/gateway/cli.d.ts +1 -1
  22. package/dist/src/gateway/cli.js +6 -7
  23. package/dist/src/gateway/cli.js.map +1 -1
  24. package/dist/src/gateway/codex-app-server.d.ts +44 -2
  25. package/dist/src/gateway/codex-app-server.js +536 -2
  26. package/dist/src/gateway/codex-app-server.js.map +1 -1
  27. package/dist/src/gateway/codex-local-transport.d.ts +10 -6
  28. package/dist/src/gateway/codex-local-transport.js +101 -56
  29. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  30. package/dist/src/gateway/compatibility.d.ts +42 -3
  31. package/dist/src/gateway/compatibility.js +190 -37
  32. package/dist/src/gateway/compatibility.js.map +1 -1
  33. package/dist/src/gateway/control.js +28 -30
  34. package/dist/src/gateway/control.js.map +1 -1
  35. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  36. package/dist/src/gateway/dashboard-copy.en.d.ts +42 -17
  37. package/dist/src/gateway/dashboard-copy.en.js +56 -31
  38. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  39. package/dist/src/gateway/dashboard-copy.js +42 -17
  40. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  41. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +42 -17
  42. package/dist/src/gateway/dashboard-copy.zh-CN.js +56 -31
  43. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  44. package/dist/src/gateway/dashboard-model.d.ts +199 -11
  45. package/dist/src/gateway/dashboard-model.js +478 -75
  46. package/dist/src/gateway/dashboard-model.js.map +1 -1
  47. package/dist/src/gateway/dashboard.d.ts +3 -0
  48. package/dist/src/gateway/dashboard.js +62 -137
  49. package/dist/src/gateway/dashboard.js.map +1 -1
  50. package/dist/src/gateway/deepseek-detect.d.ts +13 -0
  51. package/dist/src/gateway/deepseek-detect.js +125 -0
  52. package/dist/src/gateway/deepseek-detect.js.map +1 -0
  53. package/dist/src/gateway/live-dashboard-app/app.js +158 -256
  54. package/dist/src/gateway/live-dashboard-assets.js +2 -1
  55. package/dist/src/gateway/live-dashboard-assets.js.map +1 -1
  56. package/dist/src/gateway/live-dashboard-stream.d.ts +2 -2
  57. package/dist/src/gateway/live-dashboard-stream.js +2 -2
  58. package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
  59. package/dist/src/gateway/progress-watch-machine.d.ts +50 -64
  60. package/dist/src/gateway/progress-watch-machine.js +42 -133
  61. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  62. package/dist/src/gateway/provenance-envelope.d.ts +2 -0
  63. package/dist/src/gateway/provenance-envelope.js +13 -3
  64. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  65. package/dist/src/gateway/providers.d.ts +80 -16
  66. package/dist/src/gateway/providers.js +782 -115
  67. package/dist/src/gateway/providers.js.map +1 -1
  68. package/dist/src/gateway/server.d.ts +10 -5
  69. package/dist/src/gateway/server.js +100 -13
  70. package/dist/src/gateway/server.js.map +1 -1
  71. package/dist/src/gateway/service.d.ts +46 -3
  72. package/dist/src/gateway/service.js +363 -101
  73. package/dist/src/gateway/service.js.map +1 -1
  74. package/dist/src/gateway/store.d.ts +23 -28
  75. package/dist/src/gateway/store.js +479 -467
  76. package/dist/src/gateway/store.js.map +1 -1
  77. package/dist/src/gateway/types.d.ts +40 -12
  78. package/dist/src/gateway/types.js +145 -4
  79. package/dist/src/gateway/types.js.map +1 -1
  80. package/docs/CONFIGURATION.md +8 -8
  81. package/docs/CONFIGURATION.zh-CN.md +8 -8
  82. package/docs/DASHBOARD.md +35 -6
  83. package/docs/DASHBOARD.zh-CN.md +1 -1
  84. package/docs/GATEWAY-ARCHITECTURE.md +173 -61
  85. package/package.json +1 -1
  86. package/skills/embassy-peer/SKILL.md +2 -2
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { BridgeError } from "../errors.js";
3
3
  import { KeyedMutex } from "../mutex.js";
4
- import { certifiedCompatibilityVersions, compatibilitySurfaces, evaluateCompatibilityAttestation, isCompatibilityAttestation, } from "./compatibility.js";
4
+ import { certifiedCompatibilityVersions, compatibilitySurfaceDefinitions, evaluateCompatibilityAttestation, isCompatibilityAttestation, } from "./compatibility.js";
5
5
  import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, } from "./control.js";
6
6
  import { publishGatewayDashboard } from "./dashboard.js";
7
7
  import { createDeliveryMachine, isTerminalDeliveryMachine, projectDelivery, projectDeliveryWakeups, transitionDelivery, } from "./delivery-machine.js";
@@ -9,7 +9,7 @@ import { createCodexRegistrationSuccession, transitionCodexRegistrationSuccessio
9
9
  import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
10
10
  import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
11
11
  import { GatewayStore, } from "./store.js";
12
- import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, arePublicAvailablePeerSnapshots, projectGatewayPublicSnapshot, } from "./types.js";
12
+ import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, arePublicAvailablePeerSnapshots, projectPublicCompatibilityCheck, projectGatewayPublicSnapshot, } from "./types.js";
13
13
  const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
14
14
  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;
15
15
  const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
@@ -22,10 +22,37 @@ const NATIVE_HELD_THRESHOLD_MS = 1_000;
22
22
  const RUNTIME_ACTIVITY_CAPACITY = 256;
23
23
  const DELIVERY_SETTLEMENT_RETRY_MS = 250;
24
24
  const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
25
+ const BOOT_REACTIVATION_TRUST_FAILURE_CODES = new Set([
26
+ "HOME_INVALID",
27
+ "MANAGED_CODEX_INVALID",
28
+ "LOCAL_APP_SERVER_ENDPOINT_UNSAFE",
29
+ "APP_SERVER_VERSION_MISMATCH",
30
+ "ENDPOINT_GENERATION_CHANGED",
31
+ "CODEX_ENDPOINT_GENERATION_CHURN",
32
+ "CODEX_FACTORY_ATTESTATION_INVALID",
33
+ "CLEANUP_FAILED",
34
+ "FACTORY_CLOSED",
35
+ "INVALID_CONFIGURATION",
36
+ "CLAUDE_PEER_CALLBACK_UNSAFE",
37
+ "CODEX_ROUTE_CLEANUP_FAILED",
38
+ "CODEX_PROVIDER_CLEANUP_FAILED",
39
+ "CODEX_PROVIDER_CLEANUP_AMBIGUOUS",
40
+ "CODEX_PROVIDER_CLEANUP_TIMEOUT",
41
+ ]);
42
+ function boundedErrorCode(error) {
43
+ if (typeof error !== "object" ||
44
+ error === null ||
45
+ !("code" in error) ||
46
+ typeof error.code !== "string" ||
47
+ !SAFE_CODE.test(error.code)) {
48
+ return undefined;
49
+ }
50
+ return error.code;
51
+ }
25
52
  function progressWatchActivity(text, conversationId, actorAlias, explicitIdleMinutes) {
26
53
  const prefixedOpen = text.startsWith("TRACK:");
27
54
  const completionSignal = text.startsWith("DONE:");
28
- if (completionSignal && (prefixedOpen || explicitIdleMinutes !== undefined)) {
55
+ if (completionSignal && explicitIdleMinutes !== undefined) {
29
56
  throw new BridgeError("PROGRESS_WATCH_SIGNAL_CONFLICT", "A single message cannot both open and complete a progress watch.");
30
57
  }
31
58
  const openIdleMs = explicitIdleMinutes === undefined
@@ -122,6 +149,8 @@ export class GatewayService {
122
149
  config;
123
150
  store;
124
151
  adapters;
152
+ compatibilityObservers;
153
+ compatibilitySurfaceDefinitions;
125
154
  publishDashboard;
126
155
  now;
127
156
  timers;
@@ -133,6 +162,8 @@ export class GatewayService {
133
162
  bindingAliases = new Map();
134
163
  candidates = new Map();
135
164
  availablePeers = [];
165
+ registryEvidenceByConnector = new Map();
166
+ registryAdaptersWithParseableRecords = new Set();
136
167
  conversations = new Map();
137
168
  messageContexts = new Map();
138
169
  providerTurnContinuations = new Map();
@@ -145,6 +176,8 @@ export class GatewayService {
145
176
  deliveryTokens = new Map();
146
177
  runtimeAlerts = [];
147
178
  activityEvents = [];
179
+ /** Startup probe failures that must preflight cross-provider mutations. */
180
+ incompatibleProviderSurfaces = new Map();
148
181
  activeCodexIncompatibility;
149
182
  activitySequence = 0;
150
183
  detachedReceiptWrites = new Set();
@@ -214,6 +247,9 @@ export class GatewayService {
214
247
  constructor(options) {
215
248
  this.config = options.config;
216
249
  this.adapters = [...(options.adapters ?? [])];
250
+ this.compatibilityObservers = [...(options.compatibilityObservers ?? [])];
251
+ this.compatibilitySurfaceDefinitions =
252
+ options.compatibilitySurfaceDefinitions ?? compatibilitySurfaceDefinitions;
217
253
  this.publishDashboard = options.publishDashboard ?? publishGatewayDashboard;
218
254
  this.now = options.now ?? (() => new Date());
219
255
  this.store =
@@ -260,6 +296,12 @@ export class GatewayService {
260
296
  throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
261
297
  }
262
298
  seen.add(key);
299
+ }
300
+ if (this.adapters.length > 0) {
301
+ await this.runAutomaticCompatibilityProbesLocked();
302
+ assertStartActive();
303
+ }
304
+ for (const adapter of this.adapters) {
263
305
  const observation = await adapter.initialize(this.callbacksFor(adapter));
264
306
  assertStartActive();
265
307
  await this.store.observeConnector({
@@ -274,15 +316,6 @@ export class GatewayService {
274
316
  });
275
317
  assertStartActive();
276
318
  }
277
- if (this.adapters.length > 0 &&
278
- this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
279
- adapter.runCompatibilityProbes !== undefined)) {
280
- const attestations = await this.runAutomaticCompatibilityProbesLocked();
281
- const incompatible = attestations.find((surface) => surface.tier === "incompatible");
282
- if (incompatible !== undefined) {
283
- throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
284
- }
285
- }
286
319
  await this.recoverCodexSuccessionAfterRestartLocked();
287
320
  bootReactivatedIdleAliases =
288
321
  await this.reactivateRetainedCodexRoutesAfterRestartLocked();
@@ -338,7 +371,7 @@ export class GatewayService {
338
371
  try {
339
372
  await control.close();
340
373
  }
341
- catch {
374
+ catch (error) {
342
375
  closeFailed = true;
343
376
  }
344
377
  }
@@ -404,7 +437,7 @@ export class GatewayService {
404
437
  await this.drainDetachedReceiptWritesLocked();
405
438
  await this.publish();
406
439
  }
407
- catch {
440
+ catch (error) {
408
441
  closeFailed = true;
409
442
  }
410
443
  const adapterResults = await Promise.allSettled(this.adapters.map(async (adapter) => adapter.close()));
@@ -427,6 +460,9 @@ export class GatewayService {
427
460
  this.deliveryTrackers.clear();
428
461
  this.deliveryTokens.clear();
429
462
  this.runtimeAlerts.length = 0;
463
+ this.incompatibleProviderSurfaces.clear();
464
+ this.registryEvidenceByConnector.clear();
465
+ this.registryAdaptersWithParseableRecords.clear();
430
466
  this.detachedReceiptWrites.clear();
431
467
  this.nativeIngressByConversation.clear();
432
468
  this.callbackQueue.length = 0;
@@ -511,10 +547,7 @@ export class GatewayService {
511
547
  observeSnapshot: async () => await this.observeSnapshot(),
512
548
  deliveryStatus: async (params) => await this.deliveryStatus(params.token),
513
549
  untrack: async (params) => await this.exclusiveDecision(async () => {
514
- const settled = await this.store.settleProgressWatch({
515
- conversationId: params.conversationId,
516
- outcome: "done",
517
- });
550
+ const settled = await this.store.endProgressWatch(params.conversationId);
518
551
  if (!settled) {
519
552
  throw new BridgeError("PROGRESS_WATCH_NOT_FOUND", "No active progress watch matches that conversation token.");
520
553
  }
@@ -540,25 +573,58 @@ export class GatewayService {
540
573
  return (await this.observeSnapshot()).snapshot;
541
574
  }
542
575
  async runAutomaticCompatibilityProbesLocked() {
543
- const bySurface = new Map(this.adapters.map((adapter) => {
544
- if (adapter.compatibilitySurface === undefined ||
545
- adapter.runCompatibilityProbes === undefined) {
546
- throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Every local provider must expose the bounded compatibility probe contract.");
547
- }
548
- const observation = adapter.compatibilitySurface();
549
- return [observation.surface, { adapter, observation }];
576
+ const entries = [
577
+ ...this.adapters.map((adapter) => {
578
+ if (adapter.compatibilitySurface === undefined ||
579
+ adapter.runCompatibilityProbes === undefined) {
580
+ throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Every local provider must expose the bounded compatibility probe contract.");
581
+ }
582
+ return {
583
+ observer: adapter,
584
+ adapter,
585
+ };
586
+ }),
587
+ ...this.compatibilityObservers.map((observer) => ({ observer })),
588
+ ];
589
+ const observedEntries = entries.map((entry) => ({
590
+ ...entry,
591
+ observation: entry.observer.compatibilitySurface(),
550
592
  }));
551
- if (bySurface.size !== compatibilitySurfaces.length ||
552
- compatibilitySurfaces.some((surface) => !bySurface.has(surface))) {
553
- throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "The bounded compatibility probe requires one Claude and one Codex surface.");
554
- }
593
+ const bySurface = new Map(observedEntries.map((entry) => [entry.observation.surface, entry]));
594
+ if (bySurface.size !== entries.length) {
595
+ throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Each compatibility observer must expose one distinct surface.");
596
+ }
597
+ const declared = new Set(this.compatibilitySurfaceDefinitions.map(({ surface }) => surface));
598
+ if ([...bySurface.keys()].some((surface) => !declared.has(surface))) {
599
+ throw new BridgeError("COMPAT_SURFACE_UNDECLARED", "Every compatibility observer must use a declared surface.");
600
+ }
601
+ const forbiddenCodexThreadIds = (await this.store.inspectPrivateCodexRoutes()).map((route) => route.binding.routeHandle);
602
+ const probeContext = {
603
+ stateRoot: this.store.rootDir,
604
+ forbiddenCodexThreadIds,
605
+ };
555
606
  const attestations = [];
556
- for (const surface of compatibilitySurfaces) {
607
+ for (const definition of this.compatibilitySurfaceDefinitions) {
608
+ const { surface } = definition;
557
609
  const entry = bySurface.get(surface);
558
610
  if (entry === undefined) {
611
+ if (!definition.required)
612
+ continue;
559
613
  throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
560
614
  }
561
- const probes = await entry.adapter.runCompatibilityProbes();
615
+ const probes = entry.adapter === undefined
616
+ ? await entry.observer.runCompatibilityProbes()
617
+ : await entry.adapter.runCompatibilityProbes(probeContext);
618
+ if (entry.adapter !== undefined) {
619
+ const writeProbe = entry.adapter.latestWriteCompatibilityProbeObservation?.(entry.adapter.identity.endpointGeneration);
620
+ if (writeProbe?.outcome === "fail") {
621
+ this.addRuntimeAlert(writeProbe.safeErrorCode, writeProbe.safeErrorCode === "CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED" ||
622
+ writeProbe.safeErrorCode === "CODEX_WRITE_PROBE_CLEANUP_UNCONFIRMED"
623
+ ? "error"
624
+ : "warning", { provider: "codex", host: entry.adapter.identity.hostId });
625
+ }
626
+ this.captureRegistryObservation(entry.adapter, entry.adapter.latestRegistryObservation?.());
627
+ }
562
628
  const attestation = evaluateCompatibilityAttestation({
563
629
  surface,
564
630
  version: entry.observation.version,
@@ -567,13 +633,84 @@ export class GatewayService {
567
633
  probes,
568
634
  });
569
635
  await this.store.recordCompatibilityAttestation(attestation);
570
- if (attestation.tier !== "incompatible") {
571
- entry.adapter.acceptCompatibilityAttestation?.(attestation);
636
+ entry.observer.acceptCompatibilityAttestation?.(attestation);
637
+ if (attestation.tier === "incompatible" && entry.adapter !== undefined) {
638
+ const code = attestation.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED";
639
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey(entry.adapter.identity.provider, entry.adapter.identity.hostId), code);
640
+ if (surface === "codex") {
641
+ this.activeCodexIncompatibility = structuredClone(attestation);
642
+ }
643
+ }
644
+ else if (surface === "codex" &&
645
+ attestation.tier === "schema_attested" &&
646
+ entry.adapter !== undefined) {
647
+ // A live but untested App Server schema is useful for observation only.
648
+ // Fence retained boot reactivation before it can trigger endpoint
649
+ // refresh churn; an explicit registration may still re-probe a later,
650
+ // certified generation through the provider's bounded refresh path.
651
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", entry.adapter.identity.hostId), "CODEX_MONITOR_ONLY");
572
652
  }
573
653
  attestations.push(attestation);
574
654
  }
575
655
  return attestations;
576
656
  }
657
+ captureRegistryObservation(adapter, observation) {
658
+ if (adapter.identity.provider !== "claude" ||
659
+ observation === undefined ||
660
+ !Number.isSafeInteger(observation.entriesScanned) ||
661
+ observation.entriesScanned < 0 ||
662
+ !Number.isSafeInteger(observation.parseableRecords) ||
663
+ observation.parseableRecords < 0 ||
664
+ observation.parseableRecords > observation.entriesScanned ||
665
+ !Array.isArray(observation.rejected)) {
666
+ return;
667
+ }
668
+ const counts = new Map();
669
+ for (const row of observation.rejected) {
670
+ if (!row ||
671
+ typeof row !== "object" ||
672
+ typeof row.safeErrorCode !== "string" ||
673
+ !SAFE_CODE.test(row.safeErrorCode) ||
674
+ !Number.isSafeInteger(row.count) ||
675
+ row.count <= 0) {
676
+ continue;
677
+ }
678
+ const next = (counts.get(row.safeErrorCode) ?? 0) + row.count;
679
+ counts.set(row.safeErrorCode, Math.min(Number.MAX_SAFE_INTEGER, next));
680
+ }
681
+ const ordered = [...counts]
682
+ .sort(([left], [right]) => left.localeCompare(right))
683
+ .map(([safeErrorCode, count]) => ({ safeErrorCode, count }));
684
+ const rejected = ordered.slice(0, gatewayPublicSnapshotLimits.registryRejectionCodes);
685
+ if (observation.parseableRecords > 0) {
686
+ this.registryAdaptersWithParseableRecords.add(adapter);
687
+ }
688
+ const hasObservedParseableRecord = this.registryAdaptersWithParseableRecords.has(adapter);
689
+ const row = {
690
+ entriesScanned: observation.entriesScanned,
691
+ parseableRecords: observation.parseableRecords,
692
+ parseableRecordSeenSinceBoot: hasObservedParseableRecord,
693
+ rejected,
694
+ rejectedCodesOmitted: Math.max(0, ordered.length - rejected.length),
695
+ };
696
+ this.registryEvidenceByConnector.set(`${adapter.identity.provider}\0${adapter.identity.hostId}`, row);
697
+ }
698
+ connectorsWithRegistry(connectors) {
699
+ return connectors.map((connector) => {
700
+ const registry = this.registryEvidenceByConnector.get(`${connector.provider}\0${connector.host}`);
701
+ return {
702
+ ...connector,
703
+ ...(connector.provider !== "claude" || registry === undefined
704
+ ? {}
705
+ : {
706
+ registry: {
707
+ ...registry,
708
+ rejected: registry.rejected.map((row) => ({ ...row })),
709
+ },
710
+ }),
711
+ };
712
+ });
713
+ }
577
714
  async observeSnapshot() {
578
715
  return await this.mutex.run("service", async () => {
579
716
  const changed = await this.processLifecycleLocked();
@@ -921,17 +1058,31 @@ export class GatewayService {
921
1058
  await this.publish();
922
1059
  }
923
1060
  this.pruneTransient();
1061
+ const deferredRouteObservations = [];
924
1062
  while (this.hasPendingCallbackWork()) {
925
1063
  // Preserve the complete ordinary queue order. In particular, every
926
1064
  // delivery observation already retained before endpoint replacement is
927
1065
  // reduced before the lifecycle transition can settle its generation.
928
1066
  const ordinaryBatch = this.callbackQueue.splice(0, this.callbackQueue.length);
1067
+ const pendingRefresh = this.endpointRefreshCallback?.value;
929
1068
  for (const event of ordinaryBatch) {
930
1069
  try {
931
1070
  if (event.type === "delivery") {
932
1071
  await this.onDelivery(event.source, event.value, event.receivedAt);
933
1072
  }
934
1073
  else if (event.type === "route") {
1074
+ if (pendingRefresh?.outcome === "compatible" &&
1075
+ event.source.provider === "codex" &&
1076
+ event.source.hostId === pendingRefresh.current.hostId &&
1077
+ event.source.endpointGeneration ===
1078
+ pendingRefresh.current.endpointGeneration &&
1079
+ pendingRefresh.routes.some((route) => route.routeHandle === event.value.routeHandle)) {
1080
+ // The provider can observe G2 after staging it but before this
1081
+ // worker installs G2's durable binding. Preserve that fresher
1082
+ // state until the exact transition evidence has been reduced.
1083
+ deferredRouteObservations.push(event);
1084
+ continue;
1085
+ }
935
1086
  await this.onRouteState(event.source, event.value);
936
1087
  }
937
1088
  else if (event.type === "claude_reply") {
@@ -994,6 +1145,14 @@ export class GatewayService {
994
1145
  this.endpointRefreshCallback = requestedRetry;
995
1146
  }
996
1147
  }
1148
+ for (const event of deferredRouteObservations) {
1149
+ try {
1150
+ await this.onRouteState(event.source, event.value);
1151
+ }
1152
+ catch {
1153
+ this.dashboardHealthy = false;
1154
+ }
1155
+ }
997
1156
  }
998
1157
  /**
999
1158
  * A selector can synchronously admit callbacks while its promise is still
@@ -1149,6 +1308,23 @@ export class GatewayService {
1149
1308
  throw new BridgeError("PROVIDER_UNAVAILABLE", "The selected provider is unavailable.", true);
1150
1309
  return adapter;
1151
1310
  }
1311
+ providerSurfaceKey(provider, host) {
1312
+ return `${provider}@${host}`;
1313
+ }
1314
+ incompatibleProviderCode(provider, host) {
1315
+ return this.incompatibleProviderSurfaces.get(this.providerSurfaceKey(provider, host));
1316
+ }
1317
+ /** Reject before either half of a cross-provider mutation can acquire state. */
1318
+ assertCrossProviderMutationCompatible(host, options = { allowCodexRefresh: false }) {
1319
+ const claudeCode = this.incompatibleProviderCode("claude", host);
1320
+ if (claudeCode !== undefined) {
1321
+ throw new BridgeError(claudeCode, "The Claude provider is observation-only until Embassy restarts with a compatible runtime.");
1322
+ }
1323
+ const codexCode = this.incompatibleProviderCode("codex", host);
1324
+ if (codexCode !== undefined && !options.allowCodexRefresh) {
1325
+ throw new BridgeError(codexCode, "The Codex provider is observation-only until a compatible endpoint is re-observed.");
1326
+ }
1327
+ }
1152
1328
  async assertClaudeWorkspaceDisjoint(adapter, routeHandle) {
1153
1329
  if (adapter.identity.provider !== "claude") {
1154
1330
  throw new BridgeError("CLAUDE_PROVIDER_MISMATCH", "The selected workspace guard does not belong to Claude.");
@@ -1219,6 +1395,13 @@ export class GatewayService {
1219
1395
  this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1220
1396
  continue;
1221
1397
  }
1398
+ if (this.incompatibleProviderCode("codex", route.binding.hostId) !==
1399
+ undefined ||
1400
+ this.incompatibleProviderCode("claude", route.binding.hostId) !==
1401
+ undefined) {
1402
+ this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1403
+ continue;
1404
+ }
1222
1405
  let routeSelected = false;
1223
1406
  let advertiseAttempted = false;
1224
1407
  let storeReactivated = false;
@@ -1308,7 +1491,7 @@ export class GatewayService {
1308
1491
  if (selected.state === "idle")
1309
1492
  idleAliases.push(route.alias);
1310
1493
  }
1311
- catch {
1494
+ catch (error) {
1312
1495
  let cleanupFailed = false;
1313
1496
  if (advertiseAttempted) {
1314
1497
  await claudeAdapter
@@ -1327,6 +1510,11 @@ export class GatewayService {
1327
1510
  if (cleanupFailed || storeReactivated || deferredEndpointRefresh) {
1328
1511
  throw new BridgeError("CODEX_BOOT_REACTIVATION_CLEANUP_FAILED", "A Codex boot reactivation crossed its durable boundary without exact live authority.");
1329
1512
  }
1513
+ const failureCode = boundedErrorCode(error);
1514
+ if (failureCode !== undefined &&
1515
+ BOOT_REACTIVATION_TRUST_FAILURE_CODES.has(failureCode)) {
1516
+ throw error;
1517
+ }
1330
1518
  this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1331
1519
  }
1332
1520
  }
@@ -2119,6 +2307,9 @@ export class GatewayService {
2119
2307
  if (!params.alias.startsWith("codex-")) {
2120
2308
  throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
2121
2309
  }
2310
+ this.assertCrossProviderMutationCompatible(params.hostId, {
2311
+ allowCodexRefresh: params.succeedsAlias === undefined,
2312
+ });
2122
2313
  if (this.codexSuccessionPoisoned.has(params.alias) ||
2123
2314
  this.codexOrphanCleanupPending.has(params.alias) ||
2124
2315
  (params.succeedsAlias !== undefined &&
@@ -2338,7 +2529,7 @@ export class GatewayService {
2338
2529
  unwrittenOutcome: "cancelled",
2339
2530
  safeErrorCode: "ROUTE_UNREGISTERED",
2340
2531
  });
2341
- const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements);
2532
+ const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements, "operator");
2342
2533
  for (const settlement of settlements) {
2343
2534
  await this.applyTerminalSettlementLocked(settlement);
2344
2535
  }
@@ -2573,6 +2764,7 @@ export class GatewayService {
2573
2764
  const base = await this.store.publicSnapshot();
2574
2765
  const snapshot = projectGatewayPublicSnapshot({
2575
2766
  ...base,
2767
+ connectors: this.connectorsWithRegistry(base.connectors),
2576
2768
  availablePeers: this.availablePeers.map((peer) => ({ ...peer })),
2577
2769
  });
2578
2770
  return JSON.stringify({
@@ -2607,6 +2799,7 @@ export class GatewayService {
2607
2799
  // authority. The provider still revalidates the exact target before
2608
2800
  // any write.
2609
2801
  }
2802
+ this.captureRegistryObservation(adapter, discovered.registry ?? adapter.latestRegistryObservation?.());
2610
2803
  const grouped = new Map();
2611
2804
  const byHandle = new Map();
2612
2805
  for (const peer of discovered.peers) {
@@ -3094,6 +3287,32 @@ export class GatewayService {
3094
3287
  await this.selectAndPairClaude(params.alias, await this.inferCodexAlias(params.codexThreadId));
3095
3288
  }
3096
3289
  async selectAndPairClaude(selector, codexAlias) {
3290
+ const codexBinding = this.routeBindings.get(codexAlias);
3291
+ const retainedCodexRoute = codexBinding?.provider === "codex"
3292
+ ? undefined
3293
+ : await this.store.inspectPrivateRoute(codexAlias);
3294
+ const codexHost = codexBinding?.provider === "codex"
3295
+ ? codexBinding.hostId
3296
+ : retainedCodexRoute?.binding.provider === "codex"
3297
+ ? retainedCodexRoute.binding.hostId
3298
+ : undefined;
3299
+ if (codexHost !== undefined) {
3300
+ this.assertCrossProviderMutationCompatible(codexHost);
3301
+ }
3302
+ const codexAdapter = codexBinding?.provider === "codex"
3303
+ ? this.adapters.find((adapter) => adapter.identity.provider === "codex" &&
3304
+ adapter.identity.hostId === codexBinding.hostId)
3305
+ : undefined;
3306
+ const codexState = this.routeStates.get(codexAlias);
3307
+ if (codexBinding?.provider !== "codex" ||
3308
+ codexAdapter === undefined ||
3309
+ codexAdapter.identity.endpointGeneration !==
3310
+ codexBinding.endpointGeneration ||
3311
+ (codexState !== "idle" &&
3312
+ codexState !== "busy" &&
3313
+ codexState !== "awaiting_approval")) {
3314
+ throw new BridgeError("CODEX_ROUTE_UNAVAILABLE", "Claude selection requires an exact positively observed live Codex route.");
3315
+ }
3097
3316
  const discoveryChanged = await this.refreshClaudeDiscovery();
3098
3317
  const candidate = this.claudeCandidate(selector);
3099
3318
  if (candidate === undefined)
@@ -3201,6 +3420,16 @@ export class GatewayService {
3201
3420
  this.routeStates.set(alias, state);
3202
3421
  this.bindingAliases.set(bindingKey(binding), alias);
3203
3422
  }
3423
+ reanchorQueuedMessageContexts(alias, previous, current) {
3424
+ const previousKey = bindingKey(previous);
3425
+ const currentKey = bindingKey(current);
3426
+ for (const context of this.messageContexts.values()) {
3427
+ if (context.targetAlias === alias &&
3428
+ context.targetBindingKey === previousKey) {
3429
+ context.targetBindingKey = currentKey;
3430
+ }
3431
+ }
3432
+ }
3204
3433
  forgetBinding(alias) {
3205
3434
  const heldTimer = this.heldRedispatchTimers.get(alias);
3206
3435
  if (heldTimer !== undefined) {
@@ -3924,14 +4153,7 @@ export class GatewayService {
3924
4153
  return false;
3925
4154
  }
3926
4155
  const actions = await this.store.advanceDueProgressWatches();
3927
- let changed = actions.length > 0;
3928
4156
  for (const action of actions) {
3929
- if (action.type === "settled") {
3930
- this.addRuntimeAlert(action.outcome === "unresponsive"
3931
- ? "PROGRESS_WATCH_UNRESPONSIVE"
3932
- : "PROGRESS_WATCH_ENDPOINT_RETIRED", action.outcome === "unresponsive" ? "error" : "warning", { alias: action.ownerAlias });
3933
- continue;
3934
- }
3935
4157
  const text = `[Embassy automated liveness check — ${action.conversationId} / ` +
3936
4158
  `Embassy 自动活跃检查 — ${action.conversationId}]\n` +
3937
4159
  "Reply with the result or a status. / 请回复结果或状态。";
@@ -3945,20 +4167,13 @@ export class GatewayService {
3945
4167
  nudgeNumber: action.nudgeNumber,
3946
4168
  },
3947
4169
  });
3948
- this.addRuntimeAlert("PROGRESS_WATCH_NUDGE", "warning", {
3949
- alias: action.ownerAlias,
3950
- });
3951
4170
  }
3952
4171
  catch {
3953
- await this.store.deferProgressWatchNudge(action.conversationId);
3954
- this.addRuntimeAlert("PROGRESS_WATCH_NUDGE_DEFERRED", "warning", {
3955
- alias: action.ownerAlias,
3956
- });
3957
- changed = true;
4172
+ await this.store.deferProgressWatchNudge(action.conversationId, action.nudgeNumber);
3958
4173
  }
3959
4174
  }
3960
4175
  await this.refreshNextProgressWatchWakeLocked();
3961
- return changed;
4176
+ return actions.length > 0;
3962
4177
  }
3963
4178
  async processLifecycleLocked() {
3964
4179
  const now = this.now().getTime();
@@ -4253,6 +4468,9 @@ export class GatewayService {
4253
4468
  const conversationId = options.existingConversationId ?? createGatewayConversationId();
4254
4469
  const conversation = this.conversations.get(conversationId) ?? {
4255
4470
  id: conversationId,
4471
+ ...(options.existingConversationId === undefined
4472
+ ? {}
4473
+ : { dedupeScope: createGatewayConversationId() }),
4256
4474
  sourceAlias,
4257
4475
  targetAlias,
4258
4476
  expectsReply,
@@ -4269,7 +4487,7 @@ export class GatewayService {
4269
4487
  sourceAlias,
4270
4488
  targetAlias,
4271
4489
  body: text,
4272
- dedupeKey: `${conversationId}:${sequence}`,
4490
+ dedupeKey: `${conversation.dedupeScope ?? conversationId}:${sequence}`,
4273
4491
  conversationIdSuffix: conversationId.slice(-8),
4274
4492
  deadlineAt,
4275
4493
  ...(options.steer === true ? { steer: true } : {}),
@@ -4348,7 +4566,7 @@ export class GatewayService {
4348
4566
  binding: capability.binding,
4349
4567
  },
4350
4568
  body: text,
4351
- dedupeKey: `${conversation.id}:${sequence}`,
4569
+ dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
4352
4570
  conversationIdSuffix: conversation.id.slice(-8),
4353
4571
  deadlineAt,
4354
4572
  ...(conversation.pair === true ? { pair: true } : {}),
@@ -4410,7 +4628,7 @@ export class GatewayService {
4410
4628
  },
4411
4629
  targetAlias: conversation.sourceAlias,
4412
4630
  body: text,
4413
- dedupeKey: `${conversation.id}:${sequence}`,
4631
+ dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
4414
4632
  conversationIdSuffix: conversation.id.slice(-8),
4415
4633
  deadlineAt,
4416
4634
  authorizedPairTeardownReply: true,
@@ -4525,14 +4743,17 @@ export class GatewayService {
4525
4743
  await this.publishNativeHeldLocked(afterCallbacks);
4526
4744
  }
4527
4745
  }
4528
- restoreQueuedMessageContextLocked(item, binding) {
4746
+ restoreQueuedMessageContextLocked(item, binding, canonicalConversationId) {
4529
4747
  this.pruneTransient();
4530
- const generatedConversationId = createGatewayConversationId();
4531
- const conversationId = item.conversationIdSuffix === undefined
4532
- ? generatedConversationId
4533
- : `${generatedConversationId.slice(0, -8)}${item.conversationIdSuffix}`;
4534
- const conversation = {
4748
+ const conversationId = canonicalConversationId ??
4749
+ (item.conversationIdSuffix === undefined
4750
+ ? createGatewayConversationId()
4751
+ : `${createGatewayConversationId().slice(0, -8)}${item.conversationIdSuffix}`);
4752
+ const conversation = this.conversations.get(conversationId) ?? {
4535
4753
  id: conversationId,
4754
+ ...(canonicalConversationId === undefined
4755
+ ? {}
4756
+ : { dedupeScope: createGatewayConversationId() }),
4536
4757
  sourceAlias: item.sourceAlias,
4537
4758
  targetAlias: item.targetAlias,
4538
4759
  expectsReply: false,
@@ -4548,7 +4769,6 @@ export class GatewayService {
4548
4769
  authorization: "selected_route",
4549
4770
  targetAlias: item.targetAlias,
4550
4771
  deadlineAt: item.deadlineAt,
4551
- recovered: true,
4552
4772
  };
4553
4773
  this.conversations.set(conversationId, conversation);
4554
4774
  this.messageContexts.set(item.messageId, context);
@@ -4589,6 +4809,7 @@ export class GatewayService {
4589
4809
  return;
4590
4810
  const selectedBinding = this.routeBindings.get(targetAlias);
4591
4811
  let context = this.messageContexts.get(item.messageId);
4812
+ let progressWatchActive;
4592
4813
  if (context === undefined && selectedBinding === undefined) {
4593
4814
  const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
4594
4815
  if (result.status === "settled") {
@@ -4598,7 +4819,15 @@ export class GatewayService {
4598
4819
  return;
4599
4820
  }
4600
4821
  if (context === undefined && selectedBinding !== undefined) {
4601
- context = this.restoreQueuedMessageContextLocked(item, selectedBinding);
4822
+ const resolution = item.pair === true && item.conversationIdSuffix !== undefined
4823
+ ? await this.store.resolveProgressWatchDispatch({
4824
+ recoveredConversationIdSuffix: item.conversationIdSuffix,
4825
+ sourceAlias: item.sourceAlias,
4826
+ targetAlias: item.targetAlias,
4827
+ })
4828
+ : undefined;
4829
+ context = this.restoreQueuedMessageContextLocked(item, selectedBinding, resolution?.conversationId);
4830
+ progressWatchActive = resolution?.markerActive === true;
4602
4831
  }
4603
4832
  if (context === undefined) {
4604
4833
  const result = await this.store.settleMessage({
@@ -4634,22 +4863,11 @@ export class GatewayService {
4634
4863
  inspection?.state === "stale" ||
4635
4864
  inspection?.safeErrorCode === "CODEX_ROUTE_STALE");
4636
4865
  if (codexRouteStale) {
4637
- if (context.recovered === true) {
4638
- const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
4639
- if (result.status === "settled") {
4640
- await this.applyTerminalSettlementLocked(result.settlement);
4641
- }
4642
- await this.changed();
4643
- return;
4866
+ const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
4867
+ if (result.status === "settled") {
4868
+ await this.applyTerminalSettlementLocked(result.settlement);
4644
4869
  }
4645
- await this.advanceDeliveryLocked(item.messageId, {
4646
- type: "route_terminated",
4647
- at: this.now().getTime(),
4648
- unwrittenOutcome: "failed",
4649
- safeErrorCode: "CODEX_ROUTE_STALE",
4650
- });
4651
4870
  await this.changed();
4652
- this.scheduleDispatch(targetAlias);
4653
4871
  return;
4654
4872
  }
4655
4873
  const storedDispatchable = binding.provider === "claude" ||
@@ -4741,23 +4959,16 @@ export class GatewayService {
4741
4959
  this.scheduleLifecycleWakeLocked();
4742
4960
  return;
4743
4961
  }
4744
- let progressWatches = await this.store.inspectProgressWatches();
4745
- if (context.recovered === true &&
4746
- item.pair === true &&
4747
- !item.body.startsWith("DONE:") &&
4748
- progressWatches.some((watch) => watch.capability === "route" &&
4749
- watch.ownerAlias === item.sourceAlias &&
4750
- watch.workerAlias === item.targetAlias)) {
4751
- await this.store.bindProgressWatchConversation({
4752
- conversationId: context.conversationId,
4753
- ownerAlias: item.sourceAlias,
4754
- workerAlias: item.targetAlias,
4755
- });
4756
- progressWatches = await this.store.inspectProgressWatches();
4962
+ if (progressWatchActive === undefined) {
4963
+ progressWatchActive =
4964
+ item.pair === true
4965
+ ? (await this.store.resolveProgressWatchDispatch({
4966
+ conversationId: context.conversationId,
4967
+ sourceAlias: item.sourceAlias,
4968
+ targetAlias: item.targetAlias,
4969
+ })).markerActive
4970
+ : false;
4757
4971
  }
4758
- const progressWatchActive = progressWatches.some((watch) => watch.capability === "conversation" &&
4759
- watch.workerAlias === item.targetAlias &&
4760
- watch.conversationId === context.conversationId);
4761
4972
  let result;
4762
4973
  try {
4763
4974
  result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
@@ -4771,6 +4982,9 @@ export class GatewayService {
4771
4982
  expectsReply: context.expectsReply,
4772
4983
  deadlineAt: item.deadlineAt,
4773
4984
  ...(item.steer === true ? { steer: true } : {}),
4985
+ ...(item.steer === true && item.queuedAhead !== undefined
4986
+ ? { queuedAhead: item.queuedAhead }
4987
+ : {}),
4774
4988
  ...(progressWatchActive
4775
4989
  ? { progressWatchActive: true }
4776
4990
  : {}),
@@ -5304,6 +5518,9 @@ export class GatewayService {
5304
5518
  return activationDeferred;
5305
5519
  }
5306
5520
  finally {
5521
+ if (!succeeded && event.outcome === "compatible") {
5522
+ adapter.releaseEndpointRefreshSelectorClaim?.(event.current.endpointGeneration);
5523
+ }
5307
5524
  if (ownsActiveTransition &&
5308
5525
  this.activeEndpointRefreshCallbackKey === transitionKey) {
5309
5526
  this.activeEndpointRefreshCallback = undefined;
@@ -5404,11 +5621,37 @@ export class GatewayService {
5404
5621
  if (event.outcome === "incompatible") {
5405
5622
  await this.requireEndpointRefreshUnconflicted();
5406
5623
  this.activeCodexIncompatibility = structuredClone(event.attestation);
5624
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.previous.hostId), event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE");
5407
5625
  this.addRuntimeAlert(event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE", "error", { provider: "codex", host: event.previous.hostId });
5408
5626
  this.handledIncompatibleEndpointRefreshes.set(event.previous.hostId, transitionKey);
5409
5627
  await this.changed();
5410
5628
  return false;
5411
5629
  }
5630
+ if (event.attestation.tier === "schema_attested") {
5631
+ // A passing untested-version schema is useful observation, but it is not
5632
+ // durable route or write authority. Settle the provider's pending probe
5633
+ // so a later generation can be examined, while leaving every G1 route
5634
+ // stale and refusing selector/registration activation.
5635
+ await this.requireEndpointRefreshUnconflicted();
5636
+ await this.store.observeConnector({
5637
+ identity: event.current,
5638
+ health: "degraded",
5639
+ compatibility: "unknown",
5640
+ protocol: adapter.protocol,
5641
+ protocolVersion: adapter.protocolVersion,
5642
+ safeErrorCode: "CODEX_MONITOR_ONLY",
5643
+ });
5644
+ adapter.acceptCompatibilityAttestation(event.attestation);
5645
+ this.activeCodexIncompatibility = undefined;
5646
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.current.hostId), "CODEX_MONITOR_ONLY");
5647
+ this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
5648
+ await this.changed();
5649
+ if (options.deferActivation === true ||
5650
+ options.requiredRoute !== undefined) {
5651
+ throw new BridgeError("CODEX_MONITOR_ONLY", "This Codex App Server passed read-only schema probes but cannot authorize a task route or message write.");
5652
+ }
5653
+ return false;
5654
+ }
5412
5655
  await this.store.observeConnector({
5413
5656
  identity: event.current,
5414
5657
  health: "healthy",
@@ -5430,6 +5673,18 @@ export class GatewayService {
5430
5673
  },
5431
5674
  ];
5432
5675
  });
5676
+ const reanchorPendingMessageContexts = () => {
5677
+ for (const route of pendingReanchors) {
5678
+ const previous = previousRoutes.find((candidate) => candidate.alias === route.alias)?.binding;
5679
+ if (previous === undefined)
5680
+ continue;
5681
+ this.reanchorQueuedMessageContexts(route.alias, previous, {
5682
+ ...event.current,
5683
+ routeHandle: route.threadId,
5684
+ ownerLease: route.ownerLease,
5685
+ });
5686
+ }
5687
+ };
5433
5688
  if (pendingReanchors.length > 0) {
5434
5689
  try {
5435
5690
  await this.store.reanchorCodexRoutes({
@@ -5437,6 +5692,7 @@ export class GatewayService {
5437
5692
  newEndpoint: event.current,
5438
5693
  routes: pendingReanchors,
5439
5694
  });
5695
+ reanchorPendingMessageContexts();
5440
5696
  this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
5441
5697
  }
5442
5698
  catch (error) {
@@ -5458,6 +5714,7 @@ export class GatewayService {
5458
5714
  return matches.length === 1;
5459
5715
  });
5460
5716
  if (committedExactly) {
5717
+ reanchorPendingMessageContexts();
5461
5718
  this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
5462
5719
  try {
5463
5720
  await this.store.markConnectorOffline(event.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
@@ -5639,11 +5896,7 @@ export class GatewayService {
5639
5896
  // Provider writes remain closed until every durable and native transition
5640
5897
  // above is complete. This acknowledgement is deliberately the final
5641
5898
  // fallible activation step.
5642
- const accept = adapter.acceptCompatibilityAttestation;
5643
- if (accept === undefined) {
5644
- throw new BridgeError("CODEX_ENDPOINT_REFRESH_ACTIVATION_UNAVAILABLE", "The Codex provider cannot acknowledge the validated endpoint transition.");
5645
- }
5646
- accept.call(adapter, event.attestation);
5899
+ adapter.acceptCompatibilityAttestation(event.attestation);
5647
5900
  }
5648
5901
  catch (error) {
5649
5902
  try {
@@ -5665,6 +5918,7 @@ export class GatewayService {
5665
5918
  }
5666
5919
  this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
5667
5920
  this.activeCodexIncompatibility = undefined;
5921
+ this.incompatibleProviderSurfaces.delete(this.providerSurfaceKey("codex", event.current.hostId));
5668
5922
  return false;
5669
5923
  }
5670
5924
  async onRouteState(source, event) {
@@ -5726,6 +5980,9 @@ export class GatewayService {
5726
5980
  : event.state === "awaiting_approval"
5727
5981
  ? "waiting"
5728
5982
  : "busy");
5983
+ if (previousState === "stale" && event.state !== "stale") {
5984
+ adapter.rearmEndpointRefreshActivation?.(binding.endpointGeneration);
5985
+ }
5729
5986
  }
5730
5987
  if (event.state === "idle" || event.state === "stale") {
5731
5988
  const heldTimer = this.heldRedispatchTimers.get(alias);
@@ -5841,14 +6098,14 @@ export class GatewayService {
5841
6098
  async publicSnapshotLocked() {
5842
6099
  const base = await this.store.publicSnapshot();
5843
6100
  const compatibilityChecks = [];
5844
- for (const surface of compatibilitySurfaces) {
6101
+ for (const { surface } of this.compatibilitySurfaceDefinitions) {
5845
6102
  if (surface === "codex" && this.activeCodexIncompatibility !== undefined) {
5846
6103
  compatibilityChecks.push(structuredClone(this.activeCodexIncompatibility));
5847
6104
  continue;
5848
6105
  }
5849
6106
  const currentVersions = [
5850
- ...new Set(this.adapters.flatMap((adapter) => {
5851
- const observation = adapter.compatibilitySurface?.();
6107
+ ...new Set([...this.adapters, ...this.compatibilityObservers].flatMap((observer) => {
6108
+ const observation = observer.compatibilitySurface?.();
5852
6109
  return observation?.surface === surface ? [observation.version] : [];
5853
6110
  })),
5854
6111
  ].sort((left, right) => left.localeCompare(right));
@@ -5891,7 +6148,12 @@ export class GatewayService {
5891
6148
  });
5892
6149
  return projectGatewayPublicSnapshot({
5893
6150
  ...base,
5894
- ...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
6151
+ connectors: this.connectorsWithRegistry(base.connectors),
6152
+ ...(compatibilityChecks.length === 0
6153
+ ? {}
6154
+ : {
6155
+ compatibilityChecks: compatibilityChecks.map(projectPublicCompatibilityCheck),
6156
+ }),
5895
6157
  availablePeers: peers,
5896
6158
  activityEvents: this.activityEvents.map((event) => ({
5897
6159
  ...event,