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,13 +1,13 @@
1
- import { createHash, randomBytes } from "node:crypto";
1
+ import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { BridgeError } from "../errors.js";
4
4
  import { CLAUDE_PEER_COMPATIBILITY, ClaudePeerAdapter, } from "./claude-peer.js";
5
- import { startClaudeCompatibilityScratch, } from "./claude-compatibility-scratch.js";
6
- import { compatibilityProbeNames, sharesCompatibilityMajor, } from "./compatibility.js";
5
+ import { certifiedCompatibilityVersions, compatibilityProbeNames, evaluateCompatibilityAttestation, sharesCompatibilityMajor, } from "./compatibility.js";
7
6
  import { CODEX_APP_SERVER_WRITABLE_VERSIONS, CodexAppServerConnector, CodexConnectorError, } from "./codex-app-server.js";
8
7
  import { LocalCodexTransportError } from "./codex-local-transport.js";
9
8
  import { ClaudeNativeHelperSupervisor, } from "./claude-helper-supervisor.js";
10
9
  import { isDashboardLocale } from "./locale.js";
10
+ import { composeProvenanceEnvelope } from "./provenance-envelope.js";
11
11
  const LOCAL_HOST = "this-mac";
12
12
  const NATIVE_CLAUDE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
13
13
  const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
@@ -22,10 +22,42 @@ const DEFAULT_CODEX_CLEANUP_TIMEOUT_MS = 5_000;
22
22
  const DEFAULT_CODEX_CLEANUP_POLL_MS = 25;
23
23
  const DEFAULT_CODEX_RECOVERY_INITIAL_MS = 250;
24
24
  const DEFAULT_CODEX_RECOVERY_MAX_MS = 5_000;
25
+ const MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES = 3;
25
26
  const COMPATIBILITY_PROBE_THREAD_ID = "00000000-0000-7000-8000-000000000000";
26
- const COMPATIBILITY_TURN_TIMEOUT_MS = 60_000;
27
- const CLAUDE_CERTIFICATION_TIMEOUT_MS = 10_000;
28
- const CLAUDE_CERTIFICATION_DISCOVERY_POLL_MS = 100;
27
+ const CLAUDE_CLEAN_PREWRITE_RETRY_CODES = new Set([
28
+ "CLAUDE_PEER_TARGET_UNKNOWN",
29
+ "CLAUDE_PEER_TARGET_STALE",
30
+ "CLAUDE_PEER_TARGET_CHANGED",
31
+ "CLAUDE_PEER_WORKSPACE_UNATTESTED",
32
+ ]);
33
+ function claudeCleanPrewriteResult(error) {
34
+ if (error instanceof BridgeError &&
35
+ error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
36
+ return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
37
+ }
38
+ if (error instanceof BridgeError &&
39
+ CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
40
+ return { state: "deferred", safeErrorCode: error.code };
41
+ }
42
+ const systemCode = typeof error === "object" &&
43
+ error !== null &&
44
+ "code" in error &&
45
+ typeof error.code === "string"
46
+ ? error.code
47
+ : undefined;
48
+ return {
49
+ state: "failed",
50
+ safeErrorCode: error instanceof BridgeError
51
+ ? error.code
52
+ : systemCode === "ENOENT"
53
+ ? "CLAUDE_DISPATCH_PREWRITE_PATH_MISSING"
54
+ : systemCode === "EACCES" || systemCode === "EPERM"
55
+ ? "CLAUDE_DISPATCH_PREWRITE_ACCESS_DENIED"
56
+ : systemCode === "ETIMEDOUT"
57
+ ? "CLAUDE_DISPATCH_PREWRITE_TIMEOUT"
58
+ : "CLAUDE_DISPATCH_PREWRITE_FAILED",
59
+ };
60
+ }
29
61
  function passedProbe(name) {
30
62
  return { name, outcome: "pass" };
31
63
  }
@@ -73,7 +105,8 @@ function invokeCallback(operation) {
73
105
  }
74
106
  }
75
107
  function validateAttestedClaudeRuntime(runtime) {
76
- if (!sharesCompatibilityMajor(runtime.claudeCodeVersion, CLAUDE_PEER_COMPATIBILITY.claudeCodeVersion) ||
108
+ if (runtime.claudeCodeVersion !==
109
+ CLAUDE_PEER_COMPATIBILITY.claudeCodeVersion ||
77
110
  [
78
111
  runtime.claudeExecutable,
79
112
  runtime.sessionsDir,
@@ -110,7 +143,6 @@ export class LocalClaudeGatewayProvider {
110
143
  now;
111
144
  selectedStateRoots = new Map();
112
145
  nativeHelpers;
113
- compatibilityScratchFactory;
114
146
  discovered = new Map();
115
147
  selected = new Map();
116
148
  /** Exact live same-UID sessions observed through native inbound frames. */
@@ -143,7 +175,6 @@ export class LocalClaudeGatewayProvider {
143
175
  retiredNativeCodexListener;
144
176
  /** Fences discovery callbacks and monitor restarts across listener rotation. */
145
177
  nativeCodexSuccessionFreeze;
146
- compatibilityCertificationInFlight = false;
147
178
  initialized = false;
148
179
  closed = false;
149
180
  constructor(options) {
@@ -169,9 +200,6 @@ export class LocalClaudeGatewayProvider {
169
200
  locale,
170
201
  deliveryNotices,
171
202
  })))(options.runtime, options.locale ?? "en", options.deliveryNotices ?? "merged");
172
- this.compatibilityScratchFactory =
173
- options.compatibilityScratchFactory ??
174
- (() => startClaudeCompatibilityScratch(options.runtime));
175
203
  this.nativeHelpers =
176
204
  options.nativeHelpers === undefined
177
205
  ? undefined
@@ -222,90 +250,6 @@ export class LocalClaudeGatewayProvider {
222
250
  ];
223
251
  }
224
252
  }
225
- async runCompatibilityCertification(input) {
226
- void input.routeHandle;
227
- void input.withTurn;
228
- const depth = "wire";
229
- let scratch;
230
- let listener;
231
- if (this.compatibilityCertificationInFlight) {
232
- return {
233
- depth,
234
- outcome: "fail",
235
- certifiedAt: new Date(this.now()).toISOString(),
236
- safeErrorCode: "CLAUDE_CERTIFICATION_BUSY",
237
- };
238
- }
239
- this.compatibilityCertificationInFlight = true;
240
- try {
241
- this.assertReady();
242
- scratch = await this.compatibilityScratchFactory();
243
- const deadlineAt = this.now() + CLAUDE_CERTIFICATION_TIMEOUT_MS;
244
- let targetId;
245
- while (this.now() < deadlineAt) {
246
- scratch.assertRunning();
247
- const discovery = await this.peer.discover();
248
- if (discovery.truncated) {
249
- throw new BridgeError("CLAUDE_CERTIFICATION_DISCOVERY_INCOMPLETE", "The scratch session could not be proven from a complete registry scan.", true);
250
- }
251
- const matches = discovery.peers.filter((peer) => peer.alias === scratch?.name && peer.targetId === scratch?.sessionId);
252
- if (matches.length > 1) {
253
- throw new BridgeError("CLAUDE_CERTIFICATION_TARGET_COLLISION", "The scratch session identity was not unique.");
254
- }
255
- if (matches.length === 1) {
256
- targetId = matches[0]?.targetId;
257
- break;
258
- }
259
- await new Promise((resolve) => {
260
- const timer = setTimeout(resolve, CLAUDE_CERTIFICATION_DISCOVERY_POLL_MS);
261
- timer.unref();
262
- });
263
- }
264
- if (targetId === undefined) {
265
- throw new BridgeError("CLAUDE_CERTIFICATION_TARGET_UNAVAILABLE", "The bounded scratch session did not publish a discoverable peer record.", true);
266
- }
267
- await this.peer.assertTargetWorkspaceDisjoint(targetId, input.controllerStateRoot);
268
- listener = await this.peer.listen({
269
- onMessage: () => undefined,
270
- onReceipt: () => undefined,
271
- });
272
- const sent = await this.peer.send(targetId, "[embassy compat-certify] ignore", {
273
- listener,
274
- receiptDeadlineAt: deadlineAt,
275
- });
276
- if (sent.transportStatus !== "transport_written" ||
277
- sent.receiptStatus !== "pending") {
278
- throw new BridgeError("CLAUDE_CERTIFICATION_WRITE_UNCONFIRMED", "The scratch diagnostic frame was not accepted for receipt tracking.", true);
279
- }
280
- // A direct `crossSessionInbound: accept` write emits no native
281
- // peer_message_status frame. That status family describes the optional
282
- // approval lifecycle, not universal delivery acknowledgement. For this
283
- // wire-depth certification, the strongest portable evidence is the
284
- // confirmed write to the exact, uniquely discovered scratch session.
285
- return {
286
- depth,
287
- outcome: "pass",
288
- certifiedAt: new Date(this.now()).toISOString(),
289
- };
290
- }
291
- catch (error) {
292
- const observedCode = error instanceof BridgeError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code)
293
- ? error.code
294
- : "CLAUDE_CERTIFICATION_FAILED";
295
- return {
296
- depth,
297
- outcome: "fail",
298
- certifiedAt: new Date(this.now()).toISOString(),
299
- safeErrorCode: observedCode,
300
- };
301
- }
302
- finally {
303
- await listener?.close().catch(() => undefined);
304
- await scratch?.close().catch(() => undefined);
305
- await this.peer.discover().catch(() => undefined);
306
- this.compatibilityCertificationInFlight = false;
307
- }
308
- }
309
253
  async initialize(callbacks) {
310
254
  if (this.closed) {
311
255
  throw new BridgeError("CLAUDE_PROVIDER_CLOSED", "The local Claude provider is closed.");
@@ -881,22 +825,22 @@ export class LocalClaudeGatewayProvider {
881
825
  return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
882
826
  }
883
827
  if (input.authorization === "native_reply") {
884
- try {
885
- await this.refreshClaudeDiscovery();
886
- }
887
- catch {
888
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
889
- }
890
828
  const observedRoute = this.nativeInbound.get(input.binding.routeHandle);
891
- const current = this.discovered.get(input.binding.routeHandle);
892
829
  if (observedRoute === undefined ||
893
- observedRoute.listenerGeneration !== activeListener.generation ||
894
- current?.alias !== observedRoute.alias) {
830
+ observedRoute.listenerGeneration !== activeListener.generation) {
895
831
  this.nativeInbound.delete(input.binding.routeHandle);
896
832
  return { state: "failed", safeErrorCode: "CLAUDE_NATIVE_REPLY_STALE" };
897
833
  }
898
834
  }
899
- else if (this.selected.get(input.binding.routeHandle) === undefined) {
835
+ else {
836
+ const selectedAlias = this.selected.get(input.binding.routeHandle);
837
+ const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
838
+ if (selectedAlias === undefined ||
839
+ stateRoot === undefined) {
840
+ return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
841
+ }
842
+ }
843
+ if (activeListener.registration?.alias !== input.sourceAlias) {
900
844
  return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
901
845
  }
902
846
  if (this.activeNativeCodexListener !== activeListener ||
@@ -912,15 +856,44 @@ export class LocalClaudeGatewayProvider {
912
856
  this.providerIdByGatewayId.has(input.messageId)) {
913
857
  return { state: "failed", safeErrorCode: "CLAUDE_RECEIPT_CAPACITY" };
914
858
  }
915
- this.providerIdByGatewayId.set(input.messageId, "");
916
- this.pendingTargetByGatewayId.set(input.messageId, input.binding.routeHandle);
859
+ let content;
860
+ try {
861
+ content = composeProvenanceEnvelope({
862
+ direction: "claude",
863
+ sourceAlias: input.sourceAlias,
864
+ targetAlias: input.targetAlias,
865
+ conversationId: input.conversationId,
866
+ body: input.text,
867
+ });
868
+ }
869
+ catch (error) {
870
+ if (error instanceof BridgeError &&
871
+ (error.code === "PROVENANCE_ENVELOPE_INVALID" ||
872
+ error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")) {
873
+ return { state: "failed", safeErrorCode: error.code };
874
+ }
875
+ throw error;
876
+ }
917
877
  if (input.authorization === "selected_route") {
878
+ const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
879
+ if (stateRoot === undefined) {
880
+ return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
881
+ }
918
882
  this.selectedDispatchEpoch.set(input.binding.routeHandle, (this.selectedDispatchEpoch.get(input.binding.routeHandle) ?? 0) + 1);
919
- // Once a selected-route dispatch begins, keep the route conservatively
920
- // busy until a later registry refresh observes authoritative state.
883
+ // The exact workspace attestation is refreshed before every send. A
884
+ // clean prewrite failure remains retryable within the message deadline;
885
+ // retry timing does not depend on the model turn's observed state.
921
886
  this.selectedObservationDirty.add(input.binding.routeHandle);
922
887
  this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
888
+ try {
889
+ await this.peer.assertTargetWorkspaceDisjoint(input.binding.routeHandle, stateRoot);
890
+ }
891
+ catch (error) {
892
+ return claudeCleanPrewriteResult(error);
893
+ }
923
894
  }
895
+ this.providerIdByGatewayId.set(input.messageId, "");
896
+ this.pendingTargetByGatewayId.set(input.messageId, input.binding.routeHandle);
924
897
  let providerId;
925
898
  let trackingFailed = false;
926
899
  let terminalDuringSend = false;
@@ -962,7 +935,7 @@ export class LocalClaudeGatewayProvider {
962
935
  // listener's bounded timeout is authoritative and may settle it later.
963
936
  };
964
937
  try {
965
- const result = await this.peer.send(input.binding.routeHandle, input.text, {
938
+ const result = await this.peer.send(input.binding.routeHandle, content, {
966
939
  listener: activeListener.listener,
967
940
  receiptDeadlineAt: deadline,
968
941
  onTransportStatus,
@@ -1017,11 +990,7 @@ export class LocalClaudeGatewayProvider {
1017
990
  }
1018
991
  this.providerIdByGatewayId.delete(input.messageId);
1019
992
  this.pendingTargetByGatewayId.delete(input.messageId);
1020
- if (error instanceof BridgeError &&
1021
- error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
1022
- return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
1023
- }
1024
- return { state: "failed", safeErrorCode: "CLAUDE_DISPATCH_REJECTED" };
993
+ return claudeCleanPrewriteResult(error);
1025
994
  }
1026
995
  }
1027
996
  async releaseRoute(routeHandle) {
@@ -1483,29 +1452,16 @@ export class LocalClaudeGatewayProvider {
1483
1452
  this.discovered.set(peer.targetId, row);
1484
1453
  rows.push(row);
1485
1454
  }
1486
- for (const [routeHandle, route] of this.nativeInbound) {
1487
- const current = this.discovered.get(routeHandle);
1488
- if (current !== undefined) {
1489
- // The stable session UUID owns native reply authority. A rename only
1490
- // updates its current public coordinate.
1491
- this.nativeInbound.set(routeHandle, {
1492
- alias: current.alias,
1493
- listenerGeneration: route.listenerGeneration,
1494
- });
1495
- }
1496
- else if (!discovery.truncated) {
1497
- this.nativeInbound.delete(routeHandle);
1498
- }
1499
- }
1500
1455
  for (const [routeHandle, selectedAlias] of this.selected) {
1501
1456
  const row = this.discovered.get(routeHandle);
1502
1457
  if (row === undefined) {
1503
1458
  this.emitClaudeRouteObservation(routeHandle, "busy", "CLAUDE_PEER_NOT_OBSERVED", true);
1504
1459
  }
1505
1460
  else {
1506
- if (row.alias !== selectedAlias) {
1461
+ if (!discovery.truncated && row.alias !== selectedAlias) {
1507
1462
  // A Claude rename changes only the live name index. The native
1508
- // session UUID remains the selected logical route.
1463
+ // session UUID remains the selected logical route. An incomplete
1464
+ // scan is display evidence only and cannot rename dispatch authority.
1509
1465
  this.selected.set(routeHandle, row.alias);
1510
1466
  }
1511
1467
  if (row.state === "idle" &&
@@ -1585,13 +1541,13 @@ function validateCodexFactory(factory) {
1585
1541
  const schema = factory.schemaCompatibility;
1586
1542
  const write = factory.writeCompatibility;
1587
1543
  const certified = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(factory.appServerVersion);
1588
- const observedCandidate = factory.compatibilityPolicy === "observed" &&
1589
- schema.observedSchemaCandidate === true &&
1544
+ const schemaCandidate = schema.observedSchemaCandidate === true &&
1590
1545
  CODEX_APP_SERVER_WRITABLE_VERSIONS.some((version) => sharesCompatibilityMajor(version, factory.appServerVersion));
1591
1546
  if (factory.hostId !== LOCAL_HOST ||
1592
1547
  factory.protocol !== "codex-app-server" ||
1593
1548
  factory.protocolVersion !== factory.appServerVersion ||
1594
- (!certified && !observedCandidate) ||
1549
+ (!certified &&
1550
+ (!schemaCandidate || write !== null || factory.writableReady)) ||
1595
1551
  schema.appServerVersion !== factory.appServerVersion ||
1596
1552
  schema.endpointGeneration !== factory.endpointGeneration ||
1597
1553
  schema.protocol !== "app-server-v2-stable" ||
@@ -1638,10 +1594,8 @@ function codexRouteSafeCode(observation) {
1638
1594
  return undefined;
1639
1595
  }
1640
1596
  export class LocalCodexGatewayProvider {
1641
- identity;
1642
- protocol;
1643
- protocolVersion;
1644
1597
  factory;
1598
+ refreshFactory;
1645
1599
  maxCallbacks;
1646
1600
  maxRoutes;
1647
1601
  maxReplyBytes;
@@ -1651,32 +1605,37 @@ export class LocalCodexGatewayProvider {
1651
1605
  recoveryMaxMs;
1652
1606
  now;
1653
1607
  routes = new Map();
1608
+ /** Exact public alias authority retained across internal route recovery. */
1609
+ routeAliases = new Map();
1654
1610
  routeCreations = new Map();
1655
1611
  routeReleases = new Map();
1656
1612
  routeRecoveryAttempts = new Map();
1657
1613
  routeRecoveryTimers = new Map();
1658
1614
  routeRecoveries = new Map();
1659
1615
  routesRequiringRecovery = new Set();
1616
+ trackedRoutes = new Set();
1617
+ retiredEndpointGeneration;
1660
1618
  expectsReply = new Map();
1661
- compatibilityCertifyingRoutes = new Set();
1662
- compatibilityTurns = new Map();
1663
1619
  callbackQueue = [];
1664
1620
  callbackScheduled = false;
1665
1621
  callbacks;
1622
+ endpointRefresh;
1623
+ endpointRefreshDelivery;
1624
+ endpointActivationRetry;
1625
+ endpointActivationRetryTimer;
1626
+ callbackDrainFrozen = false;
1666
1627
  compatibilityAttested;
1628
+ endpointUnavailable = false;
1629
+ pendingEndpointAttestation;
1630
+ pendingEndpointRefreshEvent;
1667
1631
  initialized = false;
1668
1632
  closing = false;
1669
1633
  closed = false;
1670
1634
  constructor(options) {
1671
1635
  validateCodexFactory(options.factory);
1672
1636
  this.factory = options.factory;
1673
- this.identity = {
1674
- provider: "codex",
1675
- hostId: exactLocalHost(options.factory.hostId),
1676
- endpointGeneration: options.factory.endpointGeneration,
1677
- };
1678
- this.protocol = options.factory.protocol;
1679
- this.protocolVersion = options.factory.protocolVersion;
1637
+ this.refreshFactory = options.refreshFactory;
1638
+ exactLocalHost(options.factory.hostId);
1680
1639
  this.compatibilityAttested = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(options.factory.appServerVersion);
1681
1640
  this.maxCallbacks = positiveBounded(options.maxCallbackEvents, MAX_CODEX_CALLBACKS, 4_096);
1682
1641
  this.maxRoutes = positiveBounded(options.maxRoutes, MAX_CODEX_ROUTES, 256);
@@ -1693,30 +1652,61 @@ export class LocalCodexGatewayProvider {
1693
1652
  }
1694
1653
  this.now = options.now ?? (() => new Date());
1695
1654
  }
1655
+ get identity() {
1656
+ return {
1657
+ provider: "codex",
1658
+ hostId: exactLocalHost(this.factory.hostId),
1659
+ endpointGeneration: this.factory.endpointGeneration,
1660
+ };
1661
+ }
1662
+ get protocol() {
1663
+ return this.factory.protocol;
1664
+ }
1665
+ get protocolVersion() {
1666
+ return this.factory.protocolVersion;
1667
+ }
1696
1668
  compatibilitySurface() {
1697
1669
  return { surface: "codex", version: this.factory.appServerVersion };
1698
1670
  }
1699
1671
  acceptCompatibilityAttestation(attestation) {
1672
+ const pending = this.pendingEndpointAttestation;
1700
1673
  if (attestation.surface !== "codex" ||
1701
1674
  attestation.version !== this.factory.appServerVersion ||
1702
- attestation.tier === "incompatible") {
1675
+ attestation.tier !== "certified" ||
1676
+ !CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(attestation.version) ||
1677
+ (this.endpointUnavailable && pending === undefined) ||
1678
+ (pending !== undefined &&
1679
+ (attestation.checkedAt !== pending.checkedAt ||
1680
+ attestation.tier !== pending.tier ||
1681
+ attestation.safeErrorCode !== pending.safeErrorCode ||
1682
+ attestation.probes.length !== pending.probes.length ||
1683
+ attestation.probes.some((probe, index) => probe.name !== pending.probes[index]?.name ||
1684
+ probe.outcome !== pending.probes[index]?.outcome ||
1685
+ probe.safeErrorCode !== pending.probes[index]?.safeErrorCode)))) {
1703
1686
  throw new BridgeError("CODEX_COMPATIBILITY_ATTESTATION_MISMATCH", "The Codex compatibility attestation does not match this endpoint.");
1704
1687
  }
1688
+ this.pendingEndpointAttestation = undefined;
1689
+ this.pendingEndpointRefreshEvent = undefined;
1690
+ this.clearEndpointActivationRetry();
1705
1691
  this.compatibilityAttested = true;
1692
+ this.endpointUnavailable = false;
1706
1693
  }
1707
1694
  async runCompatibilityProbes() {
1695
+ return await this.runCompatibilityProbesFor(this.factory);
1696
+ }
1697
+ async runCompatibilityProbesFor(factory) {
1708
1698
  const [installation, control, initialize, threadList] = compatibilityProbeNames.codex;
1709
1699
  let transport;
1710
1700
  let connector;
1711
1701
  let stage = "transport";
1712
1702
  try {
1713
- transport = await this.factory.connectTransport();
1703
+ transport = await factory.connectTransport();
1714
1704
  stage = "initialize";
1715
1705
  connector = await CodexAppServerConnector.connect({
1716
- compatibility: this.factory.schemaCompatibility,
1706
+ compatibility: factory.schemaCompatibility,
1717
1707
  writesEnabled: false,
1718
1708
  route: {
1719
- endpointGeneration: this.identity.endpointGeneration,
1709
+ endpointGeneration: factory.endpointGeneration,
1720
1710
  threadId: COMPATIBILITY_PROBE_THREAD_ID,
1721
1711
  },
1722
1712
  transport,
@@ -1727,7 +1717,6 @@ export class LocalCodexGatewayProvider {
1727
1717
  });
1728
1718
  stage = "thread_list";
1729
1719
  await connector.observeLoadedThread(connector.guard());
1730
- this.compatibilityAttested = true;
1731
1720
  return [
1732
1721
  passedProbe(installation),
1733
1722
  passedProbe(control),
@@ -1736,6 +1725,8 @@ export class LocalCodexGatewayProvider {
1736
1725
  ];
1737
1726
  }
1738
1727
  catch (error) {
1728
+ if (this.isEndpointGenerationChanged(error))
1729
+ throw error;
1739
1730
  if (stage === "transport") {
1740
1731
  const controlFailure = error instanceof LocalCodexTransportError &&
1741
1732
  [
@@ -1772,107 +1763,6 @@ export class LocalCodexGatewayProvider {
1772
1763
  }
1773
1764
  }
1774
1765
  }
1775
- async runCompatibilityCertification(input) {
1776
- void input.controllerStateRoot;
1777
- const certifiedAt = this.now().toISOString();
1778
- const depth = input.withTurn ? "turn" : "thread_ops";
1779
- const routeHandle = input.routeHandle;
1780
- if (routeHandle === undefined || this.compatibilityCertifyingRoutes.has(routeHandle)) {
1781
- return {
1782
- depth,
1783
- outcome: "fail",
1784
- certifiedAt,
1785
- safeErrorCode: "CODEX_CERTIFICATION_ROUTE_REQUIRED",
1786
- };
1787
- }
1788
- this.assertReady();
1789
- const route = this.routes.get(routeHandle);
1790
- if (route === undefined || !this.observeRouteSuccessionBarrier(routeHandle).clean) {
1791
- return {
1792
- depth,
1793
- outcome: "fail",
1794
- certifiedAt,
1795
- safeErrorCode: "CODEX_CERTIFICATION_ROUTE_BUSY",
1796
- };
1797
- }
1798
- this.compatibilityCertifyingRoutes.add(routeHandle);
1799
- try {
1800
- const loaded = await route.connector.observeLoadedThread(route.connector.guard());
1801
- if (!loaded.selectedThreadLoaded) {
1802
- return {
1803
- depth,
1804
- outcome: "fail",
1805
- certifiedAt: this.now().toISOString(),
1806
- safeErrorCode: "CODEX_CERTIFICATION_THREAD_UNAVAILABLE",
1807
- };
1808
- }
1809
- await route.connector.certifyThreadOperations(route.connector.guard());
1810
- if (!input.withTurn) {
1811
- return {
1812
- depth,
1813
- outcome: "pass",
1814
- certifiedAt: this.now().toISOString(),
1815
- };
1816
- }
1817
- const messageId = `compat_${randomBytes(12).toString("hex")}`;
1818
- const result = new Promise((resolve) => {
1819
- const timer = setTimeout(() => {
1820
- this.compatibilityTurns.delete(messageId);
1821
- resolve({
1822
- messageId,
1823
- outcome: "expired",
1824
- replyCode: "REPLY_UNAVAILABLE",
1825
- text: null,
1826
- });
1827
- }, COMPATIBILITY_TURN_TIMEOUT_MS);
1828
- timer.unref();
1829
- this.compatibilityTurns.set(messageId, { resolve, timer });
1830
- });
1831
- try {
1832
- const disposition = await route.connector.submitMessage(route.connector.guard(), {
1833
- deadlineAt: new Date(this.now().getTime() + COMPATIBILITY_TURN_TIMEOUT_MS).toISOString(),
1834
- messageId,
1835
- text: "reply OK",
1836
- });
1837
- if (disposition.disposition !== "started") {
1838
- throw new BridgeError("CODEX_CERTIFICATION_TURN_NOT_STARTED", "The bounded certification turn did not start on the exact idle route.");
1839
- }
1840
- const settled = await result;
1841
- if (settled.outcome !== "completed" ||
1842
- settled.text?.trim() !== "OK") {
1843
- return {
1844
- depth,
1845
- outcome: "fail",
1846
- certifiedAt: this.now().toISOString(),
1847
- safeErrorCode: "CODEX_CERTIFICATION_TURN_FAILED",
1848
- };
1849
- }
1850
- return {
1851
- depth,
1852
- outcome: "pass",
1853
- certifiedAt: this.now().toISOString(),
1854
- };
1855
- }
1856
- finally {
1857
- const pending = this.compatibilityTurns.get(messageId);
1858
- if (pending !== undefined) {
1859
- clearTimeout(pending.timer);
1860
- this.compatibilityTurns.delete(messageId);
1861
- }
1862
- }
1863
- }
1864
- catch {
1865
- return {
1866
- depth,
1867
- outcome: "fail",
1868
- certifiedAt: this.now().toISOString(),
1869
- safeErrorCode: "CODEX_CERTIFICATION_FAILED",
1870
- };
1871
- }
1872
- finally {
1873
- this.compatibilityCertifyingRoutes.delete(routeHandle);
1874
- }
1875
- }
1876
1766
  async initialize(callbacks) {
1877
1767
  if (this.closed || this.initialized) {
1878
1768
  throw new BridgeError("CODEX_PROVIDER_INITIALIZATION_REJECTED", "The local Codex provider cannot be initialized in this state.");
@@ -1888,7 +1778,9 @@ export class LocalCodexGatewayProvider {
1888
1778
  : { health: "healthy", compatibility: "compatible" };
1889
1779
  }
1890
1780
  async selectRoute(input) {
1891
- this.assertReady();
1781
+ if (!this.initialized || this.closing || this.closed) {
1782
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
1783
+ }
1892
1784
  if (!PUBLIC_ALIAS.test(input.alias) ||
1893
1785
  !input.alias.endsWith(`@${this.identity.hostId}`)) {
1894
1786
  throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex alias is outside the exact local provider boundary.");
@@ -1896,25 +1788,116 @@ export class LocalCodexGatewayProvider {
1896
1788
  if (!OPAQUE_ROUTE.test(input.routeHandle)) {
1897
1789
  throw new BridgeError("CODEX_ROUTE_INVALID", "The Codex task identifier is outside the exact provider boundary.");
1898
1790
  }
1791
+ const retainedAlias = this.routeAliases.get(input.routeHandle);
1792
+ if (retainedAlias !== undefined && retainedAlias !== input.alias) {
1793
+ throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex route is already bound to another exact public alias.");
1794
+ }
1899
1795
  this.cancelRouteRecovery(input.routeHandle);
1900
- const route = await this.ensureRoute(input.routeHandle);
1796
+ let refreshResult;
1797
+ let selectorRefreshReserved = false;
1798
+ let route;
1799
+ const stageRefreshedRoute = async (result) => {
1800
+ if (result === undefined ||
1801
+ result.event.outcome !== "compatible") {
1802
+ throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex endpoint replacement was not compatibility-attested.");
1803
+ }
1804
+ refreshResult = result;
1805
+ selectorRefreshReserved = this.reserveSelectorRefresh(result);
1806
+ if (!selectorRefreshReserved) {
1807
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
1808
+ }
1809
+ try {
1810
+ return await this.ensureRoute(input.routeHandle, false, true);
1811
+ }
1812
+ catch (selectionError) {
1813
+ this.emitSelectorRefreshFallback(result, input.routeHandle);
1814
+ throw selectionError;
1815
+ }
1816
+ };
1817
+ if (this.endpointRefresh !== undefined) {
1818
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1819
+ }
1820
+ else if (this.endpointUnavailable &&
1821
+ this.pendingEndpointAttestation === undefined) {
1822
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1823
+ }
1824
+ else {
1825
+ this.assertReady();
1826
+ try {
1827
+ route = await this.ensureRoute(input.routeHandle);
1828
+ }
1829
+ catch (error) {
1830
+ const refreshPending = error instanceof BridgeError &&
1831
+ error.code === "CODEX_ENDPOINT_REFRESH_PENDING";
1832
+ if (!this.isEndpointGenerationChanged(error) && !refreshPending) {
1833
+ throw error;
1834
+ }
1835
+ if (this.isEndpointGenerationChanged(error)) {
1836
+ this.endpointUnavailable = true;
1837
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1838
+ }
1839
+ else if (this.endpointRefresh !== undefined) {
1840
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1841
+ }
1842
+ else {
1843
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
1844
+ }
1845
+ }
1846
+ if (refreshResult === undefined &&
1847
+ (route.endpointGeneration !== this.factory.endpointGeneration ||
1848
+ this.endpointUnavailable ||
1849
+ this.endpointRefresh !== undefined)) {
1850
+ if (this.endpointRefresh === undefined) {
1851
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
1852
+ }
1853
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1854
+ }
1855
+ }
1901
1856
  const observation = route.connector.observation();
1902
1857
  const state = codexRouteState(observation);
1903
1858
  if (state === "stale") {
1904
- await this.releaseRoute(input.routeHandle);
1859
+ if (refreshResult !== undefined && selectorRefreshReserved) {
1860
+ // Publish the transition before any asynchronous connector cleanup so
1861
+ // the controller can accept or durably reject the exact replacement
1862
+ // generation. A route which closed after refresh staging is not valid
1863
+ // re-anchoring evidence and must be removed from both the callback and
1864
+ // its retained retry authority.
1865
+ this.emitSelectorRefreshFallback(refreshResult, input.routeHandle);
1866
+ }
1867
+ this.routesRequiringRecovery.add(input.routeHandle);
1868
+ await this.releaseRouteInternal(input.routeHandle);
1869
+ if (this.trackedRoutes.has(input.routeHandle)) {
1870
+ this.scheduleRouteRecovery(input.routeHandle);
1871
+ }
1905
1872
  throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task connection closed during route selection.");
1906
1873
  }
1907
- this.queueRouteObservation(route, observation);
1874
+ this.trackedRoutes.add(input.routeHandle);
1875
+ this.routeAliases.set(input.routeHandle, input.alias);
1876
+ if (refreshResult === undefined) {
1877
+ this.queueRouteObservation(route, observation);
1878
+ }
1879
+ const endpointRefresh = refreshResult === undefined || !selectorRefreshReserved
1880
+ ? undefined
1881
+ : refreshResult.event;
1882
+ if (endpointRefresh !== undefined) {
1883
+ this.armEndpointActivationRetry(endpointRefresh);
1884
+ }
1908
1885
  return {
1909
1886
  routeHandle: input.routeHandle,
1910
1887
  state,
1888
+ ...(endpointRefresh === undefined ? {} : { endpointRefresh }),
1911
1889
  };
1912
1890
  }
1913
1891
  observeRouteSuccessionBarrier(routeHandle) {
1914
- this.assertReady();
1892
+ if (!this.initialized || this.closing || this.closed) {
1893
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
1894
+ }
1915
1895
  const route = this.routes.get(routeHandle);
1916
1896
  const observation = route?.connector.observation();
1917
- const routeCreationInFlight = this.routeCreations.has(routeHandle);
1897
+ const routeCreationInFlight = this.routeCreations.has(routeHandle) ||
1898
+ this.routeRecoveries.has(routeHandle) ||
1899
+ (this.endpointRefresh !== undefined && this.trackedRoutes.has(routeHandle)) ||
1900
+ this.pendingEndpointActivationClaims(routeHandle);
1918
1901
  const routeReleaseInFlight = this.routeReleases.has(routeHandle);
1919
1902
  const pendingReplyCorrelations = this.expectsReply.size;
1920
1903
  const pendingCallbacks = Math.max(this.callbackQueue.length, this.callbackScheduled ? 1 : 0);
@@ -1951,6 +1934,8 @@ export class LocalCodexGatewayProvider {
1951
1934
  if (!this.initialized ||
1952
1935
  this.closing ||
1953
1936
  this.closed ||
1937
+ this.endpointUnavailable ||
1938
+ this.endpointRefresh !== undefined ||
1954
1939
  !this.compatibilityAttested ||
1955
1940
  input.authorization !== "selected_route" ||
1956
1941
  !sameEndpoint(input.binding, this.identity)) {
@@ -1958,12 +1943,12 @@ export class LocalCodexGatewayProvider {
1958
1943
  }
1959
1944
  const route = this.routes.get(input.binding.routeHandle);
1960
1945
  if (route === undefined ||
1961
- this.routeReleases.has(input.binding.routeHandle)) {
1946
+ this.routeAliases.get(input.binding.routeHandle) !== input.targetAlias ||
1947
+ this.routeReleases.has(input.binding.routeHandle) ||
1948
+ route.endpointGeneration !== input.binding.endpointGeneration ||
1949
+ route.endpointGeneration !== this.factory.endpointGeneration) {
1962
1950
  return { state: "failed", safeErrorCode: "CODEX_ROUTE_UNAVAILABLE" };
1963
1951
  }
1964
- if (this.compatibilityCertifyingRoutes.has(input.binding.routeHandle)) {
1965
- return { state: "deferred", safeErrorCode: "CODEX_ROUTE_HELD" };
1966
- }
1967
1952
  if (!this.factory.writableReady ||
1968
1953
  this.factory.writeCompatibility === null) {
1969
1954
  return { state: "failed", safeErrorCode: "CODEX_WRITES_DISABLED" };
@@ -1978,13 +1963,31 @@ export class LocalCodexGatewayProvider {
1978
1963
  if (this.expectsReply.has(input.messageId)) {
1979
1964
  return { state: "failed", safeErrorCode: "CODEX_MESSAGE_DUPLICATE" };
1980
1965
  }
1966
+ let content;
1967
+ try {
1968
+ content = composeProvenanceEnvelope({
1969
+ direction: "codex",
1970
+ sourceAlias: input.sourceAlias,
1971
+ targetAlias: input.targetAlias,
1972
+ conversationId: input.conversationId,
1973
+ body: input.text,
1974
+ });
1975
+ }
1976
+ catch (error) {
1977
+ const safeErrorCode = error instanceof BridgeError &&
1978
+ (error.code === "PROVENANCE_ENVELOPE_INVALID" ||
1979
+ error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")
1980
+ ? error.code
1981
+ : "PROVENANCE_ENVELOPE_INVALID";
1982
+ return { state: "failed", safeErrorCode };
1983
+ }
1981
1984
  this.expectsReply.set(input.messageId, input.expectsReply);
1982
1985
  try {
1983
1986
  const disposition = await route.connector.submitMessage(guard, {
1984
1987
  deadlineAt: input.deadlineAt,
1985
1988
  messageId: input.messageId,
1986
1989
  ...(input.steer === true ? { steer: true } : {}),
1987
- text: input.text,
1990
+ text: content,
1988
1991
  });
1989
1992
  if (disposition.disposition === "deferred") {
1990
1993
  this.expectsReply.delete(input.messageId);
@@ -2019,11 +2022,22 @@ export class LocalCodexGatewayProvider {
2019
2022
  }
2020
2023
  }
2021
2024
  async releaseRoute(routeHandle) {
2025
+ if (this.pendingEndpointActivationClaims(routeHandle)) {
2026
+ throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route still belongs to an unaccepted endpoint transition.", true);
2027
+ }
2028
+ this.trackedRoutes.delete(routeHandle);
2022
2029
  this.cancelRouteRecovery(routeHandle);
2023
2030
  const recovery = this.routeRecoveries.get(routeHandle);
2024
2031
  if (recovery !== undefined)
2025
2032
  await recovery;
2033
+ const endpointRefresh = this.endpointRefresh;
2034
+ if (endpointRefresh !== undefined)
2035
+ await endpointRefresh;
2036
+ if (this.pendingEndpointActivationClaims(routeHandle)) {
2037
+ throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route acquired endpoint-transition authority during release.", true);
2038
+ }
2026
2039
  await this.releaseRouteInternal(routeHandle);
2040
+ this.routeAliases.delete(routeHandle);
2027
2041
  }
2028
2042
  async releaseRouteInternal(routeHandle) {
2029
2043
  const pending = this.routeReleases.get(routeHandle);
@@ -2053,6 +2067,12 @@ export class LocalCodexGatewayProvider {
2053
2067
  clearTimeout(timer);
2054
2068
  this.routeRecoveryTimers.clear();
2055
2069
  this.routesRequiringRecovery.clear();
2070
+ this.clearEndpointActivationRetry();
2071
+ this.pendingEndpointRefreshEvent = undefined;
2072
+ this.pendingEndpointAttestation = undefined;
2073
+ const refreshResult = this.endpointRefresh === undefined
2074
+ ? []
2075
+ : await Promise.allSettled([this.endpointRefresh]);
2056
2076
  const entries = [...this.routes.entries()];
2057
2077
  const routeResults = await Promise.allSettled(entries.map(async ([routeHandle]) => this.releaseRoute(routeHandle)));
2058
2078
  await Promise.allSettled([
@@ -2060,7 +2080,8 @@ export class LocalCodexGatewayProvider {
2060
2080
  ...this.routeCreations.values(),
2061
2081
  ]);
2062
2082
  this.drainCallbackQueue();
2063
- if (routeResults.some((result) => result.status === "rejected")) {
2083
+ if (refreshResult.some((result) => result.status === "rejected") ||
2084
+ routeResults.some((result) => result.status === "rejected")) {
2064
2085
  this.closing = false;
2065
2086
  throw new BridgeError("CODEX_PROVIDER_CLEANUP_FAILED", "An exact owned Codex turn did not confirm termination.");
2066
2087
  }
@@ -2074,17 +2095,10 @@ export class LocalCodexGatewayProvider {
2074
2095
  this.closed = true;
2075
2096
  this.closing = false;
2076
2097
  this.callbackQueue.length = 0;
2077
- for (const [messageId, pending] of this.compatibilityTurns) {
2078
- clearTimeout(pending.timer);
2079
- pending.resolve({
2080
- messageId,
2081
- outcome: "abandoned",
2082
- replyCode: "REPLY_UNAVAILABLE",
2083
- text: null,
2084
- });
2085
- }
2086
- this.compatibilityTurns.clear();
2087
- this.compatibilityCertifyingRoutes.clear();
2098
+ this.pendingEndpointAttestation = undefined;
2099
+ this.pendingEndpointRefreshEvent = undefined;
2100
+ this.trackedRoutes.clear();
2101
+ this.routeAliases.clear();
2088
2102
  this.expectsReply.clear();
2089
2103
  this.callbacks = undefined;
2090
2104
  }
@@ -2092,16 +2106,25 @@ export class LocalCodexGatewayProvider {
2092
2106
  if (!this.initialized ||
2093
2107
  this.closing ||
2094
2108
  this.closed ||
2109
+ this.endpointUnavailable ||
2110
+ this.endpointRefresh !== undefined ||
2095
2111
  !this.compatibilityAttested) {
2096
2112
  throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2097
2113
  }
2098
2114
  }
2099
- async ensureRoute(threadId) {
2100
- this.assertReady();
2115
+ async ensureRoute(threadId, queueInitialObservation = true, allowPendingEndpointAttestation = false) {
2116
+ if (allowPendingEndpointAttestation) {
2117
+ if (!this.initialized || this.closing || this.closed) {
2118
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2119
+ }
2120
+ }
2121
+ else {
2122
+ this.assertReady();
2123
+ }
2101
2124
  const pendingRelease = this.routeReleases.get(threadId);
2102
2125
  if (pendingRelease !== undefined) {
2103
2126
  await pendingRelease;
2104
- return await this.ensureRoute(threadId);
2127
+ return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
2105
2128
  }
2106
2129
  const existing = this.routes.get(threadId);
2107
2130
  if (existing !== undefined) {
@@ -2113,7 +2136,7 @@ export class LocalCodexGatewayProvider {
2113
2136
  // a fresh one. The identity-checked release lets automatic recovery and
2114
2137
  // an explicit selector join the same cleanup safely.
2115
2138
  await this.releaseRouteInternal(threadId);
2116
- return await this.ensureRoute(threadId);
2139
+ return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
2117
2140
  }
2118
2141
  const pending = this.routeCreations.get(threadId);
2119
2142
  if (pending !== undefined)
@@ -2121,42 +2144,51 @@ export class LocalCodexGatewayProvider {
2121
2144
  if (this.routes.size + this.routeCreations.size >= this.maxRoutes) {
2122
2145
  throw new BridgeError("CODEX_ROUTE_CAPACITY", "The bounded local Codex route table is full.", true);
2123
2146
  }
2124
- const creation = this.createRoute(threadId);
2125
- this.routeCreations.set(threadId, creation);
2126
- try {
2127
- const route = await creation;
2147
+ const admittedFactory = this.factory;
2148
+ const creation = (async () => {
2149
+ const route = await this.createRoute(threadId, admittedFactory, queueInitialObservation);
2128
2150
  if (this.closing || this.closed) {
2129
2151
  await this.closeRoute(route);
2130
2152
  throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider closed during route creation.");
2131
2153
  }
2154
+ if (!allowPendingEndpointAttestation &&
2155
+ (this.endpointUnavailable ||
2156
+ this.endpointRefresh !== undefined ||
2157
+ admittedFactory !== this.factory ||
2158
+ route.endpointGeneration !== this.factory.endpointGeneration)) {
2159
+ await this.closeRoute(route);
2160
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The Codex endpoint moved during exact route creation.", true);
2161
+ }
2132
2162
  this.routes.set(threadId, route);
2133
2163
  return route;
2164
+ })();
2165
+ this.routeCreations.set(threadId, creation);
2166
+ try {
2167
+ return await creation;
2134
2168
  }
2135
2169
  finally {
2136
2170
  this.routeCreations.delete(threadId);
2137
2171
  }
2138
2172
  }
2139
- async createRoute(threadId) {
2173
+ async createRoute(threadId, factory, queueInitialObservation) {
2140
2174
  let transport;
2141
2175
  let connector;
2142
2176
  const generation = Symbol(threadId);
2143
2177
  try {
2144
- transport = await this.factory.connectTransport();
2145
- const writesEnabled = this.factory.writableReady &&
2146
- this.factory.writeCompatibility !== null;
2178
+ transport = await factory.connectTransport();
2179
+ const writesEnabled = factory.writableReady && factory.writeCompatibility !== null;
2147
2180
  connector = await CodexAppServerConnector.connect({
2148
- compatibility: this.factory.writeCompatibility ??
2149
- this.factory.schemaCompatibility,
2181
+ compatibility: factory.writeCompatibility ?? factory.schemaCompatibility,
2150
2182
  writesEnabled,
2151
2183
  route: {
2152
- endpointGeneration: this.identity.endpointGeneration,
2184
+ endpointGeneration: factory.endpointGeneration,
2153
2185
  threadId,
2154
2186
  },
2155
2187
  transport,
2156
2188
  maxReplyBytes: this.maxReplyBytes,
2157
2189
  now: this.now,
2158
2190
  onEvent: (event) => this.onConnectorEvent(threadId, generation, event),
2159
- onTurnResult: (result) => this.onTurnResult(result),
2191
+ onTurnResult: (result) => this.onTurnResult(factory.endpointGeneration, result),
2160
2192
  });
2161
2193
  const loaded = await connector.observeLoadedThread(connector.guard());
2162
2194
  if (!loaded.selectedThreadLoaded) {
@@ -2165,11 +2197,14 @@ export class LocalCodexGatewayProvider {
2165
2197
  await connector.resumeThread(connector.guard());
2166
2198
  const route = {
2167
2199
  connector,
2200
+ endpointGeneration: factory.endpointGeneration,
2168
2201
  generation,
2169
2202
  threadId,
2170
2203
  transport,
2171
2204
  };
2172
- this.queueRouteObservation(route, connector.observation());
2205
+ if (queueInitialObservation) {
2206
+ this.queueRouteObservation(route, connector.observation());
2207
+ }
2173
2208
  return route;
2174
2209
  }
2175
2210
  catch (error) {
@@ -2179,6 +2214,8 @@ export class LocalCodexGatewayProvider {
2179
2214
  else if (transport !== undefined) {
2180
2215
  await transport.close().catch(() => undefined);
2181
2216
  }
2217
+ if (this.isEndpointGenerationChanged(error))
2218
+ throw error;
2182
2219
  if (error instanceof BridgeError)
2183
2220
  throw error;
2184
2221
  if (error instanceof CodexConnectorError) {
@@ -2236,6 +2273,19 @@ export class LocalCodexGatewayProvider {
2236
2273
  return;
2237
2274
  }
2238
2275
  try {
2276
+ if (this.endpointUnavailable) {
2277
+ if (this.pendingEndpointAttestation !== undefined) {
2278
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is awaiting controller activation.", true);
2279
+ }
2280
+ const refreshed = await this.refreshEndpoint("callback");
2281
+ if (refreshed?.event.outcome === "compatible" &&
2282
+ refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
2283
+ this.routesRequiringRecovery.delete(threadId);
2284
+ this.routeRecoveryAttempts.delete(threadId);
2285
+ return;
2286
+ }
2287
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is not ready.", true);
2288
+ }
2239
2289
  const current = this.routes.get(threadId);
2240
2290
  if (current !== undefined &&
2241
2291
  codexRouteState(current.connector.observation()) === "stale") {
@@ -2255,7 +2305,17 @@ export class LocalCodexGatewayProvider {
2255
2305
  this.routeRecoveryAttempts.delete(threadId);
2256
2306
  this.queueRouteObservation(replacement, replacement.connector.observation());
2257
2307
  }
2258
- catch {
2308
+ catch (error) {
2309
+ if (this.isEndpointGenerationChanged(error)) {
2310
+ this.endpointUnavailable = true;
2311
+ const refreshed = await this.refreshEndpoint("callback").catch(() => undefined);
2312
+ if (refreshed?.event.outcome === "compatible" &&
2313
+ refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
2314
+ this.routesRequiringRecovery.delete(threadId);
2315
+ this.routeRecoveryAttempts.delete(threadId);
2316
+ return;
2317
+ }
2318
+ }
2259
2319
  this.routeRecoveryAttempts.set(threadId, (this.routeRecoveryAttempts.get(threadId) ?? 0) + 1);
2260
2320
  }
2261
2321
  }
@@ -2267,14 +2327,321 @@ export class LocalCodexGatewayProvider {
2267
2327
  clearTimeout(timer);
2268
2328
  this.routeRecoveryTimers.delete(threadId);
2269
2329
  }
2270
- onTurnResult(result) {
2271
- const certification = this.compatibilityTurns.get(result.messageId);
2272
- if (certification !== undefined) {
2273
- clearTimeout(certification.timer);
2274
- this.compatibilityTurns.delete(result.messageId);
2275
- certification.resolve(result);
2330
+ isEndpointGenerationChanged(error) {
2331
+ return (error instanceof LocalCodexTransportError &&
2332
+ error.code === "ENDPOINT_GENERATION_CHANGED");
2333
+ }
2334
+ pendingEndpointActivationClaims(routeHandle) {
2335
+ const event = this.pendingEndpointRefreshEvent;
2336
+ return (event !== undefined &&
2337
+ event.current.endpointGeneration === this.factory.endpointGeneration &&
2338
+ event.routes.some((route) => route.routeHandle === routeHandle));
2339
+ }
2340
+ async refreshEndpoint(delivery) {
2341
+ if (this.refreshFactory === undefined || this.closing || this.closed) {
2342
+ return undefined;
2343
+ }
2344
+ const existing = this.endpointRefresh;
2345
+ if (existing !== undefined) {
2346
+ // A selector already running inside the controller lock takes ownership
2347
+ // of the transition result. This avoids waiting on a callback that needs
2348
+ // that same lock while still ensuring exactly one activation path.
2349
+ if (delivery === "selector")
2350
+ this.endpointRefreshDelivery = "selector";
2351
+ return await existing;
2352
+ }
2353
+ this.endpointRefreshDelivery = delivery;
2354
+ const refresh = this.performEndpointRefresh();
2355
+ this.endpointRefresh = refresh;
2356
+ try {
2357
+ return await refresh;
2358
+ }
2359
+ finally {
2360
+ if (this.endpointRefresh === refresh) {
2361
+ this.endpointRefresh = undefined;
2362
+ this.endpointRefreshDelivery = undefined;
2363
+ }
2364
+ }
2365
+ }
2366
+ async performEndpointRefresh() {
2367
+ const refreshFactory = this.refreshFactory;
2368
+ if (refreshFactory === undefined)
2369
+ return undefined;
2370
+ const previousFactory = this.factory;
2371
+ const previous = this.identity;
2372
+ let candidate;
2373
+ try {
2374
+ const resolved = await this.resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous);
2375
+ candidate = resolved.factory;
2376
+ const { attestation, identity: candidateIdentity } = resolved;
2377
+ await this.retireEndpointGeneration(previousFactory, previous.endpointGeneration);
2378
+ if (attestation.tier === "incompatible") {
2379
+ await candidate.close();
2380
+ candidate = undefined;
2381
+ this.compatibilityAttested = false;
2382
+ this.pendingEndpointAttestation = undefined;
2383
+ this.pendingEndpointRefreshEvent = undefined;
2384
+ for (const routeHandle of this.trackedRoutes) {
2385
+ this.routesRequiringRecovery.add(routeHandle);
2386
+ }
2387
+ if (this.closing || this.closed)
2388
+ return undefined;
2389
+ const result = {
2390
+ event: {
2391
+ outcome: "incompatible",
2392
+ previous,
2393
+ candidate: candidateIdentity,
2394
+ attestation,
2395
+ },
2396
+ delivery: "callback",
2397
+ selectorClaimed: true,
2398
+ };
2399
+ this.emitEndpointRefresh(result.event);
2400
+ for (const routeHandle of this.routesRequiringRecovery) {
2401
+ if (this.trackedRoutes.has(routeHandle)) {
2402
+ this.scheduleRouteRecovery(routeHandle);
2403
+ }
2404
+ }
2405
+ return result;
2406
+ }
2407
+ if (this.closing || this.closed) {
2408
+ await candidate.close();
2409
+ candidate = undefined;
2410
+ return undefined;
2411
+ }
2412
+ this.factory = candidate;
2413
+ candidate = undefined;
2414
+ this.retiredEndpointGeneration = undefined;
2415
+ this.compatibilityAttested = false;
2416
+ this.endpointUnavailable = true;
2417
+ this.pendingEndpointAttestation = attestation;
2418
+ const refreshedRoutes = [];
2419
+ for (const routeHandle of [...this.trackedRoutes]) {
2420
+ if (this.closing || this.closed)
2421
+ break;
2422
+ try {
2423
+ const route = await this.createRoute(routeHandle, this.factory, false);
2424
+ if (this.closing || this.closed) {
2425
+ await this.closeRoute(route);
2426
+ break;
2427
+ }
2428
+ const state = codexRouteState(route.connector.observation());
2429
+ if (state === "stale") {
2430
+ await this.closeRoute(route);
2431
+ throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task was stale on the replacement endpoint.");
2432
+ }
2433
+ this.routes.set(routeHandle, route);
2434
+ this.routesRequiringRecovery.delete(routeHandle);
2435
+ this.routeRecoveryAttempts.delete(routeHandle);
2436
+ refreshedRoutes.push({ routeHandle, state });
2437
+ }
2438
+ catch {
2439
+ this.routesRequiringRecovery.add(routeHandle);
2440
+ }
2441
+ }
2442
+ if (this.closing || this.closed)
2443
+ return undefined;
2444
+ const result = {
2445
+ event: {
2446
+ outcome: "compatible",
2447
+ previous,
2448
+ current: this.identity,
2449
+ attestation,
2450
+ routes: refreshedRoutes,
2451
+ },
2452
+ delivery: this.endpointRefreshDelivery ?? "callback",
2453
+ selectorClaimed: false,
2454
+ };
2455
+ if (result.event.outcome === "compatible") {
2456
+ this.pendingEndpointRefreshEvent = result.event;
2457
+ }
2458
+ if (result.delivery === "callback") {
2459
+ result.selectorClaimed = true;
2460
+ this.publishEndpointRefresh(result.event);
2461
+ }
2462
+ for (const routeHandle of this.routesRequiringRecovery) {
2463
+ if (this.trackedRoutes.has(routeHandle)) {
2464
+ this.scheduleRouteRecovery(routeHandle);
2465
+ }
2466
+ }
2467
+ return result;
2468
+ }
2469
+ catch (error) {
2470
+ if (candidate !== undefined && candidate !== previousFactory) {
2471
+ await candidate.close().catch(() => undefined);
2472
+ }
2473
+ throw error;
2474
+ }
2475
+ finally {
2476
+ if (this.callbackDrainFrozen) {
2477
+ this.callbackDrainFrozen = false;
2478
+ this.drainCallbackQueue();
2479
+ }
2480
+ }
2481
+ }
2482
+ async resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous) {
2483
+ for (let attempt = 0; attempt < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES; attempt += 1) {
2484
+ let candidate;
2485
+ try {
2486
+ candidate = await refreshFactory();
2487
+ validateCodexFactory(candidate);
2488
+ if (candidate.hostId !== previous.hostId ||
2489
+ candidate.protocol !== previousFactory.protocol) {
2490
+ throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The replacement Codex factory moved outside its pinned boundary.");
2491
+ }
2492
+ if (candidate === previousFactory ||
2493
+ candidate.endpointGeneration === previous.endpointGeneration) {
2494
+ if (candidate !== previousFactory)
2495
+ await candidate.close();
2496
+ candidate = undefined;
2497
+ if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
2498
+ continue;
2499
+ }
2500
+ throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2501
+ }
2502
+ // Endpoint generations never inherit cached compatibility. Every
2503
+ // candidate independently proves all four read-only wire probes.
2504
+ const probes = await this.runCompatibilityProbesFor(candidate);
2505
+ const attestation = evaluateCompatibilityAttestation({
2506
+ surface: "codex",
2507
+ version: candidate.appServerVersion,
2508
+ checkedAt: this.now().toISOString(),
2509
+ certifiedVersions: certifiedCompatibilityVersions.codex,
2510
+ probes,
2511
+ });
2512
+ return {
2513
+ factory: candidate,
2514
+ identity: {
2515
+ provider: "codex",
2516
+ hostId: previous.hostId,
2517
+ endpointGeneration: candidate.endpointGeneration,
2518
+ },
2519
+ attestation,
2520
+ };
2521
+ }
2522
+ catch (error) {
2523
+ if (candidate !== undefined && candidate !== previousFactory) {
2524
+ await candidate.close();
2525
+ }
2526
+ if (this.isEndpointGenerationChanged(error)) {
2527
+ if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
2528
+ continue;
2529
+ }
2530
+ throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2531
+ }
2532
+ throw error;
2533
+ }
2534
+ }
2535
+ throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2536
+ }
2537
+ async retireEndpointGeneration(factory, endpointGeneration) {
2538
+ this.callbackDrainFrozen = true;
2539
+ this.drainCallbackQueue(true);
2540
+ // Fence every operation admitted on the previous immutable factory.
2541
+ // Ordinary route creation is blocked by endpointRefresh; joining these
2542
+ // exact promises ensures none can be inserted after the close sweep.
2543
+ await Promise.allSettled([...this.routeCreations.values()]);
2544
+ await Promise.allSettled([...this.routeReleases.values()]);
2545
+ this.drainCallbackQueue(true);
2546
+ for (const [routeHandle, route] of [...this.routes]) {
2547
+ if (route.endpointGeneration !== endpointGeneration)
2548
+ continue;
2549
+ await this.closeRoute(route);
2550
+ if (this.routes.get(routeHandle) === route) {
2551
+ this.routes.delete(routeHandle);
2552
+ }
2553
+ }
2554
+ this.drainCallbackQueue(true);
2555
+ await factory.close();
2556
+ this.drainCallbackQueue(true);
2557
+ this.retiredEndpointGeneration = endpointGeneration;
2558
+ }
2559
+ reserveSelectorRefresh(result) {
2560
+ if (result.delivery !== "selector" || result.selectorClaimed) {
2561
+ return false;
2562
+ }
2563
+ result.selectorClaimed = true;
2564
+ return true;
2565
+ }
2566
+ emitSelectorRefreshFallback(result, staleRouteHandle) {
2567
+ if (result.delivery !== "selector" || !result.selectorClaimed)
2276
2568
  return;
2569
+ let event = result.event;
2570
+ if (staleRouteHandle !== undefined &&
2571
+ event.outcome === "compatible" &&
2572
+ event.routes.some((route) => route.routeHandle === staleRouteHandle)) {
2573
+ event = {
2574
+ ...event,
2575
+ routes: event.routes.filter((route) => route.routeHandle !== staleRouteHandle),
2576
+ };
2577
+ result.event = event;
2578
+ this.pendingEndpointRefreshEvent = event;
2277
2579
  }
2580
+ this.publishEndpointRefresh(event);
2581
+ }
2582
+ publishEndpointRefresh(event) {
2583
+ this.emitEndpointRefresh(event);
2584
+ if (event.outcome === "compatible") {
2585
+ this.armEndpointActivationRetry(event);
2586
+ }
2587
+ }
2588
+ armEndpointActivationRetry(event) {
2589
+ this.clearEndpointActivationRetry();
2590
+ this.endpointActivationRetry = { event, attempt: 0 };
2591
+ this.scheduleEndpointActivationRetry();
2592
+ }
2593
+ scheduleEndpointActivationRetry() {
2594
+ const pending = this.endpointActivationRetry;
2595
+ if (pending === undefined ||
2596
+ this.closing ||
2597
+ this.closed ||
2598
+ pending.attempt >= 3 ||
2599
+ (pending.event.outcome === "compatible" &&
2600
+ this.pendingEndpointAttestation === undefined)) {
2601
+ this.clearEndpointActivationRetry();
2602
+ return;
2603
+ }
2604
+ const delay = Math.max(DEFAULT_CODEX_RECOVERY_INITIAL_MS, Math.min(this.recoveryMaxMs, this.recoveryInitialMs * 2 ** pending.attempt));
2605
+ const timer = setTimeout(() => {
2606
+ if (this.endpointActivationRetryTimer !== timer)
2607
+ return;
2608
+ this.endpointActivationRetryTimer = undefined;
2609
+ const current = this.endpointActivationRetry;
2610
+ if (current === undefined || current.event !== pending.event)
2611
+ return;
2612
+ if (current.event.outcome === "compatible" &&
2613
+ this.pendingEndpointAttestation === undefined) {
2614
+ this.clearEndpointActivationRetry();
2615
+ return;
2616
+ }
2617
+ this.emitEndpointRefresh(current.event);
2618
+ if (this.endpointActivationRetry === undefined ||
2619
+ this.endpointActivationRetry.event !== current.event) {
2620
+ return;
2621
+ }
2622
+ this.endpointActivationRetry = {
2623
+ event: current.event,
2624
+ attempt: current.attempt + 1,
2625
+ };
2626
+ this.scheduleEndpointActivationRetry();
2627
+ }, delay);
2628
+ timer.unref();
2629
+ this.endpointActivationRetryTimer = timer;
2630
+ }
2631
+ clearEndpointActivationRetry() {
2632
+ if (this.endpointActivationRetryTimer !== undefined) {
2633
+ clearTimeout(this.endpointActivationRetryTimer);
2634
+ }
2635
+ this.endpointActivationRetryTimer = undefined;
2636
+ this.endpointActivationRetry = undefined;
2637
+ }
2638
+ emitEndpointRefresh(event) {
2639
+ const callbacks = this.callbacks;
2640
+ if (callbacks?.onEndpointRefresh === undefined)
2641
+ return;
2642
+ invokeCallback(() => callbacks.onEndpointRefresh?.(event));
2643
+ }
2644
+ onTurnResult(endpointGeneration, result) {
2278
2645
  const wantsReply = this.expectsReply.get(result.messageId) === true;
2279
2646
  this.expectsReply.delete(result.messageId);
2280
2647
  let replyText;
@@ -2306,6 +2673,7 @@ export class LocalCodexGatewayProvider {
2306
2673
  : "CODEX_DELIVERY_AMBIGUOUS";
2307
2674
  this.enqueueCodexCallback({
2308
2675
  type: "delivery",
2676
+ endpointGeneration,
2309
2677
  value: {
2310
2678
  messageId: result.messageId,
2311
2679
  state,
@@ -2317,6 +2685,7 @@ export class LocalCodexGatewayProvider {
2317
2685
  queueRouteObservation(route, observation) {
2318
2686
  this.enqueueCodexCallback({
2319
2687
  type: "route",
2688
+ endpointGeneration: route.endpointGeneration,
2320
2689
  generation: route.generation,
2321
2690
  routeHandle: route.threadId,
2322
2691
  state: codexRouteState(observation),
@@ -2340,6 +2709,8 @@ export class LocalCodexGatewayProvider {
2340
2709
  }
2341
2710
  }
2342
2711
  this.callbackQueue.push(event);
2712
+ if (this.callbackDrainFrozen)
2713
+ return;
2343
2714
  if (this.callbackScheduled)
2344
2715
  return;
2345
2716
  this.callbackScheduled = true;
@@ -2353,6 +2724,10 @@ export class LocalCodexGatewayProvider {
2353
2724
  const callbacks = this.callbacks;
2354
2725
  if (event === undefined || callbacks === undefined)
2355
2726
  return;
2727
+ if (event.endpointGeneration !== this.factory.endpointGeneration ||
2728
+ this.retiredEndpointGeneration === event.endpointGeneration) {
2729
+ return;
2730
+ }
2356
2731
  if (event.type === "delivery") {
2357
2732
  invokeCallback(() => callbacks.onDelivery(event.value));
2358
2733
  }
@@ -2361,7 +2736,11 @@ export class LocalCodexGatewayProvider {
2361
2736
  return;
2362
2737
  }
2363
2738
  invokeCallback(() => callbacks.onRouteState({
2364
- endpoint: callbackEndpoint(this.identity, event.routeHandle),
2739
+ endpoint: callbackEndpoint({
2740
+ provider: "codex",
2741
+ hostId: this.identity.hostId,
2742
+ endpointGeneration: event.endpointGeneration,
2743
+ }, event.routeHandle),
2365
2744
  state: event.state,
2366
2745
  ...(event.safeErrorCode === undefined
2367
2746
  ? {}
@@ -2369,7 +2748,9 @@ export class LocalCodexGatewayProvider {
2369
2748
  }));
2370
2749
  }
2371
2750
  }
2372
- drainCallbackQueue() {
2751
+ drainCallbackQueue(force = false) {
2752
+ if (this.callbackDrainFrozen && !force)
2753
+ return;
2373
2754
  while (this.callbackQueue.length > 0)
2374
2755
  this.drainOneCallback();
2375
2756
  }