agent-embassy 1.0.0 → 1.3.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 (102) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/CONTRIBUTING.md +25 -7
  3. package/README.md +66 -13
  4. package/README.zh-CN.md +41 -13
  5. package/SECURITY.md +80 -42
  6. package/assets/live-dashboard/app.css +14 -5
  7. package/dist/src/gateway/claude-helper-protocol.d.ts +3 -0
  8. package/dist/src/gateway/claude-helper-protocol.js +15 -1
  9. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  10. package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -0
  11. package/dist/src/gateway/claude-helper-supervisor.js +24 -1
  12. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  13. package/dist/src/gateway/claude-runtime.d.ts +0 -2
  14. package/dist/src/gateway/claude-runtime.js +6 -17
  15. package/dist/src/gateway/claude-runtime.js.map +1 -1
  16. package/dist/src/gateway/cli-copy.d.ts +1 -1
  17. package/dist/src/gateway/cli-copy.en.d.ts +4 -1
  18. package/dist/src/gateway/cli-copy.en.js +6 -3
  19. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  20. package/dist/src/gateway/cli-copy.js +2 -0
  21. package/dist/src/gateway/cli-copy.js.map +1 -1
  22. package/dist/src/gateway/cli-copy.zh-CN.d.ts +4 -1
  23. package/dist/src/gateway/cli-copy.zh-CN.js +6 -3
  24. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  25. package/dist/src/gateway/cli.d.ts +2 -5
  26. package/dist/src/gateway/cli.js +49 -73
  27. package/dist/src/gateway/cli.js.map +1 -1
  28. package/dist/src/gateway/codex-app-server.d.ts +1 -7
  29. package/dist/src/gateway/codex-app-server.js +10 -53
  30. package/dist/src/gateway/codex-app-server.js.map +1 -1
  31. package/dist/src/gateway/codex-local-transport.d.ts +10 -3
  32. package/dist/src/gateway/codex-local-transport.js +25 -9
  33. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  34. package/dist/src/gateway/compatibility.d.ts +0 -27
  35. package/dist/src/gateway/compatibility.js +1 -101
  36. package/dist/src/gateway/compatibility.js.map +1 -1
  37. package/dist/src/gateway/config.d.ts +0 -3
  38. package/dist/src/gateway/config.js +4 -12
  39. package/dist/src/gateway/config.js.map +1 -1
  40. package/dist/src/gateway/control.d.ts +11 -20
  41. package/dist/src/gateway/control.js +50 -47
  42. package/dist/src/gateway/control.js.map +1 -1
  43. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  44. package/dist/src/gateway/dashboard-copy.en.d.ts +12 -7
  45. package/dist/src/gateway/dashboard-copy.en.js +26 -21
  46. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  47. package/dist/src/gateway/dashboard-copy.js +12 -7
  48. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  49. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +12 -7
  50. package/dist/src/gateway/dashboard-copy.zh-CN.js +26 -21
  51. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  52. package/dist/src/gateway/dashboard-model.d.ts +7 -9
  53. package/dist/src/gateway/dashboard-model.js +47 -16
  54. package/dist/src/gateway/dashboard-model.js.map +1 -1
  55. package/dist/src/gateway/dashboard.js +10 -10
  56. package/dist/src/gateway/dashboard.js.map +1 -1
  57. package/dist/src/gateway/live-dashboard-app/app.js +65 -55
  58. package/dist/src/gateway/live-dashboard-command.d.ts +5 -1
  59. package/dist/src/gateway/live-dashboard-command.js +18 -6
  60. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  61. package/dist/src/gateway/live-dashboard-http.d.ts +3 -4
  62. package/dist/src/gateway/live-dashboard-http.js +32 -93
  63. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  64. package/dist/src/gateway/live-dashboard-protocol.d.ts +1 -5
  65. package/dist/src/gateway/live-dashboard-protocol.js +0 -42
  66. package/dist/src/gateway/live-dashboard-protocol.js.map +1 -1
  67. package/dist/src/gateway/live-dashboard-server.d.ts +2 -2
  68. package/dist/src/gateway/live-dashboard-server.js +18 -4
  69. package/dist/src/gateway/live-dashboard-server.js.map +1 -1
  70. package/dist/src/gateway/live-dashboard-stream.js +26 -25
  71. package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
  72. package/dist/src/gateway/live-dashboard.d.ts +3 -6
  73. package/dist/src/gateway/live-dashboard.js +6 -42
  74. package/dist/src/gateway/live-dashboard.js.map +1 -1
  75. package/dist/src/gateway/provenance-envelope.d.ts +16 -0
  76. package/dist/src/gateway/provenance-envelope.js +80 -0
  77. package/dist/src/gateway/provenance-envelope.js.map +1 -0
  78. package/dist/src/gateway/providers.d.ts +40 -33
  79. package/dist/src/gateway/providers.js +683 -302
  80. package/dist/src/gateway/providers.js.map +1 -1
  81. package/dist/src/gateway/server.d.ts +2 -0
  82. package/dist/src/gateway/server.js +8 -5
  83. package/dist/src/gateway/server.js.map +1 -1
  84. package/dist/src/gateway/service.d.ts +103 -23
  85. package/dist/src/gateway/service.js +1346 -272
  86. package/dist/src/gateway/service.js.map +1 -1
  87. package/dist/src/gateway/store.d.ts +39 -3
  88. package/dist/src/gateway/store.js +604 -44
  89. package/dist/src/gateway/store.js.map +1 -1
  90. package/dist/src/gateway/types.d.ts +90 -9
  91. package/dist/src/gateway/types.js +6 -0
  92. package/dist/src/gateway/types.js.map +1 -1
  93. package/docs/GATEWAY-ARCHITECTURE.md +155 -56
  94. package/package.json +3 -2
  95. package/skills/embassy-peer/SKILL.md +36 -7
  96. package/dist/src/gateway/claude-compatibility-scratch.d.ts +0 -18
  97. package/dist/src/gateway/claude-compatibility-scratch.js +0 -158
  98. package/dist/src/gateway/claude-compatibility-scratch.js.map +0 -1
  99. package/dist/src/gateway/live-dashboard-bootstrap.d.ts +0 -36
  100. package/dist/src/gateway/live-dashboard-bootstrap.js +0 -176
  101. package/dist/src/gateway/live-dashboard-bootstrap.js.map +0 -1
  102. package/docs/DESIGN.md +0 -134
@@ -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, attachCompatibilityCertification, evaluateCompatibilityAttestation, isCompatibilityCertification, isCompatibilityCertificationReport, isCompatibilityCheckReport, } from "./compatibility.js";
4
+ import { certifiedCompatibilityVersions, compatibilitySurfaces, 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";
@@ -49,6 +49,41 @@ function bindingKey(binding) {
49
49
  binding.ownerLease,
50
50
  ].join("\0");
51
51
  }
52
+ function endpointRefreshCallbackKey(event) {
53
+ const attestation = {
54
+ schemaVersion: event.attestation.schemaVersion,
55
+ surface: event.attestation.surface,
56
+ version: event.attestation.version,
57
+ tier: event.attestation.tier,
58
+ checkedAt: event.attestation.checkedAt,
59
+ probes: event.attestation.probes.map((probe) => ({
60
+ name: probe.name,
61
+ outcome: probe.outcome,
62
+ ...(probe.safeErrorCode === undefined
63
+ ? {}
64
+ : { safeErrorCode: probe.safeErrorCode }),
65
+ })),
66
+ ...(event.attestation.safeErrorCode === undefined
67
+ ? {}
68
+ : { safeErrorCode: event.attestation.safeErrorCode }),
69
+ };
70
+ return event.outcome === "compatible"
71
+ ? JSON.stringify({
72
+ outcome: event.outcome,
73
+ previous: event.previous,
74
+ current: event.current,
75
+ attestation,
76
+ routes: [...event.routes]
77
+ .map((route) => ({ ...route }))
78
+ .sort((left, right) => left.routeHandle.localeCompare(right.routeHandle)),
79
+ })
80
+ : JSON.stringify({
81
+ outcome: event.outcome,
82
+ previous: event.previous,
83
+ candidate: event.candidate,
84
+ attestation,
85
+ });
86
+ }
52
87
  function stableLease(provider, value) {
53
88
  return `lease_${createHash("sha256").update(provider).update("\0").update(value).digest("base64url")}`;
54
89
  }
@@ -107,10 +142,34 @@ export class GatewayService {
107
142
  deliveryTokens = new Map();
108
143
  runtimeAlerts = [];
109
144
  activityEvents = [];
145
+ activeCodexIncompatibility;
110
146
  activitySequence = 0;
111
147
  detachedReceiptWrites = new Set();
112
148
  nativeIngressByConversation = new Map();
113
149
  callbackQueue = [];
150
+ /**
151
+ * Endpoint replacement is authority-bearing lifecycle evidence, not an
152
+ * ordinary refreshable observation. Keep one coalesced slot outside the
153
+ * bounded callback queue so delivery evidence can never displace it (or be
154
+ * displaced by it).
155
+ */
156
+ endpointRefreshCallback;
157
+ activeEndpointRefreshCallback;
158
+ endpointRefreshRetryRequested;
159
+ endpointRefreshConflictBoundary;
160
+ activeEndpointRefreshCallbackKey;
161
+ endpointRefreshCallbackPoisoned = false;
162
+ endpointRefreshConflictPending = false;
163
+ /** Last successfully activated transition per allowed Codex host. */
164
+ activatedEndpointRefreshes = new Map();
165
+ /** Durable reanchors awaiting their exact native-listener activation. */
166
+ durablyAppliedEndpointRefreshes = new Map();
167
+ /** Selector transitions staged until their exact registration owns a helper. */
168
+ pendingSelectorEndpointActivations = new Map();
169
+ /** Last incompatible transition already projected per allowed Codex host. */
170
+ handledIncompatibleEndpointRefreshes = new Map();
171
+ /** Process-local dedupe for automatic public activity rows across retries. */
172
+ endpointRefreshActivityKeys = new Set();
114
173
  callbackWorker;
115
174
  callbackScheduled = false;
116
175
  callbackCapacity;
@@ -136,6 +195,12 @@ export class GatewayService {
136
195
  /** Route-scoped fences; unrelated Codex registrations remain dispatchable. */
137
196
  codexSuccessionDispatchFrozen = new Set();
138
197
  codexSuccessionPoisoned = new Set();
198
+ /**
199
+ * Exact stale authority whose process-owned native advertisement was already
200
+ * removed, but whose durable orphan deletion did not commit. Keep dispatch
201
+ * frozen and permit only the same exact cleanup transaction to retry.
202
+ */
203
+ codexOrphanCleanupPending = new Map();
139
204
  codexSuccessionState;
140
205
  pendingCodexSuccessionRecovery;
141
206
  /**
@@ -171,6 +236,7 @@ export class GatewayService {
171
236
  async start(signal) {
172
237
  if (this.running || this.closing)
173
238
  return;
239
+ let bootReactivatedIdleAliases = [];
174
240
  const assertStartActive = () => {
175
241
  if (signal?.aborted === true || this.closing) {
176
242
  throw new BridgeError("GATEWAY_START_CANCELLED", "Gateway startup was cancelled before it became ready.", true);
@@ -191,7 +257,7 @@ export class GatewayService {
191
257
  throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
192
258
  }
193
259
  seen.add(key);
194
- const observation = await adapter.initialize(this.callbacksFor(adapter.identity));
260
+ const observation = await adapter.initialize(this.callbacksFor(adapter));
195
261
  assertStartActive();
196
262
  await this.store.observeConnector({
197
263
  identity: adapter.identity,
@@ -208,13 +274,15 @@ export class GatewayService {
208
274
  if (this.adapters.length > 0 &&
209
275
  this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
210
276
  adapter.runCompatibilityProbes !== undefined)) {
211
- const report = await this.compatibilityCheckLocked();
212
- const incompatible = report.surfaces.find((surface) => surface.tier === "incompatible");
277
+ const attestations = await this.runAutomaticCompatibilityProbesLocked();
278
+ const incompatible = attestations.find((surface) => surface.tier === "incompatible");
213
279
  if (incompatible !== undefined) {
214
280
  throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
215
281
  }
216
282
  }
217
283
  await this.recoverCodexSuccessionAfterRestartLocked();
284
+ bootReactivatedIdleAliases =
285
+ await this.reactivateRetainedCodexRoutesAfterRestartLocked();
218
286
  assertStartActive();
219
287
  const control = await startGatewayControlServer({
220
288
  stateDir: this.store.rootDir,
@@ -235,6 +303,9 @@ export class GatewayService {
235
303
  this.nextDashboardRefreshAt = undefined;
236
304
  await this.refreshNextProgressWatchWakeLocked();
237
305
  this.scheduleLifecycleWakeLocked();
306
+ for (const alias of bootReactivatedIdleAliases) {
307
+ this.scheduleDispatch(alias);
308
+ }
238
309
  await this.publish();
239
310
  assertStartActive();
240
311
  }
@@ -284,7 +355,10 @@ export class GatewayService {
284
355
  for (const messageId of [...this.providerTurnContinuations.keys()]) {
285
356
  await this.finishProviderTurnContinuationLocked(messageId, "cancelled");
286
357
  }
358
+ const retainedQueuedMessageIds = new Set(await this.store.inspectQueuedMessageIds());
287
359
  for (const messageId of [...this.messageContexts.keys()]) {
360
+ if (retainedQueuedMessageIds.has(messageId))
361
+ continue;
288
362
  const queued = await this.store.settleQueuedMessage({
289
363
  messageId,
290
364
  state: "cancelled",
@@ -300,6 +374,8 @@ export class GatewayService {
300
374
  });
301
375
  }
302
376
  for (const tracker of this.deliveryTrackers.values()) {
377
+ if (retainedQueuedMessageIds.has(tracker.messageId))
378
+ continue;
303
379
  if (!isTerminalDeliveryMachine(tracker.machine)) {
304
380
  await this.advanceDeliveryLocked(tracker.messageId, {
305
381
  type: "shutdown",
@@ -351,6 +427,19 @@ export class GatewayService {
351
427
  this.detachedReceiptWrites.clear();
352
428
  this.nativeIngressByConversation.clear();
353
429
  this.callbackQueue.length = 0;
430
+ this.endpointRefreshCallback = undefined;
431
+ this.activeEndpointRefreshCallback = undefined;
432
+ this.endpointRefreshRetryRequested = undefined;
433
+ this.endpointRefreshConflictBoundary = undefined;
434
+ this.activeEndpointRefreshCallbackKey = undefined;
435
+ this.endpointRefreshConflictPending = false;
436
+ this.endpointRefreshCallbackPoisoned = true;
437
+ this.activatedEndpointRefreshes.clear();
438
+ this.durablyAppliedEndpointRefreshes.clear();
439
+ this.pendingSelectorEndpointActivations.clear();
440
+ this.handledIncompatibleEndpointRefreshes.clear();
441
+ this.endpointRefreshActivityKeys.clear();
442
+ this.codexOrphanCleanupPending.clear();
354
443
  this.candidates.clear();
355
444
  this.routeBindings.clear();
356
445
  this.routeStates.clear();
@@ -390,6 +479,11 @@ export class GatewayService {
390
479
  action: "codex_unregistered",
391
480
  aliases: [params.alias],
392
481
  }),
482
+ removeStaleCodexRegistration: async (params) => await this.exclusiveDecision(async () => this.removeStaleCodexRegistration(params.alias), {
483
+ kind: "recovery",
484
+ action: "codex_orphan_removed",
485
+ aliases: [params.alias],
486
+ }),
393
487
  selectClaude: async (params) => await this.exclusiveDecision(async () => this.selectClaude(params), {
394
488
  kind: "selection",
395
489
  action: "claude_selected",
@@ -412,8 +506,6 @@ export class GatewayService {
412
506
  }),
413
507
  listSnapshot: async () => (await this.observeSnapshot()).snapshot,
414
508
  observeSnapshot: async () => await this.observeSnapshot(),
415
- compatibilityCheck: async () => await this.compatibilityCheck(),
416
- compatibilityCertify: async (params) => await this.compatibilityCertify(params),
417
509
  deliveryStatus: async (params) => await this.deliveryStatus(params.token),
418
510
  untrack: async (params) => await this.exclusiveDecision(async () => {
419
511
  const settled = await this.store.settleProgressWatch({
@@ -444,133 +536,7 @@ export class GatewayService {
444
536
  async snapshot() {
445
537
  return (await this.observeSnapshot()).snapshot;
446
538
  }
447
- async compatibilityCheck() {
448
- return await this.mutex.run("service", async () => await this.compatibilityCheckLocked());
449
- }
450
- async compatibilityCertify(params) {
451
- const plan = await this.mutex.run("service", async () => {
452
- if (!this.running || this.closing) {
453
- throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Compatibility certification requires one running broker.");
454
- }
455
- const report = await this.compatibilityCheckLocked();
456
- const codexRoutes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.enabled);
457
- const codexRoute = params.codexAlias === undefined
458
- ? codexRoutes.length === 1
459
- ? codexRoutes[0]
460
- : undefined
461
- : codexRoutes.find((route) => route.alias === params.codexAlias);
462
- if (codexRoute === undefined) {
463
- throw new BridgeError("COMPAT_CERTIFY_CODEX_ROUTE_REQUIRED", "Choose one exact registered Codex route for live certification.");
464
- }
465
- const adapters = new Map(this.adapters.map((adapter) => [adapter.identity.provider, adapter]));
466
- return { adapters, codexRoute, report };
467
- });
468
- const certifiedAt = this.now().toISOString();
469
- const updated = [];
470
- for (const attestation of plan.report.surfaces) {
471
- const adapter = plan.adapters.get(attestation.surface);
472
- let certification;
473
- if (attestation.tier === "incompatible" ||
474
- adapter?.runCompatibilityCertification === undefined) {
475
- certification = {
476
- depth: attestation.surface === "claude"
477
- ? "wire"
478
- : params.withTurn
479
- ? "turn"
480
- : "thread_ops",
481
- outcome: "fail",
482
- certifiedAt,
483
- safeErrorCode: attestation.safeErrorCode ??
484
- `${attestation.surface.toUpperCase()}_CERTIFICATION_UNAVAILABLE`,
485
- };
486
- }
487
- else {
488
- try {
489
- certification = await adapter.runCompatibilityCertification({
490
- controllerStateRoot: this.store.rootDir,
491
- withTurn: params.withTurn,
492
- ...(attestation.surface === "codex"
493
- ? { routeHandle: plan.codexRoute.binding.routeHandle }
494
- : {}),
495
- });
496
- }
497
- catch {
498
- certification = {
499
- depth: attestation.surface === "claude"
500
- ? "wire"
501
- : params.withTurn
502
- ? "turn"
503
- : "thread_ops",
504
- outcome: "fail",
505
- certifiedAt: this.now().toISOString(),
506
- safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_FAILED`,
507
- };
508
- }
509
- }
510
- const expectedDepth = attestation.surface === "claude"
511
- ? "wire"
512
- : params.withTurn
513
- ? "turn"
514
- : "thread_ops";
515
- if (!isCompatibilityCertification(certification) ||
516
- certification.depth !== expectedDepth) {
517
- certification = {
518
- depth: expectedDepth,
519
- outcome: "fail",
520
- certifiedAt: this.now().toISOString(),
521
- safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_EVIDENCE_INVALID`,
522
- };
523
- }
524
- updated.push(attachCompatibilityCertification(attestation, certification));
525
- }
526
- return await this.mutex.run("service", async () => {
527
- const currentCodexRoute = (await this.store.inspectPrivateCodexRoutes()).find((route) => route.enabled &&
528
- route.alias === plan.codexRoute.alias &&
529
- bindingKey(route.binding) === bindingKey(plan.codexRoute.binding));
530
- const reconciled = updated.map((attestation) => {
531
- if (attestation.surface === "codex" && currentCodexRoute === undefined) {
532
- return attachCompatibilityCertification(attestation, {
533
- depth: params.withTurn ? "turn" : "thread_ops",
534
- outcome: "fail",
535
- certifiedAt: this.now().toISOString(),
536
- safeErrorCode: "CODEX_CERTIFICATION_ROUTE_CHANGED",
537
- });
538
- }
539
- const observation = plan.adapters
540
- .get(attestation.surface)
541
- ?.compatibilitySurface?.();
542
- if (observation?.surface === attestation.surface &&
543
- observation.version === attestation.version) {
544
- return attestation;
545
- }
546
- return attachCompatibilityCertification(attestation, {
547
- depth: attestation.surface === "claude"
548
- ? "wire"
549
- : params.withTurn
550
- ? "turn"
551
- : "thread_ops",
552
- outcome: "fail",
553
- certifiedAt: this.now().toISOString(),
554
- safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_VERSION_CHANGED`,
555
- });
556
- });
557
- const result = {
558
- policy: plan.report.policy,
559
- certified: reconciled.every((attestation) => attestation.certification?.outcome === "pass"),
560
- withTurn: params.withTurn,
561
- surfaces: reconciled,
562
- };
563
- if (!isCompatibilityCertificationReport(result)) {
564
- throw new BridgeError("COMPAT_CERTIFICATION_REPORT_INVALID", "Compatibility certification evidence failed its closed report schema.");
565
- }
566
- for (const attestation of reconciled) {
567
- await this.store.recordCompatibilityAttestation(attestation);
568
- }
569
- await this.changed();
570
- return result;
571
- });
572
- }
573
- async compatibilityCheckLocked() {
539
+ async runAutomaticCompatibilityProbesLocked() {
574
540
  const bySurface = new Map(this.adapters.map((adapter) => {
575
541
  if (adapter.compatibilitySurface === undefined ||
576
542
  adapter.runCompatibilityProbes === undefined) {
@@ -589,39 +555,21 @@ export class GatewayService {
589
555
  if (entry === undefined) {
590
556
  throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
591
557
  }
592
- const cached = await this.store.inspectCompatibilityAttestation(surface, entry.observation.version);
593
- const probes = cached?.probes ?? (await entry.adapter.runCompatibilityProbes());
594
- const checkedAt = cached?.checkedAt ?? this.now().toISOString();
595
- let attestation = evaluateCompatibilityAttestation({
558
+ const probes = await entry.adapter.runCompatibilityProbes();
559
+ const attestation = evaluateCompatibilityAttestation({
596
560
  surface,
597
561
  version: entry.observation.version,
598
- checkedAt,
599
- policy: this.config.compatibilityPolicy ?? "observed",
562
+ checkedAt: this.now().toISOString(),
600
563
  certifiedVersions: certifiedCompatibilityVersions[surface],
601
564
  probes,
602
565
  });
603
- if (cached?.certification !== undefined &&
604
- attestation.tier !== "incompatible") {
605
- attestation = attachCompatibilityCertification(attestation, cached.certification);
606
- }
607
- if (cached === undefined ||
608
- JSON.stringify(cached) !== JSON.stringify(attestation)) {
609
- await this.store.recordCompatibilityAttestation(attestation);
610
- }
566
+ await this.store.recordCompatibilityAttestation(attestation);
611
567
  if (attestation.tier !== "incompatible") {
612
568
  entry.adapter.acceptCompatibilityAttestation?.(attestation);
613
569
  }
614
570
  attestations.push(attestation);
615
571
  }
616
- const report = {
617
- policy: this.config.compatibilityPolicy ?? "observed",
618
- compatible: attestations.every((attestation) => attestation.tier !== "incompatible"),
619
- surfaces: attestations,
620
- };
621
- if (!isCompatibilityCheckReport(report)) {
622
- throw new BridgeError("COMPAT_REPORT_INVALID", "Compatibility evidence failed its closed report schema.");
623
- }
624
- return report;
572
+ return attestations;
625
573
  }
626
574
  async observeSnapshot() {
627
575
  return await this.mutex.run("service", async () => {
@@ -663,9 +611,10 @@ export class GatewayService {
663
611
  }
664
612
  return createHash("sha256").update(encoded, "utf8").digest("base64url");
665
613
  }
666
- callbacksFor(source) {
614
+ callbacksFor(adapter) {
667
615
  return {
668
616
  onDelivery: (event) => {
617
+ const source = adapter.identity;
669
618
  if (!this.running ||
670
619
  !MESSAGE_ID.test(event.messageId) ||
671
620
  this.deliveryCorrelation(event.messageId) === undefined ||
@@ -694,6 +643,7 @@ export class GatewayService {
694
643
  });
695
644
  },
696
645
  onRouteState: (event) => {
646
+ const source = adapter.identity;
697
647
  if (!this.running ||
698
648
  event.endpoint.provider !== source.provider ||
699
649
  event.endpoint.hostId !== source.hostId ||
@@ -716,6 +666,7 @@ export class GatewayService {
716
666
  });
717
667
  },
718
668
  onClaudeReply: (event) => {
669
+ const source = adapter.identity;
719
670
  if (!this.running ||
720
671
  event.endpoint.provider !== source.provider ||
721
672
  event.endpoint.hostId !== source.hostId ||
@@ -742,6 +693,7 @@ export class GatewayService {
742
693
  });
743
694
  },
744
695
  onClaudeMessage: (event) => {
696
+ const source = adapter.identity;
745
697
  const ingressFailureCode = this.closing
746
698
  ? "GATEWAY_SHUTDOWN"
747
699
  : "NATIVE_INGRESS_INVALID";
@@ -779,6 +731,7 @@ export class GatewayService {
779
731
  }
780
732
  },
781
733
  onProtocolNotice: (event) => {
734
+ const source = adapter.identity;
782
735
  if (!this.running || !SAFE_CODE.test(event.code))
783
736
  return;
784
737
  this.enqueueCallback({
@@ -787,11 +740,61 @@ export class GatewayService {
787
740
  value: { code: event.code },
788
741
  });
789
742
  },
743
+ onEndpointRefresh: (event) => {
744
+ if (!this.running ||
745
+ adapter.identity.provider !== "codex" ||
746
+ event.previous.provider !== "codex" ||
747
+ event.previous.hostId !== adapter.identity.hostId ||
748
+ !isCompatibilityAttestation(event.attestation) ||
749
+ event.attestation.surface !== "codex") {
750
+ return;
751
+ }
752
+ if (event.outcome === "compatible") {
753
+ const routeHandles = event.routes.map((route) => route.routeHandle);
754
+ if (event.current.provider !== "codex" ||
755
+ event.current.hostId !== event.previous.hostId ||
756
+ event.current.endpointGeneration !==
757
+ adapter.identity.endpointGeneration ||
758
+ event.attestation.version !== adapter.protocolVersion ||
759
+ event.attestation.tier === "incompatible" ||
760
+ routeHandles.length > this.config.limits.maxRoutes ||
761
+ new Set(routeHandles).size !== routeHandles.length ||
762
+ routeHandles.some((routeHandle) => !PRIVATE_HANDLE.test(routeHandle))) {
763
+ return;
764
+ }
765
+ }
766
+ else if (event.candidate.provider !== "codex" ||
767
+ event.candidate.hostId !== event.previous.hostId ||
768
+ event.previous.endpointGeneration !==
769
+ adapter.identity.endpointGeneration ||
770
+ event.attestation.tier !== "incompatible") {
771
+ return;
772
+ }
773
+ this.enqueueCallback({
774
+ type: "endpoint_refresh",
775
+ adapter,
776
+ value: event.outcome === "compatible"
777
+ ? {
778
+ ...event,
779
+ previous: { ...event.previous },
780
+ current: { ...event.current },
781
+ routes: event.routes.map((route) => ({ ...route })),
782
+ }
783
+ : {
784
+ ...event,
785
+ previous: { ...event.previous },
786
+ candidate: { ...event.candidate },
787
+ },
788
+ });
789
+ },
790
790
  };
791
791
  }
792
792
  enqueueCallback(event) {
793
793
  if (!this.acceptingCallbacks)
794
794
  return false;
795
+ if (event.type === "endpoint_refresh") {
796
+ return this.enqueueEndpointRefreshCallback(event);
797
+ }
795
798
  if (event.type === "delivery") {
796
799
  const duplicate = this.callbackQueue.findIndex((candidate) => candidate.type === "delivery" &&
797
800
  candidate.value.messageId === event.value.messageId &&
@@ -834,7 +837,7 @@ export class GatewayService {
834
837
  // Delivery write/approval observations are authoritative evidence for
835
838
  // no-replay and deadline arbitration. Never evict them. A stale route
836
839
  // observation is safe to replace because discovery re-observes it.
837
- const replaceable = this.callbackQueue.findIndex((candidate) => candidate.type === "route");
840
+ const replaceable = this.callbackQueue.findIndex((candidate) => candidate.type === "route" || candidate.type === "protocol_notice");
838
841
  if (replaceable < 0) {
839
842
  this.dashboardHealthy = false;
840
843
  return false;
@@ -842,29 +845,49 @@ export class GatewayService {
842
845
  this.callbackQueue.splice(replaceable, 1);
843
846
  }
844
847
  this.callbackQueue.push(event);
845
- if (this.callbackScheduled)
846
- return true;
847
- this.callbackScheduled = true;
848
- setImmediate(() => {
849
- this.callbackScheduled = false;
850
- const worker = this.mutex.run("service", async () => {
851
- await this.drainCallbackQueueLocked();
852
- });
853
- this.callbackWorker = worker;
854
- worker
855
- .catch(() => {
856
- this.dashboardHealthy = false;
857
- })
858
- .finally(() => {
859
- if (this.callbackWorker === worker)
860
- this.callbackWorker = undefined;
861
- if (this.callbackQueue.length > 0 && !this.closing) {
862
- this.enqueueCallbackWorkerOnly();
863
- }
864
- });
865
- });
848
+ this.enqueueCallbackWorkerOnly();
866
849
  return true;
867
850
  }
851
+ enqueueEndpointRefreshCallback(event) {
852
+ if (this.endpointRefreshCallbackPoisoned)
853
+ return false;
854
+ const key = endpointRefreshCallbackKey(event.value);
855
+ if (this.activeEndpointRefreshCallbackKey !== undefined) {
856
+ if (this.activeEndpointRefreshCallbackKey === key) {
857
+ this.endpointRefreshRetryRequested = event;
858
+ return true;
859
+ }
860
+ this.poisonEndpointRefreshCallbacks(event);
861
+ return false;
862
+ }
863
+ if (this.endpointRefreshCallback !== undefined) {
864
+ if (endpointRefreshCallbackKey(this.endpointRefreshCallback.value) === key) {
865
+ return true;
866
+ }
867
+ this.poisonEndpointRefreshCallbacks(event);
868
+ return false;
869
+ }
870
+ this.endpointRefreshCallback = event;
871
+ this.enqueueCallbackWorkerOnly();
872
+ return true;
873
+ }
874
+ poisonEndpointRefreshCallbacks(conflicting) {
875
+ this.endpointRefreshConflictBoundary =
876
+ this.activeEndpointRefreshCallback ??
877
+ this.endpointRefreshCallback ??
878
+ conflicting;
879
+ this.endpointRefreshCallback = undefined;
880
+ this.endpointRefreshRetryRequested = undefined;
881
+ this.endpointRefreshCallbackPoisoned = true;
882
+ this.endpointRefreshConflictPending = true;
883
+ this.dashboardHealthy = false;
884
+ this.enqueueCallbackWorkerOnly();
885
+ }
886
+ hasPendingCallbackWork() {
887
+ return (this.callbackQueue.length > 0 ||
888
+ this.endpointRefreshCallback !== undefined ||
889
+ this.endpointRefreshConflictPending);
890
+ }
868
891
  enqueueCallbackWorkerOnly() {
869
892
  if (this.callbackScheduled)
870
893
  return;
@@ -882,7 +905,7 @@ export class GatewayService {
882
905
  .finally(() => {
883
906
  if (this.callbackWorker === worker)
884
907
  this.callbackWorker = undefined;
885
- if (this.callbackQueue.length > 0 && !this.closing) {
908
+ if (this.hasPendingCallbackWork() && !this.closing) {
886
909
  this.enqueueCallbackWorkerOnly();
887
910
  }
888
911
  });
@@ -895,10 +918,95 @@ export class GatewayService {
895
918
  await this.publish();
896
919
  }
897
920
  this.pruneTransient();
898
- while (this.callbackQueue.length > 0) {
899
- const event = this.callbackQueue.shift();
900
- if (event === undefined)
921
+ while (this.hasPendingCallbackWork()) {
922
+ // Preserve the complete ordinary queue order. In particular, every
923
+ // delivery observation already retained before endpoint replacement is
924
+ // reduced before the lifecycle transition can settle its generation.
925
+ const ordinaryBatch = this.callbackQueue.splice(0, this.callbackQueue.length);
926
+ for (const event of ordinaryBatch) {
927
+ try {
928
+ if (event.type === "delivery") {
929
+ await this.onDelivery(event.source, event.value, event.receivedAt);
930
+ }
931
+ else if (event.type === "route") {
932
+ await this.onRouteState(event.source, event.value);
933
+ }
934
+ else if (event.type === "claude_reply") {
935
+ await this.onClaudeReply(event.value);
936
+ }
937
+ else if (event.type === "claude_message") {
938
+ await this.onClaudeMessage(event.value);
939
+ }
940
+ else if (event.type === "protocol_notice") {
941
+ this.addRuntimeAlert(event.value.code, "warning", {
942
+ provider: event.source.provider,
943
+ host: event.source.hostId,
944
+ });
945
+ await this.changed();
946
+ }
947
+ }
948
+ catch {
949
+ this.dashboardHealthy = false;
950
+ }
951
+ }
952
+ await this.drainPreDeadlineDeliveryCallbacksLocked();
953
+ if (this.endpointRefreshConflictPending) {
954
+ this.endpointRefreshConflictPending = false;
955
+ const boundary = this.endpointRefreshConflictBoundary;
956
+ if (boundary !== undefined) {
957
+ await this.failClosedEndpointRefreshConflict(boundary);
958
+ }
959
+ else {
960
+ this.addRuntimeAlert("CODEX_ENDPOINT_REFRESH_CONFLICT", "error", { provider: "codex" });
961
+ await this.changed();
962
+ }
901
963
  continue;
964
+ }
965
+ const endpointRefresh = this.endpointRefreshCallback;
966
+ if (endpointRefresh === undefined)
967
+ continue;
968
+ const key = endpointRefreshCallbackKey(endpointRefresh.value);
969
+ this.endpointRefreshCallback = undefined;
970
+ this.activeEndpointRefreshCallback = endpointRefresh;
971
+ this.activeEndpointRefreshCallbackKey = key;
972
+ let succeeded = false;
973
+ try {
974
+ await this.onCodexEndpointRefresh(endpointRefresh.adapter, endpointRefresh.value);
975
+ succeeded = true;
976
+ }
977
+ catch {
978
+ this.dashboardHealthy = false;
979
+ }
980
+ finally {
981
+ this.activeEndpointRefreshCallback = undefined;
982
+ if (this.activeEndpointRefreshCallbackKey === key) {
983
+ this.activeEndpointRefreshCallbackKey = undefined;
984
+ }
985
+ }
986
+ const requestedRetry = this.endpointRefreshRetryRequested;
987
+ this.endpointRefreshRetryRequested = undefined;
988
+ if (!succeeded &&
989
+ !this.endpointRefreshCallbackPoisoned &&
990
+ requestedRetry !== undefined) {
991
+ this.endpointRefreshCallback = requestedRetry;
992
+ }
993
+ }
994
+ }
995
+ /**
996
+ * A selector can synchronously admit callbacks while its promise is still
997
+ * running under the service mutex. Drain exactly the ordinary work that was
998
+ * retained before the selector returned before applying a returned endpoint
999
+ * transition. Delivery evidence wins within that fixed snapshot; callbacks
1000
+ * admitted while it is reduced remain queued for the ordinary worker, and
1001
+ * the dedicated endpoint-transition slot is never consumed recursively.
1002
+ */
1003
+ async drainSelectorCallbackSnapshotLocked() {
1004
+ const snapshot = this.callbackQueue.splice(0, this.callbackQueue.length);
1005
+ const ordered = [
1006
+ ...snapshot.filter((event) => event.type === "delivery"),
1007
+ ...snapshot.filter((event) => event.type !== "delivery"),
1008
+ ];
1009
+ for (const event of ordered) {
902
1010
  try {
903
1011
  if (event.type === "delivery") {
904
1012
  await this.onDelivery(event.source, event.value, event.receivedAt);
@@ -912,7 +1020,7 @@ export class GatewayService {
912
1020
  else if (event.type === "claude_message") {
913
1021
  await this.onClaudeMessage(event.value);
914
1022
  }
915
- else {
1023
+ else if (event.type === "protocol_notice") {
916
1024
  this.addRuntimeAlert(event.value.code, "warning", {
917
1025
  provider: event.source.provider,
918
1026
  host: event.source.hostId,
@@ -924,6 +1032,59 @@ export class GatewayService {
924
1032
  this.dashboardHealthy = false;
925
1033
  }
926
1034
  }
1035
+ // Synchronous dispatch may have admitted authoritative delivery evidence,
1036
+ // and rejected native ingress may have started a detached terminal write.
1037
+ // Join both before the endpoint boundary can move to its next generation.
1038
+ await this.drainPreDeadlineDeliveryCallbacksLocked();
1039
+ await this.drainDetachedReceiptWritesLocked();
1040
+ }
1041
+ async failClosedEndpointRefreshConflict(boundary) {
1042
+ const identities = [
1043
+ boundary.value.previous,
1044
+ ...(boundary.value.outcome === "compatible"
1045
+ ? [boundary.value.current]
1046
+ : []),
1047
+ ];
1048
+ for (const identity of identities) {
1049
+ const routes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.binding.hostId === identity.hostId &&
1050
+ route.binding.endpointGeneration === identity.endpointGeneration);
1051
+ const aliases = routes.map((route) => route.alias);
1052
+ const inFlightSettlements = aliases.length === 0
1053
+ ? []
1054
+ : await this.planRoutesInFlightSettlementsLocked(aliases, {
1055
+ unwrittenOutcome: "failed",
1056
+ safeErrorCode: "CODEX_ENDPOINT_REFRESH_CONFLICT",
1057
+ });
1058
+ try {
1059
+ const settlements = await this.store.markConnectorOffline(identity, "CODEX_ENDPOINT_REFRESH_CONFLICT", inFlightSettlements, { preserveQueued: true });
1060
+ for (const settlement of settlements) {
1061
+ await this.applyTerminalSettlementLocked(settlement);
1062
+ }
1063
+ for (const route of routes) {
1064
+ this.routeStates.set(route.alias, "stale");
1065
+ }
1066
+ break;
1067
+ }
1068
+ catch (error) {
1069
+ if (error instanceof BridgeError &&
1070
+ error.code === "CONNECTOR_NOT_FOUND") {
1071
+ continue;
1072
+ }
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ this.addRuntimeAlert("CODEX_ENDPOINT_REFRESH_CONFLICT", "error", { provider: "codex", host: boundary.value.previous.hostId });
1077
+ await this.changed();
1078
+ }
1079
+ async requireEndpointRefreshUnconflicted() {
1080
+ if (!this.endpointRefreshCallbackPoisoned)
1081
+ return;
1082
+ this.endpointRefreshConflictPending = false;
1083
+ const boundary = this.endpointRefreshConflictBoundary;
1084
+ if (boundary !== undefined) {
1085
+ await this.failClosedEndpointRefreshConflict(boundary);
1086
+ }
1087
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "Conflicting Codex endpoint transitions require a clean broker restart.");
927
1088
  }
928
1089
  isTerminalAdapterDelivery(event) {
929
1090
  return (event.state !== "transport_uncertain" &&
@@ -1030,6 +1191,144 @@ export class GatewayService {
1030
1191
  generation: authority.journal.new.generation,
1031
1192
  }));
1032
1193
  }
1194
+ /**
1195
+ * Rebuild process-owned route and listener authority for retained Codex
1196
+ * registrations during staged startup. The provider's selectRoute performs
1197
+ * the exact loaded/list + history-free resume proof; durable state advances
1198
+ * only after the new native advertisement and status are live.
1199
+ */
1200
+ async reactivateRetainedCodexRoutesAfterRestartLocked() {
1201
+ const idleAliases = [];
1202
+ const retained = await this.store.inspectPrivateCodexRoutes();
1203
+ for (const route of retained) {
1204
+ if (!route.enabled ||
1205
+ route.state !== "stale" ||
1206
+ route.compatibility !== "expired" ||
1207
+ this.pendingCodexSuccessionRecovery?.journal.new.alias === route.alias ||
1208
+ this.codexSuccessionPoisoned.has(route.alias)) {
1209
+ continue;
1210
+ }
1211
+ const codexAdapter = this.adapters.find((adapter) => adapter.identity.provider === "codex" &&
1212
+ adapter.identity.hostId === route.binding.hostId);
1213
+ const claudeAdapter = this.adapters.find((adapter) => adapter.identity.provider === "claude" &&
1214
+ adapter.identity.hostId === route.binding.hostId);
1215
+ if (codexAdapter === undefined || claudeAdapter === undefined) {
1216
+ this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1217
+ continue;
1218
+ }
1219
+ let routeSelected = false;
1220
+ let advertiseAttempted = false;
1221
+ let storeReactivated = false;
1222
+ let deferredEndpointRefresh = false;
1223
+ try {
1224
+ const selected = await codexAdapter.selectRoute({
1225
+ alias: route.alias,
1226
+ routeHandle: route.binding.routeHandle,
1227
+ });
1228
+ routeSelected = true;
1229
+ await this.drainSelectorCallbackSnapshotLocked();
1230
+ if (selected.routeHandle !== route.binding.routeHandle) {
1231
+ throw new BridgeError("CODEX_BOOT_REACTIVATION_MISMATCH", "The retained Codex task changed during broker startup reactivation.");
1232
+ }
1233
+ if (selected.endpointRefresh !== undefined) {
1234
+ deferredEndpointRefresh = await this.onCodexEndpointRefresh(codexAdapter, selected.endpointRefresh, {
1235
+ deferActivation: true,
1236
+ deferredRoute: {
1237
+ alias: route.alias,
1238
+ threadId: route.binding.routeHandle,
1239
+ state: selected.state,
1240
+ },
1241
+ });
1242
+ if (!deferredEndpointRefresh) {
1243
+ throw new BridgeError("CODEX_BOOT_REACTIVATION_MISMATCH", "The retained Codex task could not stage a compatible endpoint refresh during startup.");
1244
+ }
1245
+ }
1246
+ const binding = {
1247
+ ...codexAdapter.identity,
1248
+ routeHandle: route.binding.routeHandle,
1249
+ ownerLease: route.binding.ownerLease,
1250
+ };
1251
+ if (deferredEndpointRefresh) {
1252
+ const afterStaging = await this.store.inspectPrivateRoute(route.alias);
1253
+ storeReactivated =
1254
+ afterStaging !== undefined &&
1255
+ afterStaging.enabled &&
1256
+ afterStaging.compatibility === "compatible" &&
1257
+ bindingKey(afterStaging.binding) === bindingKey(binding);
1258
+ }
1259
+ advertiseAttempted = true;
1260
+ const advertise = claudeAdapter.advertiseNativeCodexPeer;
1261
+ if (advertise === undefined) {
1262
+ throw new BridgeError("CODEX_BOOT_REACTIVATION_UNAVAILABLE", "The retained Codex task cannot rebuild its native advertisement.");
1263
+ }
1264
+ await advertise.call(claudeAdapter, {
1265
+ alias: route.alias,
1266
+ cwd: this.nativePeerCwd,
1267
+ });
1268
+ await claudeAdapter.updateNativeCodexPeerStatus?.(route.alias, selected.state === "idle"
1269
+ ? "idle"
1270
+ : selected.state === "awaiting_approval"
1271
+ ? "waiting"
1272
+ : "busy");
1273
+ const listenerGeneration = this.requireCurrentNativeCodexGeneration(claudeAdapter, route.alias);
1274
+ if (!storeReactivated) {
1275
+ await this.store.rebindStaleRoute({
1276
+ alias: route.alias,
1277
+ currentOwnerLease: route.binding.ownerLease,
1278
+ newBinding: binding,
1279
+ reason: "endpoint_reobserved",
1280
+ journalReason: "boot_reactivation",
1281
+ state: selected.state,
1282
+ });
1283
+ storeReactivated = true;
1284
+ }
1285
+ this.rememberBinding(route.alias, binding, selected.state);
1286
+ this.lockCodexRegistration({
1287
+ alias: route.alias,
1288
+ threadId: route.binding.routeHandle,
1289
+ hostId: route.binding.hostId,
1290
+ busyPolicy: "queue",
1291
+ }, listenerGeneration);
1292
+ if (deferredEndpointRefresh &&
1293
+ selected.endpointRefresh !== undefined) {
1294
+ await this.onCodexEndpointRefresh(codexAdapter, selected.endpointRefresh, {
1295
+ requiredRoute: {
1296
+ alias: route.alias,
1297
+ threadId: route.binding.routeHandle,
1298
+ state: selected.state,
1299
+ },
1300
+ });
1301
+ }
1302
+ if (this.requireCurrentNativeCodexGeneration(claudeAdapter, route.alias) !== listenerGeneration) {
1303
+ throw new BridgeError("CODEX_BOOT_REACTIVATION_NATIVE_LISTENER_MISSING", "The retained Codex task's native listener changed during boot reactivation.");
1304
+ }
1305
+ if (selected.state === "idle")
1306
+ idleAliases.push(route.alias);
1307
+ }
1308
+ catch {
1309
+ let cleanupFailed = false;
1310
+ if (advertiseAttempted) {
1311
+ await claudeAdapter
1312
+ .unadvertiseNativeCodexPeer?.(route.alias)
1313
+ .catch(() => {
1314
+ cleanupFailed = true;
1315
+ });
1316
+ }
1317
+ if (routeSelected) {
1318
+ await codexAdapter
1319
+ .releaseRoute?.(route.binding.routeHandle)
1320
+ .catch(() => {
1321
+ cleanupFailed = true;
1322
+ });
1323
+ }
1324
+ if (cleanupFailed || storeReactivated || deferredEndpointRefresh) {
1325
+ throw new BridgeError("CODEX_BOOT_REACTIVATION_CLEANUP_FAILED", "A Codex boot reactivation crossed its durable boundary without exact live authority.");
1326
+ }
1327
+ this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1328
+ }
1329
+ }
1330
+ return idleAliases;
1331
+ }
1033
1332
  async completePendingCodexSuccessionRecoveryLocked(params) {
1034
1333
  const recovery = this.pendingCodexSuccessionRecovery;
1035
1334
  if (recovery === undefined)
@@ -1744,7 +2043,7 @@ export class GatewayService {
1744
2043
  claude.rejectedInboundSettlements === 0 &&
1745
2044
  codexQuiet &&
1746
2045
  this.successionServiceStateClean(aliases) &&
1747
- this.callbackQueue.length === 0 &&
2046
+ this.endpointCallbacksQuiet() &&
1748
2047
  this.detachedReceiptWrites.size === 0);
1749
2048
  }
1750
2049
  async codexSuccessionBarrierCleanLocked(oldRegistration) {
@@ -1771,7 +2070,7 @@ export class GatewayService {
1771
2070
  claude.monitorFrozen &&
1772
2071
  codex.clean &&
1773
2072
  this.successionServiceStateClean(aliases) &&
1774
- this.callbackQueue.length === 0 &&
2073
+ this.endpointCallbacksQuiet() &&
1775
2074
  this.detachedReceiptWrites.size === 0);
1776
2075
  }
1777
2076
  rescheduleIdleRoutesLocked() {
@@ -1780,13 +2079,48 @@ export class GatewayService {
1780
2079
  this.scheduleDispatch(alias);
1781
2080
  }
1782
2081
  }
2082
+ codexSuccessionIsStable() {
2083
+ const state = this.codexSuccessionState;
2084
+ return (state === undefined ||
2085
+ state.phase === "active_old" ||
2086
+ (state.phase === "active_new" && state.retired === null));
2087
+ }
2088
+ async requireUnchangedStaleCodexOrphan(alias, expected) {
2089
+ let current;
2090
+ try {
2091
+ current = await this.store.inspectStaleCodexOrphan(alias);
2092
+ }
2093
+ catch {
2094
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed during recovery.");
2095
+ }
2096
+ if (bindingKey(current) !== bindingKey(expected)) {
2097
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed during recovery.");
2098
+ }
2099
+ }
2100
+ nativeCodexOrphanBarrierClean(observation, generation) {
2101
+ return (observation.clean &&
2102
+ observation.generation === generation &&
2103
+ observation.activeGenerationMatched &&
2104
+ observation.ingressQuiesced &&
2105
+ observation.monitorFrozen &&
2106
+ !observation.discoveryInFlight &&
2107
+ observation.pendingOutboundReceipts === 0 &&
2108
+ observation.pendingInboundReceipts === 0 &&
2109
+ observation.rejectedInboundSettlements === 0);
2110
+ }
2111
+ endpointCallbacksQuiet() {
2112
+ return (!this.hasPendingCallbackWork() &&
2113
+ this.activeEndpointRefreshCallbackKey === undefined);
2114
+ }
1783
2115
  async registerCodex(params) {
1784
2116
  if (!params.alias.startsWith("codex-")) {
1785
2117
  throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
1786
2118
  }
1787
2119
  if (this.codexSuccessionPoisoned.has(params.alias) ||
2120
+ this.codexOrphanCleanupPending.has(params.alias) ||
1788
2121
  (params.succeedsAlias !== undefined &&
1789
- this.codexSuccessionPoisoned.has(params.succeedsAlias))) {
2122
+ (this.codexSuccessionPoisoned.has(params.succeedsAlias) ||
2123
+ this.codexOrphanCleanupPending.has(params.succeedsAlias)))) {
1790
2124
  throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "Codex registration is offline until the incomplete succession is manually recovered.");
1791
2125
  }
1792
2126
  if (params.succeedsAlias !== undefined) {
@@ -1822,16 +2156,41 @@ export class GatewayService {
1822
2156
  alias: params.alias,
1823
2157
  routeHandle: params.threadId,
1824
2158
  });
2159
+ await this.drainSelectorCallbackSnapshotLocked();
1825
2160
  if (registered.routeHandle !== params.threadId) {
1826
2161
  throw new BridgeError("ROUTE_MISMATCH", "The connector registered a different task.");
1827
2162
  }
2163
+ let deferredEndpointRefresh = false;
2164
+ if (registered.endpointRefresh !== undefined) {
2165
+ try {
2166
+ deferredEndpointRefresh = await this.onCodexEndpointRefresh(adapter, registered.endpointRefresh, {
2167
+ deferActivation: true,
2168
+ deferredRoute: {
2169
+ alias: params.alias,
2170
+ threadId: params.threadId,
2171
+ state: registered.state,
2172
+ },
2173
+ });
2174
+ }
2175
+ catch (error) {
2176
+ await adapter.releaseRoute?.(params.threadId).catch(() => undefined);
2177
+ throw error;
2178
+ }
2179
+ }
1828
2180
  const binding = {
1829
2181
  ...adapter.identity,
1830
2182
  routeHandle: params.threadId,
1831
2183
  ownerLease: stableLease("codex", `${params.hostId}\0${params.threadId}`),
1832
2184
  };
1833
2185
  try {
1834
- await this.registerOrRebind(params.alias, binding, "endpoint_reobserved", registered.state);
2186
+ const afterRefresh = await this.store.inspectPrivateRoute(params.alias);
2187
+ const alreadyInstalledExactly = afterRefresh !== undefined &&
2188
+ afterRefresh.enabled &&
2189
+ afterRefresh.compatibility === "compatible" &&
2190
+ bindingKey(afterRefresh.binding) === bindingKey(binding);
2191
+ if (!alreadyInstalledExactly || !deferredEndpointRefresh) {
2192
+ await this.registerOrRebind(params.alias, binding, "endpoint_reobserved", registered.state);
2193
+ }
1835
2194
  }
1836
2195
  catch (error) {
1837
2196
  await adapter.releaseRoute?.(params.threadId).catch(() => undefined);
@@ -1857,10 +2216,32 @@ export class GatewayService {
1857
2216
  : "busy");
1858
2217
  listenerGeneration = this.requireCurrentNativeCodexGeneration(claudeAdapter, params.alias);
1859
2218
  await this.completePendingCodexSuccessionRecoveryLocked(params);
2219
+ this.lockCodexRegistration(params, listenerGeneration);
2220
+ if (deferredEndpointRefresh &&
2221
+ registered.endpointRefresh !== undefined) {
2222
+ await this.onCodexEndpointRefresh(adapter, registered.endpointRefresh, {
2223
+ requiredRoute: {
2224
+ alias: params.alias,
2225
+ threadId: params.threadId,
2226
+ state: registered.state,
2227
+ },
2228
+ });
2229
+ }
1860
2230
  await this.changed();
1861
2231
  }
1862
2232
  catch (error) {
1863
2233
  if (routeBeforeRegistration !== undefined) {
2234
+ if (deferredEndpointRefresh &&
2235
+ registered.endpointRefresh?.outcome === "compatible") {
2236
+ try {
2237
+ await this.store.markConnectorOffline(registered.endpointRefresh.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
2238
+ this.routeStates.set(params.alias, "stale");
2239
+ await this.changed();
2240
+ }
2241
+ catch {
2242
+ this.dashboardHealthy = false;
2243
+ }
2244
+ }
1864
2245
  // A persisted or live exact route, its queue, and any advertisement
1865
2246
  // predate this invocation, so a later status/dashboard failure cannot
1866
2247
  // safely delete them as "rollback". A post-restart reactivation also
@@ -1971,6 +2352,220 @@ export class GatewayService {
1971
2352
  });
1972
2353
  await this.changed();
1973
2354
  }
2355
+ finalizeStaleCodexOrphanRemovalLocked(alias) {
2356
+ const aliases = new Set([alias]);
2357
+ this.forgetBinding(alias);
2358
+ this.purgeSuccessionTransientState(aliases);
2359
+ const lock = this.codexRegistrationLocks.get(alias);
2360
+ if (lock !== undefined) {
2361
+ this.codexRegistrationLocks.delete(alias);
2362
+ const state = this.codexSuccessionState;
2363
+ if ((state?.phase === "active_old" && state.active.alias === alias) ||
2364
+ (state?.phase === "active_new" && state.active.alias === alias)) {
2365
+ this.codexSuccessionState = undefined;
2366
+ }
2367
+ }
2368
+ this.codexOrphanCleanupPending.delete(alias);
2369
+ this.codexSuccessionDispatchFrozen.delete(alias);
2370
+ }
2371
+ async removeStaleCodexRegistration(alias) {
2372
+ const pendingCleanup = this.codexOrphanCleanupPending.get(alias);
2373
+ if (pendingCleanup !== undefined &&
2374
+ (await this.store.wasStaleCodexOrphanRemovalCommitted({
2375
+ alias,
2376
+ binding: pendingCleanup.binding,
2377
+ previousSequence: pendingCleanup.previousSequence,
2378
+ }))) {
2379
+ this.finalizeStaleCodexOrphanRemovalLocked(alias);
2380
+ await this.changed();
2381
+ return;
2382
+ }
2383
+ const authority = await this.store.inspectStaleCodexOrphanRemovalAuthority(alias);
2384
+ const binding = authority.binding;
2385
+ const aliases = new Set([alias]);
2386
+ const resumesExactCleanup = pendingCleanup !== undefined &&
2387
+ bindingKey(pendingCleanup.binding) === bindingKey(binding) &&
2388
+ pendingCleanup.previousSequence === authority.previousSequence;
2389
+ if (pendingCleanup !== undefined && !resumesExactCleanup) {
2390
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed after native cleanup.");
2391
+ }
2392
+ if (!this.codexSuccessionIsStable() ||
2393
+ this.pendingCodexSuccessionRecovery !== undefined ||
2394
+ this.codexSuccessionPoisoned.has(alias) ||
2395
+ (this.codexSuccessionDispatchFrozen.has(alias) && !resumesExactCleanup)) {
2396
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BLOCKED", "Codex orphan recovery cannot run during registration succession.");
2397
+ }
2398
+ const lock = this.codexRegistrationLocks.get(alias);
2399
+ if (lock !== undefined &&
2400
+ (lock.threadId !== binding.routeHandle ||
2401
+ lock.hostId !== binding.hostId ||
2402
+ lock.generation === "unconfirmed" ||
2403
+ !isCodexRegistrationGeneration(lock.generation))) {
2404
+ throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener identity cannot be proven for orphan recovery.");
2405
+ }
2406
+ const claudeAdapter = this.adapter("claude", binding.hostId);
2407
+ const codexAdapter = this.adapter("codex", binding.hostId);
2408
+ const currentNative = claudeAdapter.currentNativeCodexPeerGeneration;
2409
+ if (currentNative === undefined) {
2410
+ throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The Claude provider cannot prove whether the native Codex listener exists.");
2411
+ }
2412
+ this.codexSuccessionDispatchFrozen.add(alias);
2413
+ this.scheduledDispatchTargets.delete(alias);
2414
+ let nativeGeneration;
2415
+ let nativeQuiesceAttempted = false;
2416
+ let irreversibleCleanupStarted = resumesExactCleanup;
2417
+ try {
2418
+ try {
2419
+ const observed = currentNative.call(claudeAdapter, alias);
2420
+ if (!isCodexRegistrationGeneration(observed)) {
2421
+ throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener generation is malformed.");
2422
+ }
2423
+ if (lock !== undefined && lock.generation !== observed) {
2424
+ throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener generation changed before orphan recovery.");
2425
+ }
2426
+ nativeGeneration = observed;
2427
+ }
2428
+ catch (error) {
2429
+ const exactAbsence = error instanceof BridgeError &&
2430
+ error.code === "CODEX_PEER_GENERATION_MISMATCH";
2431
+ if (!exactAbsence || (lock !== undefined && !resumesExactCleanup)) {
2432
+ if (error instanceof BridgeError &&
2433
+ error.code === "CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN") {
2434
+ throw error;
2435
+ }
2436
+ throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener identity cannot be proven for orphan recovery.");
2437
+ }
2438
+ }
2439
+ let observeNative;
2440
+ if (nativeGeneration !== undefined) {
2441
+ const quiesce = this.requireSuccessionMethod(claudeAdapter, "quiesceNativeCodexPeerGeneration");
2442
+ // Require the inverse operation before acquiring the quiescence fence
2443
+ // so every pre-unadvertise failure has one exact cleanup path.
2444
+ this.requireSuccessionMethod(claudeAdapter, "resumeNativeCodexPeerGeneration");
2445
+ observeNative = this.requireSuccessionMethod(claudeAdapter, "observeNativeCodexSuccessionBarrier");
2446
+ nativeQuiesceAttempted = true;
2447
+ await quiesce.call(claudeAdapter, nativeGeneration);
2448
+ }
2449
+ // Join every native frame admitted before the exact generation fence,
2450
+ // and every detached terminal receipt write those frames create.
2451
+ await this.drainPreDeadlineDeliveryCallbacksLocked();
2452
+ await this.drainCallbackQueueLocked();
2453
+ await this.drainDetachedReceiptWritesLocked();
2454
+ await this.requireUnchangedStaleCodexOrphan(alias, binding);
2455
+ const observeCodex = codexAdapter.observeRouteSuccessionBarrier;
2456
+ if (observeCodex === undefined) {
2457
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The Codex provider cannot prove an empty orphan route boundary.");
2458
+ }
2459
+ const beforeRelease = observeCodex.call(codexAdapter, binding.routeHandle);
2460
+ if (beforeRelease.routePresent ||
2461
+ !this.codexRouteSuccessionQuiet(beforeRelease) ||
2462
+ beforeRelease.routeCreationInFlight ||
2463
+ beforeRelease.routeReleaseInFlight) {
2464
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration still has a live provider or endpoint-refresh claim.");
2465
+ }
2466
+ if (nativeGeneration !== undefined) {
2467
+ const nativeBarrier = await observeNative.call(claudeAdapter, nativeGeneration);
2468
+ if (!this.nativeCodexOrphanBarrierClean(nativeBarrier, nativeGeneration)) {
2469
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The native Codex listener still owns ingress or receipt work.");
2470
+ }
2471
+ }
2472
+ if (!this.successionServiceStateClean(aliases) ||
2473
+ !this.endpointCallbacksQuiet() ||
2474
+ this.detachedReceiptWrites.size !== 0) {
2475
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration still has a live service claim.");
2476
+ }
2477
+ // The awaited native barrier can admit a provider-side route recovery
2478
+ // claim. Re-sample the Codex boundary after that last await and directly
2479
+ // before the generation-unqualified release call.
2480
+ const releaseBoundary = observeCodex.call(codexAdapter, binding.routeHandle);
2481
+ if (releaseBoundary.routePresent ||
2482
+ !this.codexRouteSuccessionQuiet(releaseBoundary) ||
2483
+ releaseBoundary.routeCreationInFlight ||
2484
+ releaseBoundary.routeReleaseInFlight) {
2485
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration acquired a provider recovery claim before release.");
2486
+ }
2487
+ // The pre-barrier proves this release targets an absent stale handle and
2488
+ // cannot join an already-observed creation. Revalidate immediately after
2489
+ // the provider join before touching the native advertisement.
2490
+ await codexAdapter.releaseRoute?.(binding.routeHandle);
2491
+ await this.drainCallbackQueueLocked();
2492
+ await this.drainDetachedReceiptWritesLocked();
2493
+ await this.requireUnchangedStaleCodexOrphan(alias, binding);
2494
+ const afterRelease = observeCodex.call(codexAdapter, binding.routeHandle);
2495
+ if (afterRelease.routePresent ||
2496
+ !this.codexRouteSuccessionQuiet(afterRelease) ||
2497
+ afterRelease.routeCreationInFlight ||
2498
+ afterRelease.routeReleaseInFlight ||
2499
+ !this.successionServiceStateClean(aliases) ||
2500
+ !this.endpointCallbacksQuiet() ||
2501
+ this.detachedReceiptWrites.size !== 0) {
2502
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration changed at the provider cleanup boundary.");
2503
+ }
2504
+ if (nativeGeneration !== undefined) {
2505
+ const nativeBarrier = await observeNative.call(claudeAdapter, nativeGeneration);
2506
+ if (!this.nativeCodexOrphanBarrierClean(nativeBarrier, nativeGeneration)) {
2507
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The native Codex listener changed during provider cleanup.");
2508
+ }
2509
+ if (currentNative.call(claudeAdapter, alias) !== nativeGeneration) {
2510
+ throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener generation changed before removal.");
2511
+ }
2512
+ }
2513
+ await this.requireUnchangedStaleCodexOrphan(alias, binding);
2514
+ if (nativeGeneration !== undefined) {
2515
+ const unadvertise = claudeAdapter.unadvertiseNativeCodexPeer;
2516
+ if (unadvertise === undefined) {
2517
+ throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener cannot be removed exactly.");
2518
+ }
2519
+ irreversibleCleanupStarted = true;
2520
+ this.codexOrphanCleanupPending.set(alias, {
2521
+ binding: { ...binding },
2522
+ previousSequence: authority.previousSequence,
2523
+ });
2524
+ // The supervisor may remove the exact helper and then report an
2525
+ // ambiguous close failure. Retain retry authority before invoking the
2526
+ // irreversible operation; a retry proves whether the listener remains.
2527
+ await unadvertise.call(claudeAdapter, alias);
2528
+ }
2529
+ // Exact native absence is also an irreversible cleanup boundary: if the
2530
+ // durable mutation fails, only this same binding may resume removal.
2531
+ this.codexOrphanCleanupPending.set(alias, {
2532
+ binding: { ...binding },
2533
+ previousSequence: authority.previousSequence,
2534
+ });
2535
+ irreversibleCleanupStarted = true;
2536
+ const removed = await this.store.removeStaleCodexOrphan({ alias });
2537
+ if (bindingKey(removed.binding) !== bindingKey(binding)) {
2538
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed during recovery.");
2539
+ }
2540
+ this.finalizeStaleCodexOrphanRemovalLocked(alias);
2541
+ await this.changed();
2542
+ }
2543
+ catch (error) {
2544
+ if (!irreversibleCleanupStarted) {
2545
+ let resumeFailed = false;
2546
+ if (nativeGeneration !== undefined && nativeQuiesceAttempted) {
2547
+ try {
2548
+ const resume = this.requireSuccessionMethod(claudeAdapter, "resumeNativeCodexPeerGeneration");
2549
+ await resume.call(claudeAdapter, nativeGeneration);
2550
+ }
2551
+ catch {
2552
+ resumeFailed = true;
2553
+ this.dashboardHealthy = false;
2554
+ }
2555
+ }
2556
+ if (!resumeFailed) {
2557
+ this.codexSuccessionDispatchFrozen.delete(alias);
2558
+ if (this.routeStates.get(alias) === "idle") {
2559
+ this.scheduleDispatch(alias);
2560
+ }
2561
+ }
2562
+ else {
2563
+ throw new BridgeError("CODEX_ORPHAN_RECOVERY_CLEANUP_FAILED", "The native Codex listener could not be resumed after failed orphan recovery.");
2564
+ }
2565
+ }
2566
+ throw error;
2567
+ }
2568
+ }
1974
2569
  async discoveryPublicFingerprint() {
1975
2570
  const base = await this.store.publicSnapshot();
1976
2571
  const snapshot = projectGatewayPublicSnapshot({
@@ -1993,13 +2588,22 @@ export class GatewayService {
1993
2588
  const publicBefore = await this.discoveryPublicFingerprint();
1994
2589
  this.candidates.clear();
1995
2590
  const rowsByAlias = new Map();
2591
+ const collisionByRouteHandle = new Map();
1996
2592
  const discoveredCandidates = [];
1997
2593
  const observedSelected = new Set();
1998
2594
  for (const adapter of this.adapters.filter((item) => item.identity.provider === "claude")) {
1999
- const discovered = (await adapter.discoverClaudePeers?.()) ?? {
2595
+ let discovered = {
2000
2596
  peers: [],
2001
2597
  complete: false,
2002
2598
  };
2599
+ try {
2600
+ discovered = (await adapter.discoverClaudePeers?.()) ?? discovered;
2601
+ }
2602
+ catch {
2603
+ // Registry observation is dashboard evidence, not Claude mailbox
2604
+ // authority. The provider still revalidates the exact target before
2605
+ // any write.
2606
+ }
2003
2607
  const grouped = new Map();
2004
2608
  const byHandle = new Map();
2005
2609
  for (const peer of discovered.peers) {
@@ -2019,6 +2623,8 @@ export class GatewayService {
2019
2623
  if (peer === undefined)
2020
2624
  continue;
2021
2625
  if (matches.length !== 1) {
2626
+ for (const match of matches)
2627
+ collisionByRouteHandle.set(match.routeHandle, "PEER_ALIAS_COLLISION");
2022
2628
  rowsByAlias.set(alias, {
2023
2629
  peer,
2024
2630
  adapter,
@@ -2035,6 +2641,7 @@ export class GatewayService {
2035
2641
  continue;
2036
2642
  }
2037
2643
  if ((byHandle.get(peer.routeHandle)?.length ?? 0) !== 1) {
2644
+ collisionByRouteHandle.set(peer.routeHandle, "PEER_SESSION_COLLISION");
2038
2645
  rowsByAlias.set(alias, {
2039
2646
  peer,
2040
2647
  adapter,
@@ -2060,6 +2667,8 @@ export class GatewayService {
2060
2667
  const collision = this.routeBindings.get(alias);
2061
2668
  if (collision !== undefined &&
2062
2669
  bindingKey(collision) !== bindingKey(existing[1])) {
2670
+ collisionByRouteHandle.set(routeHandle, "PEER_ALIAS_COLLISION");
2671
+ collisionByRouteHandle.set(collision.routeHandle, "PEER_ALIAS_COLLISION");
2063
2672
  rowsByAlias.set(alias, {
2064
2673
  peer: candidate,
2065
2674
  adapter,
@@ -2074,10 +2683,16 @@ export class GatewayService {
2074
2683
  catch (error) {
2075
2684
  if (!(error instanceof BridgeError))
2076
2685
  throw error;
2686
+ // Only a real alias collision may wear that receipt; every other
2687
+ // rename failure is an unobserved peer, not a name conflict.
2688
+ const safeErrorCode = error.code === "ROUTE_ALIAS_COLLISION"
2689
+ ? "PEER_ALIAS_COLLISION"
2690
+ : "PEER_NOT_OBSERVED";
2691
+ collisionByRouteHandle.set(routeHandle, safeErrorCode);
2077
2692
  rowsByAlias.set(alias, {
2078
2693
  peer: candidate,
2079
2694
  adapter,
2080
- safeErrorCode: "PEER_ALIAS_COLLISION",
2695
+ safeErrorCode,
2081
2696
  });
2082
2697
  }
2083
2698
  }
@@ -2146,6 +2761,9 @@ export class GatewayService {
2146
2761
  observedSelected.has(bindingKey(binding))) {
2147
2762
  continue;
2148
2763
  }
2764
+ const discoveryError = collisionByRouteHandle.get(binding.routeHandle);
2765
+ if (discoveryError === undefined)
2766
+ continue;
2149
2767
  const inspection = await this.store.inspectPrivateRoute(alias);
2150
2768
  if (inspection !== undefined &&
2151
2769
  inspection.enabled &&
@@ -2153,9 +2771,9 @@ export class GatewayService {
2153
2771
  await this.drainPreDeadlineDeliveryCallbacksLocked();
2154
2772
  const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(alias, {
2155
2773
  unwrittenOutcome: "failed",
2156
- safeErrorCode: "PEER_NOT_OBSERVED",
2774
+ safeErrorCode: discoveryError,
2157
2775
  });
2158
- const settlements = await this.store.invalidateRoute(binding, "PEER_NOT_OBSERVED", inFlightSettlements);
2776
+ const settlements = await this.store.invalidateRoute(binding, discoveryError, inFlightSettlements);
2159
2777
  for (const settlement of settlements) {
2160
2778
  await this.applyTerminalSettlementLocked(settlement);
2161
2779
  }
@@ -2382,6 +3000,11 @@ export class GatewayService {
2382
3000
  async resolveSelectedClaudeDestination(selector) {
2383
3001
  const discoveryChanged = await this.refreshClaudeDiscovery();
2384
3002
  try {
3003
+ const selected = [...this.routeBindings.entries()].find(([alias, binding]) => binding.provider === "claude" &&
3004
+ (alias === selector ||
3005
+ binding.routeHandle.toLowerCase() === selector.toLowerCase()));
3006
+ if (selected !== undefined)
3007
+ return { alias: selected[0], discoveryChanged };
2385
3008
  const candidate = this.claudeCandidate(selector);
2386
3009
  if (candidate === undefined) {
2387
3010
  throw new BridgeError("PEER_NOT_FOUND", "No unique compatible interactive peer matches that current name or session UUID.");
@@ -2568,6 +3191,7 @@ export class GatewayService {
2568
3191
  const previous = this.routeBindings.get(alias);
2569
3192
  if (previous !== undefined &&
2570
3193
  bindingKey(previous) !== bindingKey(binding)) {
3194
+ this.bindingAliases.delete(bindingKey(previous));
2571
3195
  this.pendingClaudeReplies.delete(bindingKey(previous));
2572
3196
  }
2573
3197
  this.routeBindings.set(alias, binding);
@@ -2695,8 +3319,10 @@ export class GatewayService {
2695
3319
  enqueuedAt: input.enqueuedAt,
2696
3320
  stallAt,
2697
3321
  deadlineAt,
2698
- // Deferred means the provider proved no write. Permit one bounded
2699
- // attempt per 500 ms slice until this message's immutable deadline.
3322
+ // Deferred means the provider proved no write. This budgets one
3323
+ // attempt per 500 ms slice of this message's immutable deadline; a
3324
+ // Codex-bound retry spends one slice each, while a Claude-bound retry
3325
+ // backs off exponentially and spends far fewer of them.
2700
3326
  maxCleanPrewriteRetries: Math.max(0, Math.ceil((deadlineAt - input.enqueuedAt) / 500)),
2701
3327
  nativeReceipt: input.nativeReceipt !== undefined,
2702
3328
  maxNativeReceiptCleanPrewriteRetries: DELIVERY_ACK_RETRY_DELAYS_MS.length,
@@ -3313,12 +3939,10 @@ export class GatewayService {
3313
3939
  `Embassy 自动活跃检查 — ${action.conversationId}]\n` +
3314
3940
  "Reply with the result or a status. / 请回复结果或状态。";
3315
3941
  try {
3316
- await this.enqueue(action.ownerAlias, action.workerAlias, text, false, false, {
3942
+ await this.enqueue(action.ownerAlias, action.workerAlias, text, false, {
3317
3943
  existingConversationId: action.conversationId,
3318
- requestedHopCount: 0,
3319
3944
  exposeDeliveryToken: false,
3320
3945
  skipProgressWatchActivity: true,
3321
- preserveConversationHop: true,
3322
3946
  progressWatchNudge: {
3323
3947
  conversationId: action.conversationId,
3324
3948
  nudgeNumber: action.nudgeNumber,
@@ -3551,7 +4175,7 @@ export class GatewayService {
3551
4175
  await this.verifyCodex(params.fromAlias, params.threadId);
3552
4176
  const resolved = await this.resolveSelectedClaudeDestination(params.toAlias);
3553
4177
  discoveryChanged = resolved.discoveryChanged;
3554
- return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, false, params.trackIdleMinutes === undefined
4178
+ return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, params.trackIdleMinutes === undefined
3555
4179
  ? {}
3556
4180
  : { trackIdleMinutes: params.trackIdleMinutes }));
3557
4181
  }
@@ -3570,7 +4194,7 @@ export class GatewayService {
3570
4194
  try {
3571
4195
  await this.verifyClaude(params.fromAlias, params.replyAddress);
3572
4196
  const steer = this.config.steeringEnabled && params.text.startsWith("STEER:");
3573
- return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, false, {
4197
+ return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, {
3574
4198
  ...(steer ? { steer: true } : {}),
3575
4199
  ...(params.trackIdleMinutes === undefined
3576
4200
  ? {}
@@ -3599,10 +4223,9 @@ export class GatewayService {
3599
4223
  throw new BridgeError("ROUTE_MISMATCH", "The caller is not a conversation participant.");
3600
4224
  const result = caller.kind === "codex" &&
3601
4225
  this.nativeIngressByConversation.has(conversation.id)
3602
- ? await this.enqueueNativeReply(conversation, from, params.text, conversation.lastHopCount + 1, true, params.trackIdleMinutes)
3603
- : await this.enqueue(from, to, params.text, false, true, {
4226
+ ? await this.enqueueNativeReply(conversation, from, params.text, true, params.trackIdleMinutes)
4227
+ : await this.enqueue(from, to, params.text, false, {
3604
4228
  existingConversationId: conversation.id,
3605
- requestedHopCount: conversation.lastHopCount + 1,
3606
4229
  ...(params.trackIdleMinutes === undefined
3607
4230
  ? {}
3608
4231
  : { trackIdleMinutes: params.trackIdleMinutes }),
@@ -3614,7 +4237,7 @@ export class GatewayService {
3614
4237
  }
3615
4238
  });
3616
4239
  }
3617
- async enqueue(sourceAlias, targetAlias, text, expectsReply, isReply, options = {}) {
4240
+ async enqueue(sourceAlias, targetAlias, text, expectsReply, options = {}) {
3618
4241
  if (this.codexSuccessionPoisoned.has(sourceAlias) ||
3619
4242
  this.codexSuccessionPoisoned.has(targetAlias)) {
3620
4243
  throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "No message can be accepted while Codex succession requires manual recovery.");
@@ -3637,11 +4260,8 @@ export class GatewayService {
3637
4260
  targetAlias,
3638
4261
  expectsReply,
3639
4262
  nextSequence: 0,
3640
- lastHopCount: 0,
3641
4263
  lastActivityAt: this.now().toISOString(),
3642
4264
  };
3643
- const hopCount = options.requestedHopCount ??
3644
- (isReply ? conversation.lastHopCount + 1 : 0);
3645
4265
  const sequence = conversation.nextSequence;
3646
4266
  const enqueuedAt = this.now().getTime();
3647
4267
  const deliveryToken = (options.exposeDeliveryToken ?? true)
@@ -3654,7 +4274,6 @@ export class GatewayService {
3654
4274
  body: text,
3655
4275
  dedupeKey: `${conversationId}:${sequence}`,
3656
4276
  conversationIdSuffix: conversationId.slice(-8),
3657
- hopCount,
3658
4277
  deadlineAt,
3659
4278
  ...(options.steer === true ? { steer: true } : {}),
3660
4279
  ...(options.skipProgressWatchActivity === true
@@ -3675,15 +4294,10 @@ export class GatewayService {
3675
4294
  if (queued.pair === true)
3676
4295
  conversation.pair = true;
3677
4296
  conversation.nextSequence += 1;
3678
- if (options.preserveConversationHop !== true) {
3679
- conversation.lastHopCount = hopCount;
3680
- }
3681
4297
  conversation.lastActivityAt = this.now().toISOString();
3682
4298
  this.messageContexts.set(queued.messageId, {
3683
4299
  conversationId,
3684
- isReply,
3685
4300
  expectsReply,
3686
- hopCount,
3687
4301
  sequence,
3688
4302
  targetBindingKey: bindingKey(target),
3689
4303
  authorization: "selected_route",
@@ -3702,7 +4316,6 @@ export class GatewayService {
3702
4316
  this.pendingClaudeReplies.set(bindingKey(target), {
3703
4317
  conversationId,
3704
4318
  bindingKey: bindingKey(target),
3705
- hopCount,
3706
4319
  deadlineAt,
3707
4320
  tainted: false,
3708
4321
  });
@@ -3716,7 +4329,7 @@ export class GatewayService {
3716
4329
  ...(deliveryToken === undefined ? {} : { deliveryToken }),
3717
4330
  };
3718
4331
  }
3719
- async enqueueNativeReply(conversation, sourceAlias, text, requestedHopCount, exposeDeliveryToken = true, trackIdleMinutes) {
4332
+ async enqueueNativeReply(conversation, sourceAlias, text, exposeDeliveryToken = true, trackIdleMinutes) {
3720
4333
  this.pruneTransient();
3721
4334
  const capability = this.nativeIngressByConversation.get(conversation.id);
3722
4335
  if (capability === undefined ||
@@ -3740,7 +4353,6 @@ export class GatewayService {
3740
4353
  body: text,
3741
4354
  dedupeKey: `${conversation.id}:${sequence}`,
3742
4355
  conversationIdSuffix: conversation.id.slice(-8),
3743
- hopCount: requestedHopCount,
3744
4356
  deadlineAt,
3745
4357
  ...(conversation.pair === true ? { pair: true } : {}),
3746
4358
  progressWatch: progressWatchActivity(text, conversation.id, sourceAlias, trackIdleMinutes),
@@ -3749,13 +4361,10 @@ export class GatewayService {
3749
4361
  throw new BridgeError("MESSAGE_REJECTED", "The native reply was not accepted.");
3750
4362
  }
3751
4363
  conversation.nextSequence += 1;
3752
- conversation.lastHopCount = requestedHopCount;
3753
4364
  conversation.lastActivityAt = this.now().toISOString();
3754
4365
  this.messageContexts.set(queued.messageId, {
3755
4366
  conversationId: conversation.id,
3756
- isReply: true,
3757
4367
  expectsReply: false,
3758
- hopCount: requestedHopCount,
3759
4368
  sequence,
3760
4369
  targetBindingKey: bindingKey(capability.binding),
3761
4370
  nativeReplyBinding: { ...capability.binding },
@@ -3787,7 +4396,7 @@ export class GatewayService {
3787
4396
  * native-ingress boundary; this preserves the exact connector generation
3788
4397
  * without recreating route authority or accepting a new unsolicited send.
3789
4398
  */
3790
- async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text, requestedHopCount) {
4399
+ async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text) {
3791
4400
  const target = await this.store.resolveRoute(conversation.sourceAlias);
3792
4401
  if (sourceBinding.provider !== "claude" ||
3793
4402
  target.provider !== "codex" ||
@@ -3806,7 +4415,6 @@ export class GatewayService {
3806
4415
  body: text,
3807
4416
  dedupeKey: `${conversation.id}:${sequence}`,
3808
4417
  conversationIdSuffix: conversation.id.slice(-8),
3809
- hopCount: requestedHopCount,
3810
4418
  deadlineAt,
3811
4419
  authorizedPairTeardownReply: true,
3812
4420
  progressWatch: progressWatchActivity(text, conversation.id, conversation.targetAlias),
@@ -3815,16 +4423,13 @@ export class GatewayService {
3815
4423
  throw new BridgeError("MESSAGE_REJECTED", "The retained provider reply was not accepted.");
3816
4424
  }
3817
4425
  conversation.nextSequence += 1;
3818
- conversation.lastHopCount = requestedHopCount;
3819
4426
  conversation.lastActivityAt = this.now().toISOString();
3820
4427
  // The old edge has already been removed. Retain only this already-observed
3821
4428
  // reply attempt; the conversation no longer grants pair reply authority.
3822
4429
  delete conversation.pair;
3823
4430
  this.messageContexts.set(queued.messageId, {
3824
4431
  conversationId: conversation.id,
3825
- isReply: true,
3826
4432
  expectsReply: false,
3827
- hopCount: requestedHopCount,
3828
4433
  sequence,
3829
4434
  targetBindingKey: bindingKey(target),
3830
4435
  authorization: "selected_route",
@@ -3923,6 +4528,42 @@ export class GatewayService {
3923
4528
  await this.publishNativeHeldLocked(afterCallbacks);
3924
4529
  }
3925
4530
  }
4531
+ restoreQueuedMessageContextLocked(item, binding) {
4532
+ this.pruneTransient();
4533
+ const generatedConversationId = createGatewayConversationId();
4534
+ const conversationId = item.conversationIdSuffix === undefined
4535
+ ? generatedConversationId
4536
+ : `${generatedConversationId.slice(0, -8)}${item.conversationIdSuffix}`;
4537
+ const conversation = {
4538
+ id: conversationId,
4539
+ sourceAlias: item.sourceAlias,
4540
+ targetAlias: item.targetAlias,
4541
+ expectsReply: false,
4542
+ nextSequence: 1,
4543
+ lastActivityAt: this.now().toISOString(),
4544
+ ...(item.pair === true ? { pair: true } : {}),
4545
+ };
4546
+ const context = {
4547
+ conversationId,
4548
+ expectsReply: false,
4549
+ sequence: 0,
4550
+ targetBindingKey: bindingKey(binding),
4551
+ authorization: "selected_route",
4552
+ targetAlias: item.targetAlias,
4553
+ deadlineAt: item.deadlineAt,
4554
+ recovered: true,
4555
+ };
4556
+ this.conversations.set(conversationId, conversation);
4557
+ this.messageContexts.set(item.messageId, context);
4558
+ this.armDeliveryTracker({
4559
+ messageId: item.messageId,
4560
+ conversationId,
4561
+ targetAlias: item.targetAlias,
4562
+ enqueuedAt: Date.parse(item.enqueuedAt),
4563
+ deadlineAt: item.deadlineAt,
4564
+ });
4565
+ return context;
4566
+ }
3926
4567
  async dispatchOne(targetAlias) {
3927
4568
  if (!this.running ||
3928
4569
  this.closing ||
@@ -3949,8 +4590,19 @@ export class GatewayService {
3949
4590
  }
3950
4591
  if (item === undefined)
3951
4592
  return;
3952
- const context = this.messageContexts.get(item.messageId);
3953
4593
  const selectedBinding = this.routeBindings.get(targetAlias);
4594
+ let context = this.messageContexts.get(item.messageId);
4595
+ if (context === undefined && selectedBinding === undefined) {
4596
+ const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
4597
+ if (result.status === "settled") {
4598
+ await this.applyTerminalSettlementLocked(result.settlement);
4599
+ }
4600
+ await this.changed();
4601
+ return;
4602
+ }
4603
+ if (context === undefined && selectedBinding !== undefined) {
4604
+ context = this.restoreQueuedMessageContextLocked(item, selectedBinding);
4605
+ }
3954
4606
  const binding = context?.nativeReplyBinding ?? selectedBinding;
3955
4607
  if (context === undefined) {
3956
4608
  const result = await this.store.settleMessage({
@@ -3974,8 +4626,8 @@ export class GatewayService {
3974
4626
  }
3975
4627
  if (context.authorization === "selected_route") {
3976
4628
  const routeState = this.routeStates.get(targetAlias);
3977
- const dispatchable = routeState === "idle" ||
3978
- (binding.provider === "claude" && routeState === "busy") ||
4629
+ const dispatchable = binding.provider === "claude" ||
4630
+ routeState === "idle" ||
3979
4631
  (binding.provider === "codex" &&
3980
4632
  item.steer === true &&
3981
4633
  routeState === "busy");
@@ -3985,6 +4637,14 @@ export class GatewayService {
3985
4637
  inspection?.state === "stale" ||
3986
4638
  inspection?.safeErrorCode === "CODEX_ROUTE_STALE");
3987
4639
  if (codexRouteStale) {
4640
+ if (context.recovered === true) {
4641
+ const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
4642
+ if (result.status === "settled") {
4643
+ await this.applyTerminalSettlementLocked(result.settlement);
4644
+ }
4645
+ await this.changed();
4646
+ return;
4647
+ }
3988
4648
  await this.advanceDeliveryLocked(item.messageId, {
3989
4649
  type: "route_terminated",
3990
4650
  at: this.now().getTime(),
@@ -3995,8 +4655,8 @@ export class GatewayService {
3995
4655
  this.scheduleDispatch(targetAlias);
3996
4656
  return;
3997
4657
  }
3998
- const storedDispatchable = inspection?.state === "idle" ||
3999
- (binding.provider === "claude" && inspection?.state === "busy") ||
4658
+ const storedDispatchable = binding.provider === "claude" ||
4659
+ inspection?.state === "idle" ||
4000
4660
  (binding.provider === "codex" &&
4001
4661
  item.steer === true &&
4002
4662
  inspection?.state === "busy");
@@ -4088,6 +4748,8 @@ export class GatewayService {
4088
4748
  try {
4089
4749
  result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
4090
4750
  sourceAlias: item.sourceAlias,
4751
+ targetAlias: item.targetAlias,
4752
+ conversationId: context.conversationId,
4091
4753
  binding,
4092
4754
  authorization: context.authorization,
4093
4755
  messageId: item.messageId,
@@ -4126,12 +4788,23 @@ export class GatewayService {
4126
4788
  if (result.state === "deferred") {
4127
4789
  const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
4128
4790
  const now = this.now().getTime();
4791
+ const deferredSafeErrorCode = safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED");
4792
+ const retryDelayMs = binding.provider === "claude"
4793
+ ? Math.min(500 * 2 ** (tracker.machine.dispatchAttempt - 1), 300_000)
4794
+ : 500;
4795
+ // Only Claude's backoff is clamped inside the deadline, so a long delay
4796
+ // still buys one last attempt. A Codex-bound retry keeps its exact
4797
+ // unclamped slice: a slice past the deadline must settle now, not wait.
4798
+ const retryAt = binding.provider === "claude"
4799
+ ? Math.min(now + retryDelayMs, tracker.deadlineAt - 1)
4800
+ : now + retryDelayMs;
4129
4801
  await this.advanceDeliveryLocked(item.messageId, {
4130
4802
  type: "dispatch_clean_prewrite_failed",
4131
4803
  at: now,
4132
- retryAt: now + 500,
4133
- safeErrorCode: safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED"),
4804
+ retryAt,
4805
+ safeErrorCode: deferredSafeErrorCode,
4134
4806
  });
4807
+ tracker.deliverySafeErrorCode = deferredSafeErrorCode;
4135
4808
  const postDeferred = this.deliveryTrackers.get(item.messageId)?.machine;
4136
4809
  if (postDeferred === undefined ||
4137
4810
  isTerminalDeliveryMachine(postDeferred)) {
@@ -4150,13 +4823,13 @@ export class GatewayService {
4150
4823
  await this.changed();
4151
4824
  return;
4152
4825
  }
4153
- const requeued = await this.store.requeueInFlightMessage(item.messageId, item.body);
4826
+ const requeued = await this.store.requeueInFlightMessage(item.messageId, item.body, deferredSafeErrorCode);
4154
4827
  if (this.activeDispatchByTarget.get(currentTargetAlias) === item.messageId) {
4155
4828
  this.activeDispatchByTarget.delete(currentTargetAlias);
4156
4829
  }
4157
4830
  if (requeued.status === "requeued") {
4158
4831
  if (context.authorization === "selected_route") {
4159
- if (!steeringWhileBusy) {
4832
+ if (binding.provider === "codex" && !steeringWhileBusy) {
4160
4833
  this.routeStates.set(currentTargetAlias, "idle");
4161
4834
  await this.store.observeRoute({
4162
4835
  binding,
@@ -4164,7 +4837,7 @@ export class GatewayService {
4164
4837
  compatibility: "compatible",
4165
4838
  });
4166
4839
  }
4167
- else {
4840
+ else if (binding.provider === "codex") {
4168
4841
  this.scheduleHeldRedispatch(currentTargetAlias);
4169
4842
  }
4170
4843
  await this.setNativeCodexStatus(currentTargetAlias, "waiting");
@@ -4275,7 +4948,9 @@ export class GatewayService {
4275
4948
  safeErrorCode: safeCode(event.safeErrorCode, "DISPATCH_OUTCOME_AMBIGUOUS"),
4276
4949
  }
4277
4950
  : event.state === "transport_written"
4278
- ? { type: "transport_written", observedAt: receivedAt }
4951
+ ? target.provider === "claude"
4952
+ ? { type: "provider_released", observedAt: receivedAt }
4953
+ : { type: "transport_written", observedAt: receivedAt }
4279
4954
  : event.state === "held"
4280
4955
  ? { type: "provider_held", observedAt: receivedAt }
4281
4956
  : event.state === "released" || event.state === "completed"
@@ -4367,7 +5042,7 @@ export class GatewayService {
4367
5042
  conversation !== undefined) {
4368
5043
  try {
4369
5044
  if (this.nativeIngressByConversation.has(conversation.id)) {
4370
- await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, context.hopCount + 1, false);
5045
+ await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, false);
4371
5046
  }
4372
5047
  else {
4373
5048
  const sourceBinding = this.routeBindings.get(conversation.targetAlias);
@@ -4382,12 +5057,11 @@ export class GatewayService {
4382
5057
  }));
4383
5058
  if ((!sourceStillRoutable || !pairStillActive) &&
4384
5059
  sourceBinding?.provider === "claude") {
4385
- await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText, context.hopCount + 1);
5060
+ await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText);
4386
5061
  }
4387
5062
  else {
4388
- await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, true, {
5063
+ await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, {
4389
5064
  existingConversationId: conversation.id,
4390
- requestedHopCount: context.hopCount + 1,
4391
5065
  exposeDeliveryToken: false,
4392
5066
  });
4393
5067
  }
@@ -4448,9 +5122,8 @@ export class GatewayService {
4448
5122
  if (conversation === undefined)
4449
5123
  return;
4450
5124
  this.pendingClaudeReplies.delete(key);
4451
- await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, true, {
5125
+ await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, {
4452
5126
  existingConversationId: conversation.id,
4453
- requestedHopCount: pending.hopCount + 1,
4454
5127
  exposeDeliveryToken: false,
4455
5128
  });
4456
5129
  }
@@ -4493,7 +5166,6 @@ export class GatewayService {
4493
5166
  body: event.text,
4494
5167
  dedupeKey: `${conversationId}:0`,
4495
5168
  conversationIdSuffix: conversationId.slice(-8),
4496
- hopCount: 0,
4497
5169
  deadlineAt,
4498
5170
  ...(steer ? { steer: true } : {}),
4499
5171
  progressWatch: progressWatchActivity(event.text, conversationId, event.sourceAlias),
@@ -4511,16 +5183,13 @@ export class GatewayService {
4511
5183
  targetAlias: event.targetAlias,
4512
5184
  expectsReply: !steer,
4513
5185
  nextSequence: 1,
4514
- lastHopCount: 0,
4515
5186
  lastActivityAt: this.now().toISOString(),
4516
5187
  ...(queued.pair === true ? { pair: true } : {}),
4517
5188
  };
4518
5189
  this.conversations.set(conversationId, conversation);
4519
5190
  this.messageContexts.set(queued.messageId, {
4520
5191
  conversationId,
4521
- isReply: false,
4522
5192
  expectsReply: !steer,
4523
- hopCount: 0,
4524
5193
  sequence: 0,
4525
5194
  targetBindingKey: bindingKey(target),
4526
5195
  authorization: "selected_route",
@@ -4586,6 +5255,401 @@ export class GatewayService {
4586
5255
  }
4587
5256
  }
4588
5257
  }
5258
+ async onCodexEndpointRefresh(adapter, event, options = {}) {
5259
+ const transitionKey = endpointRefreshCallbackKey(event);
5260
+ if (this.endpointRefreshCallbackPoisoned &&
5261
+ this.activeEndpointRefreshCallbackKey !== transitionKey) {
5262
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "Conflicting Codex endpoint transitions require a clean broker restart.");
5263
+ }
5264
+ const ownsActiveTransition = this.activeEndpointRefreshCallbackKey === undefined;
5265
+ if (!ownsActiveTransition &&
5266
+ this.activeEndpointRefreshCallbackKey !== transitionKey) {
5267
+ this.poisonEndpointRefreshCallbacks({
5268
+ type: "endpoint_refresh",
5269
+ adapter,
5270
+ value: event,
5271
+ });
5272
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "Conflicting Codex endpoint transitions require a clean broker restart.");
5273
+ }
5274
+ if (ownsActiveTransition) {
5275
+ this.activeEndpointRefreshCallback = {
5276
+ type: "endpoint_refresh",
5277
+ adapter,
5278
+ value: event,
5279
+ };
5280
+ this.activeEndpointRefreshCallbackKey = transitionKey;
5281
+ }
5282
+ let succeeded = false;
5283
+ let activationDeferred = false;
5284
+ try {
5285
+ activationDeferred = await this.applyCodexEndpointRefresh(adapter, event, transitionKey, options);
5286
+ succeeded = true;
5287
+ return activationDeferred;
5288
+ }
5289
+ finally {
5290
+ if (ownsActiveTransition &&
5291
+ this.activeEndpointRefreshCallbackKey === transitionKey) {
5292
+ this.activeEndpointRefreshCallback = undefined;
5293
+ this.activeEndpointRefreshCallbackKey = undefined;
5294
+ const requestedRetry = this.endpointRefreshRetryRequested;
5295
+ this.endpointRefreshRetryRequested = undefined;
5296
+ if ((!succeeded || activationDeferred) &&
5297
+ !this.endpointRefreshCallbackPoisoned &&
5298
+ requestedRetry !== undefined) {
5299
+ this.endpointRefreshCallback = requestedRetry;
5300
+ this.enqueueCallbackWorkerOnly();
5301
+ }
5302
+ }
5303
+ }
5304
+ }
5305
+ async applyCodexEndpointRefresh(adapter, event, transitionKey, options) {
5306
+ if (this.closing ||
5307
+ (event.outcome !== "compatible" && event.outcome !== "incompatible") ||
5308
+ !isCompatibilityAttestation(event.attestation) ||
5309
+ event.attestation.surface !== "codex" ||
5310
+ event.previous.provider !== "codex" ||
5311
+ !PRIVATE_HANDLE.test(event.previous.endpointGeneration) ||
5312
+ adapter.identity.provider !== "codex" ||
5313
+ adapter.identity.hostId !== event.previous.hostId ||
5314
+ !this.config.allowedHosts.includes(event.previous.hostId)) {
5315
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The Codex endpoint transition failed closed runtime validation.");
5316
+ }
5317
+ if (event.outcome === "compatible") {
5318
+ const routeHandles = Array.isArray(event.routes)
5319
+ ? event.routes.map((route) => route.routeHandle)
5320
+ : [];
5321
+ if (event.current.provider !== "codex" ||
5322
+ event.current.hostId !== event.previous.hostId ||
5323
+ !PRIVATE_HANDLE.test(event.current.endpointGeneration) ||
5324
+ event.current.endpointGeneration ===
5325
+ event.previous.endpointGeneration ||
5326
+ adapter.identity.endpointGeneration !==
5327
+ event.current.endpointGeneration ||
5328
+ event.attestation.version !== adapter.protocolVersion ||
5329
+ event.attestation.tier === "incompatible" ||
5330
+ !Array.isArray(event.routes) ||
5331
+ event.routes.length > this.config.limits.maxRoutes ||
5332
+ new Set(routeHandles).size !== routeHandles.length ||
5333
+ event.routes.some((route) => !PRIVATE_HANDLE.test(route.routeHandle) ||
5334
+ !["idle", "busy", "awaiting_approval"].includes(route.state))) {
5335
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The refreshed Codex provider identity changed before activation.");
5336
+ }
5337
+ if (this.activatedEndpointRefreshes.get(event.current.hostId) ===
5338
+ transitionKey) {
5339
+ return false;
5340
+ }
5341
+ }
5342
+ else {
5343
+ if (event.candidate.provider !== "codex" ||
5344
+ event.candidate.hostId !== event.previous.hostId ||
5345
+ !PRIVATE_HANDLE.test(event.candidate.endpointGeneration) ||
5346
+ event.candidate.endpointGeneration ===
5347
+ event.previous.endpointGeneration ||
5348
+ adapter.identity.endpointGeneration !==
5349
+ event.previous.endpointGeneration ||
5350
+ event.attestation.tier !== "incompatible") {
5351
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The incompatible Codex candidate no longer matches the active endpoint.");
5352
+ }
5353
+ if (this.handledIncompatibleEndpointRefreshes.get(event.previous.hostId) === transitionKey) {
5354
+ return false;
5355
+ }
5356
+ }
5357
+ let inventory = await this.store.inspectPrivateCodexRoutes();
5358
+ const previousRoutes = inventory.filter((route) => route.binding.hostId === event.previous.hostId &&
5359
+ route.binding.endpointGeneration === event.previous.endpointGeneration);
5360
+ const aliases = previousRoutes.map((route) => route.alias);
5361
+ await this.drainPreDeadlineDeliveryCallbacksLocked();
5362
+ const inFlightSettlements = aliases.length === 0
5363
+ ? []
5364
+ : await this.planRoutesInFlightSettlementsLocked(aliases, {
5365
+ unwrittenOutcome: "failed",
5366
+ safeErrorCode: "ENDPOINT_GENERATION_CHANGED",
5367
+ });
5368
+ let settlements = [];
5369
+ try {
5370
+ settlements = await this.store.markConnectorOffline(event.previous, "ENDPOINT_GENERATION_CHANGED", inFlightSettlements, { preserveQueued: true });
5371
+ }
5372
+ catch (error) {
5373
+ const alreadyInvalidatedCompatibleTransition = event.outcome === "compatible" &&
5374
+ error instanceof BridgeError &&
5375
+ error.code === "CONNECTOR_NOT_FOUND" &&
5376
+ previousRoutes.every((route) => route.state === "stale" && route.compatibility === "expired");
5377
+ if (!alreadyInvalidatedCompatibleTransition)
5378
+ throw error;
5379
+ }
5380
+ for (const settlement of settlements) {
5381
+ await this.applyTerminalSettlementLocked(settlement);
5382
+ }
5383
+ for (const route of previousRoutes) {
5384
+ this.routeStates.set(route.alias, "stale");
5385
+ }
5386
+ await this.store.recordCompatibilityAttestation(event.attestation);
5387
+ if (event.outcome === "incompatible") {
5388
+ await this.requireEndpointRefreshUnconflicted();
5389
+ this.activeCodexIncompatibility = structuredClone(event.attestation);
5390
+ this.addRuntimeAlert(event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE", "error", { provider: "codex", host: event.previous.hostId });
5391
+ this.handledIncompatibleEndpointRefreshes.set(event.previous.hostId, transitionKey);
5392
+ await this.changed();
5393
+ return false;
5394
+ }
5395
+ await this.store.observeConnector({
5396
+ identity: event.current,
5397
+ health: "healthy",
5398
+ compatibility: "compatible",
5399
+ protocol: adapter.protocol,
5400
+ protocolVersion: adapter.protocolVersion,
5401
+ });
5402
+ const refreshedByHandle = new Map(event.routes.map((route) => [route.routeHandle, route.state]));
5403
+ const pendingReanchors = previousRoutes.flatMap((route) => {
5404
+ const state = refreshedByHandle.get(route.binding.routeHandle);
5405
+ return state === undefined
5406
+ ? []
5407
+ : [
5408
+ {
5409
+ alias: route.alias,
5410
+ threadId: route.binding.routeHandle,
5411
+ ownerLease: route.binding.ownerLease,
5412
+ state,
5413
+ },
5414
+ ];
5415
+ });
5416
+ if (pendingReanchors.length > 0) {
5417
+ try {
5418
+ await this.store.reanchorCodexRoutes({
5419
+ oldEndpoint: event.previous,
5420
+ newEndpoint: event.current,
5421
+ routes: pendingReanchors,
5422
+ });
5423
+ this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
5424
+ }
5425
+ catch (error) {
5426
+ // Persistence may commit and then report an unknown outcome. Because
5427
+ // service/store mutation is serialized, exact G2 ownership for every
5428
+ // requested G1 alias proves this transition durably applied and gives
5429
+ // a later identical retry authority to restore a fail-closed G2 route.
5430
+ const after = await this.store.inspectPrivateCodexRoutes();
5431
+ const committedExactly = pendingReanchors.every((expected) => {
5432
+ const matches = after.filter((route) => route.alias === expected.alias &&
5433
+ route.binding.provider === "codex" &&
5434
+ route.binding.hostId === event.current.hostId &&
5435
+ route.binding.endpointGeneration ===
5436
+ event.current.endpointGeneration &&
5437
+ route.binding.routeHandle === expected.threadId &&
5438
+ route.binding.ownerLease === expected.ownerLease &&
5439
+ route.enabled &&
5440
+ route.compatibility === "compatible");
5441
+ return matches.length === 1;
5442
+ });
5443
+ if (committedExactly) {
5444
+ this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
5445
+ try {
5446
+ await this.store.markConnectorOffline(event.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
5447
+ for (const route of pendingReanchors) {
5448
+ this.routeStates.set(route.alias, "stale");
5449
+ }
5450
+ await this.changed();
5451
+ }
5452
+ catch {
5453
+ this.dashboardHealthy = false;
5454
+ }
5455
+ }
5456
+ throw error;
5457
+ }
5458
+ }
5459
+ // A retry may arrive after the store commit but before native status or
5460
+ // provider activation. Re-read exact durable authority and accept only one
5461
+ // unique current-generation owner for every claimed task.
5462
+ inventory = await this.store.inspectPrivateCodexRoutes();
5463
+ if (this.durablyAppliedEndpointRefreshes.get(event.current.hostId) ===
5464
+ transitionKey) {
5465
+ const observedStates = new Map(event.routes.map((route) => [route.routeHandle, route.state]));
5466
+ for (const route of inventory) {
5467
+ const state = observedStates.get(route.binding.routeHandle);
5468
+ if (state === undefined ||
5469
+ route.binding.hostId !== event.current.hostId ||
5470
+ route.binding.endpointGeneration !==
5471
+ event.current.endpointGeneration ||
5472
+ route.state !== "stale" ||
5473
+ route.compatibility !== "expired") {
5474
+ continue;
5475
+ }
5476
+ await this.registerOrRebind(route.alias, route.binding, "endpoint_reobserved", state, route.binding.ownerLease);
5477
+ }
5478
+ inventory = await this.store.inspectPrivateCodexRoutes();
5479
+ }
5480
+ const previousRouteHandles = new Set(previousRoutes.map((route) => route.binding.routeHandle));
5481
+ const activatedRoutes = event.routes.flatMap((observed) => {
5482
+ const matches = inventory.filter((route) => route.binding.provider === "codex" &&
5483
+ route.binding.hostId === event.current.hostId &&
5484
+ route.binding.endpointGeneration ===
5485
+ event.current.endpointGeneration &&
5486
+ route.binding.routeHandle === observed.routeHandle &&
5487
+ route.enabled &&
5488
+ route.compatibility === "compatible");
5489
+ if (matches.length > 1) {
5490
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "A refreshed Codex task has multiple durable owners.");
5491
+ }
5492
+ if (matches.length === 0) {
5493
+ // A selector-triggered refresh can report the freshly selected task
5494
+ // before registerRoute installs that new alias. It has no prior G1
5495
+ // owner to re-anchor, so activation covers only already-durable tasks;
5496
+ // the caller installs this one immediately after the provider ack.
5497
+ if (!previousRouteHandles.has(observed.routeHandle))
5498
+ return [];
5499
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "A previously durable Codex task was not re-anchored exactly.");
5500
+ }
5501
+ const match = matches[0];
5502
+ if (match === undefined)
5503
+ return [];
5504
+ return {
5505
+ alias: match.alias,
5506
+ threadId: observed.routeHandle,
5507
+ ownerLease: match.binding.ownerLease,
5508
+ state: observed.state,
5509
+ };
5510
+ });
5511
+ const pendingSelector = this.pendingSelectorEndpointActivations.get(event.current.hostId);
5512
+ const requiredRoute = options.deferActivation === true
5513
+ ? undefined
5514
+ : options.requiredRoute ??
5515
+ (pendingSelector?.transitionKey === transitionKey
5516
+ ? pendingSelector.route
5517
+ : undefined);
5518
+ if (requiredRoute !== undefined &&
5519
+ !activatedRoutes.some((route) => route.alias === requiredRoute.alias &&
5520
+ route.threadId === requiredRoute.threadId)) {
5521
+ const matches = inventory.filter((route) => route.alias === requiredRoute.alias &&
5522
+ route.binding.provider === "codex" &&
5523
+ route.binding.hostId === event.current.hostId &&
5524
+ route.binding.endpointGeneration ===
5525
+ event.current.endpointGeneration &&
5526
+ route.binding.routeHandle === requiredRoute.threadId &&
5527
+ route.enabled &&
5528
+ route.compatibility === "compatible");
5529
+ const match = matches[0];
5530
+ if (matches.length !== 1 || match === undefined) {
5531
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The registering Codex task was not durably installed exactly.");
5532
+ }
5533
+ activatedRoutes.push({
5534
+ alias: requiredRoute.alias,
5535
+ threadId: requiredRoute.threadId,
5536
+ ownerLease: match.binding.ownerLease,
5537
+ state: requiredRoute.state,
5538
+ });
5539
+ }
5540
+ if (options.deferActivation === true) {
5541
+ const deferredRoute = options.deferredRoute;
5542
+ if (deferredRoute === undefined) {
5543
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "A deferred selector transition requires one exact registration.");
5544
+ }
5545
+ const existing = this.pendingSelectorEndpointActivations.get(event.current.hostId);
5546
+ if (existing !== undefined &&
5547
+ (existing.transitionKey !== transitionKey ||
5548
+ existing.route.alias !== deferredRoute.alias ||
5549
+ existing.route.threadId !== deferredRoute.threadId)) {
5550
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "A different selector transition is already awaiting native activation.");
5551
+ }
5552
+ this.pendingSelectorEndpointActivations.set(event.current.hostId, {
5553
+ transitionKey,
5554
+ route: { ...deferredRoute },
5555
+ });
5556
+ return true;
5557
+ }
5558
+ const requireNativeContinuity = (route) => {
5559
+ const registration = this.codexRegistrationLocks.get(route.alias);
5560
+ if (registration === undefined ||
5561
+ registration.hostId !== event.current.hostId ||
5562
+ registration.threadId !== route.threadId ||
5563
+ !isCodexRegistrationGeneration(registration.generation)) {
5564
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_LISTENER_MISSING", "The refreshed Codex route has no exact live native listener authority.");
5565
+ }
5566
+ const nativeGeneration = this.requireCurrentNativeCodexGeneration(this.adapter("claude", event.current.hostId), route.alias);
5567
+ if (nativeGeneration !== registration.generation) {
5568
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_LISTENER_MISSING", "The refreshed Codex route's native listener generation changed before activation.");
5569
+ }
5570
+ };
5571
+ try {
5572
+ for (const route of activatedRoutes) {
5573
+ const binding = {
5574
+ ...event.current,
5575
+ routeHandle: route.threadId,
5576
+ ownerLease: route.ownerLease,
5577
+ };
5578
+ this.rememberBinding(route.alias, binding, route.state);
5579
+ }
5580
+ for (const route of activatedRoutes) {
5581
+ requireNativeContinuity(route);
5582
+ await this.setNativeCodexStatus(route.alias, route.state === "idle"
5583
+ ? "idle"
5584
+ : route.state === "awaiting_approval"
5585
+ ? "waiting"
5586
+ : "busy", true);
5587
+ requireNativeContinuity(route);
5588
+ }
5589
+ await this.requireEndpointRefreshUnconflicted();
5590
+ for (const route of activatedRoutes) {
5591
+ const activityKey = `${transitionKey}\0${route.alias}`;
5592
+ if (!this.endpointRefreshActivityKeys.has(activityKey)) {
5593
+ this.endpointRefreshActivityKeys.add(activityKey);
5594
+ while (this.endpointRefreshActivityKeys.size > RUNTIME_ACTIVITY_CAPACITY) {
5595
+ const oldest = this.endpointRefreshActivityKeys.values().next().value;
5596
+ if (oldest === undefined)
5597
+ break;
5598
+ this.endpointRefreshActivityKeys.delete(oldest);
5599
+ }
5600
+ this.activitySequence += 1;
5601
+ this.activityEvents.push({
5602
+ sequence: this.activitySequence,
5603
+ timestamp: this.now().toISOString(),
5604
+ kind: "endpoint",
5605
+ action: "endpoint_refreshed",
5606
+ outcome: "accepted",
5607
+ aliases: [route.alias],
5608
+ operatorAction: false,
5609
+ });
5610
+ while (this.activityEvents.length > RUNTIME_ACTIVITY_CAPACITY) {
5611
+ this.activityEvents.shift();
5612
+ }
5613
+ }
5614
+ if (route.state === "idle")
5615
+ this.scheduleDispatch(route.alias);
5616
+ }
5617
+ await this.changed();
5618
+ await this.requireEndpointRefreshUnconflicted();
5619
+ for (const route of activatedRoutes) {
5620
+ requireNativeContinuity(route);
5621
+ }
5622
+ // Provider writes remain closed until every durable and native transition
5623
+ // above is complete. This acknowledgement is deliberately the final
5624
+ // fallible activation step.
5625
+ const accept = adapter.acceptCompatibilityAttestation;
5626
+ if (accept === undefined) {
5627
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_ACTIVATION_UNAVAILABLE", "The Codex provider cannot acknowledge the validated endpoint transition.");
5628
+ }
5629
+ accept.call(adapter, event.attestation);
5630
+ }
5631
+ catch (error) {
5632
+ try {
5633
+ await this.store.markConnectorOffline(event.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
5634
+ for (const route of activatedRoutes) {
5635
+ this.routeStates.set(route.alias, "stale");
5636
+ }
5637
+ await this.changed();
5638
+ }
5639
+ catch {
5640
+ this.dashboardHealthy = false;
5641
+ }
5642
+ throw error;
5643
+ }
5644
+ this.activatedEndpointRefreshes.set(event.current.hostId, transitionKey);
5645
+ if (this.pendingSelectorEndpointActivations.get(event.current.hostId)
5646
+ ?.transitionKey === transitionKey) {
5647
+ this.pendingSelectorEndpointActivations.delete(event.current.hostId);
5648
+ }
5649
+ this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
5650
+ this.activeCodexIncompatibility = undefined;
5651
+ return false;
5652
+ }
4589
5653
  async onRouteState(source, event) {
4590
5654
  if (this.closing)
4591
5655
  return;
@@ -4655,15 +5719,27 @@ export class GatewayService {
4655
5719
  this.scheduleDispatch(alias);
4656
5720
  }
4657
5721
  }
4658
- async setNativeCodexStatus(alias, status) {
5722
+ async setNativeCodexStatus(alias, status, requireSuccess = false) {
4659
5723
  const binding = this.routeBindings.get(alias);
4660
5724
  if (binding?.provider !== "codex")
4661
5725
  return;
4662
- await this.adapter("claude", binding.hostId)
4663
- .updateNativeCodexPeerStatus?.(alias, status)
4664
- .catch(() => {
5726
+ const adapter = this.adapter("claude", binding.hostId);
5727
+ const update = adapter.updateNativeCodexPeerStatus;
5728
+ if (update === undefined) {
5729
+ if (requireSuccess) {
5730
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_STATUS_FAILED", "The native listener does not support refreshed Codex status activation.", true);
5731
+ }
5732
+ return;
5733
+ }
5734
+ try {
5735
+ await update.call(adapter, alias, status);
5736
+ }
5737
+ catch (error) {
4665
5738
  this.dashboardHealthy = false;
4666
- });
5739
+ if (requireSuccess) {
5740
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_STATUS_FAILED", "The refreshed Codex route could not be activated in the native listener.", true);
5741
+ }
5742
+ }
4667
5743
  }
4668
5744
  rejectDetachedNativeReceipt(hostId, receiptHandle, diagnosticCode) {
4669
5745
  const work = this.settleDetachedNativeReceipt(hostId, receiptHandle, diagnosticCode).finally(() => {
@@ -4748,15 +5824,29 @@ export class GatewayService {
4748
5824
  async publicSnapshotLocked() {
4749
5825
  const base = await this.store.publicSnapshot();
4750
5826
  const compatibilityChecks = [];
4751
- for (const adapter of this.adapters) {
4752
- const observation = adapter.compatibilitySurface?.();
4753
- if (observation === undefined)
5827
+ for (const surface of compatibilitySurfaces) {
5828
+ if (surface === "codex" && this.activeCodexIncompatibility !== undefined) {
5829
+ compatibilityChecks.push(structuredClone(this.activeCodexIncompatibility));
4754
5830
  continue;
4755
- const attestation = await this.store.inspectCompatibilityAttestation(observation.surface, observation.version);
4756
- if (attestation !== undefined)
4757
- compatibilityChecks.push(attestation);
5831
+ }
5832
+ const currentVersions = [
5833
+ ...new Set(this.adapters.flatMap((adapter) => {
5834
+ const observation = adapter.compatibilitySurface?.();
5835
+ return observation?.surface === surface ? [observation.version] : [];
5836
+ })),
5837
+ ].sort((left, right) => left.localeCompare(right));
5838
+ const currentAttestations = (await Promise.all(currentVersions.map(async (version) => await this.store.inspectCompatibilityAttestation(surface, version))))
5839
+ .filter((attestation) => attestation !== undefined)
5840
+ .sort((left, right) => {
5841
+ const checkedAt = right.checkedAt.localeCompare(left.checkedAt);
5842
+ if (checkedAt !== 0)
5843
+ return checkedAt;
5844
+ return left.version.localeCompare(right.version);
5845
+ });
5846
+ const selected = currentAttestations[0];
5847
+ if (selected !== undefined)
5848
+ compatibilityChecks.push(selected);
4758
5849
  }
4759
- compatibilityChecks.sort((left, right) => left.surface.localeCompare(right.surface));
4760
5850
  const peers = this.availablePeers.map((peer) => ({ ...peer }));
4761
5851
  if (!arePublicAvailablePeerSnapshots(peers)) {
4762
5852
  throw new BridgeError("INVALID_TRANSIENT_INVENTORY", "The transient peer inventory is unsafe.");
@@ -4782,21 +5872,6 @@ export class GatewayService {
4782
5872
  },
4783
5873
  ];
4784
5874
  });
4785
- const certificationAlerts = compatibilityChecks.flatMap((attestation) => {
4786
- const certification = attestation.certification;
4787
- if (certification?.outcome !== "fail")
4788
- return [];
4789
- const adapter = this.adapters.find((candidate) => candidate.identity.provider === attestation.surface);
4790
- return [
4791
- {
4792
- code: "COMPATIBILITY_CERTIFICATION_FAILED",
4793
- severity: "error",
4794
- timestamp: certification.certifiedAt,
4795
- provider: attestation.surface,
4796
- host: adapter?.identity.hostId ?? "this-mac",
4797
- },
4798
- ];
4799
- });
4800
5875
  return projectGatewayPublicSnapshot({
4801
5876
  ...base,
4802
5877
  ...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
@@ -4812,7 +5887,6 @@ export class GatewayService {
4812
5887
  alerts: [
4813
5888
  ...base.alerts,
4814
5889
  ...this.runtimeAlerts.map((alert) => ({ ...alert })),
4815
- ...certificationAlerts,
4816
5890
  ...stalledAlerts,
4817
5891
  ],
4818
5892
  });