agent-embassy 1.0.0 → 1.2.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 (73) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +50 -5
  3. package/README.zh-CN.md +32 -5
  4. package/SECURITY.md +26 -2
  5. package/assets/live-dashboard/app.css +14 -5
  6. package/dist/src/gateway/claude-helper-protocol.d.ts +3 -0
  7. package/dist/src/gateway/claude-helper-protocol.js +15 -1
  8. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  9. package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -0
  10. package/dist/src/gateway/claude-helper-supervisor.js +25 -1
  11. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  12. package/dist/src/gateway/cli-copy.en.d.ts +1 -0
  13. package/dist/src/gateway/cli-copy.en.js +1 -0
  14. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  15. package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -0
  16. package/dist/src/gateway/cli-copy.zh-CN.js +1 -0
  17. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  18. package/dist/src/gateway/cli.d.ts +1 -1
  19. package/dist/src/gateway/cli.js +20 -7
  20. package/dist/src/gateway/cli.js.map +1 -1
  21. package/dist/src/gateway/codex-app-server.js +6 -2
  22. package/dist/src/gateway/codex-app-server.js.map +1 -1
  23. package/dist/src/gateway/codex-local-transport.d.ts +13 -0
  24. package/dist/src/gateway/codex-local-transport.js +21 -2
  25. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  26. package/dist/src/gateway/config.js +5 -3
  27. package/dist/src/gateway/config.js.map +1 -1
  28. package/dist/src/gateway/control.d.ts +11 -1
  29. package/dist/src/gateway/control.js +48 -14
  30. package/dist/src/gateway/control.js.map +1 -1
  31. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  32. package/dist/src/gateway/dashboard-copy.en.d.ts +6 -0
  33. package/dist/src/gateway/dashboard-copy.en.js +11 -5
  34. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  35. package/dist/src/gateway/dashboard-copy.js +6 -0
  36. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  37. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +6 -0
  38. package/dist/src/gateway/dashboard-copy.zh-CN.js +11 -5
  39. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  40. package/dist/src/gateway/dashboard-model.d.ts +4 -2
  41. package/dist/src/gateway/dashboard-model.js +6 -0
  42. package/dist/src/gateway/dashboard-model.js.map +1 -1
  43. package/dist/src/gateway/dashboard.js +5 -3
  44. package/dist/src/gateway/dashboard.js.map +1 -1
  45. package/dist/src/gateway/live-dashboard-app/app.js +48 -10
  46. package/dist/src/gateway/live-dashboard-command.js +10 -0
  47. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  48. package/dist/src/gateway/live-dashboard-http.d.ts +3 -0
  49. package/dist/src/gateway/live-dashboard-http.js +13 -0
  50. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  51. package/dist/src/gateway/live-dashboard-stream.js +26 -25
  52. package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
  53. package/dist/src/gateway/provenance-envelope.d.ts +16 -0
  54. package/dist/src/gateway/provenance-envelope.js +80 -0
  55. package/dist/src/gateway/provenance-envelope.js.map +1 -0
  56. package/dist/src/gateway/providers.d.ts +43 -16
  57. package/dist/src/gateway/providers.js +688 -61
  58. package/dist/src/gateway/providers.js.map +1 -1
  59. package/dist/src/gateway/server.d.ts +2 -0
  60. package/dist/src/gateway/server.js +9 -3
  61. package/dist/src/gateway/server.js.map +1 -1
  62. package/dist/src/gateway/service.d.ts +102 -10
  63. package/dist/src/gateway/service.js +1309 -54
  64. package/dist/src/gateway/service.js.map +1 -1
  65. package/dist/src/gateway/store.d.ts +39 -3
  66. package/dist/src/gateway/store.js +528 -26
  67. package/dist/src/gateway/store.js.map +1 -1
  68. package/dist/src/gateway/types.d.ts +90 -5
  69. package/dist/src/gateway/types.js +6 -0
  70. package/dist/src/gateway/types.js.map +1 -1
  71. package/docs/GATEWAY-ARCHITECTURE.md +98 -11
  72. package/package.json +3 -2
  73. package/skills/embassy-peer/SKILL.md +15 -1
@@ -3,11 +3,12 @@ import path from "node:path";
3
3
  import { BridgeError } from "../errors.js";
4
4
  import { CLAUDE_PEER_COMPATIBILITY, ClaudePeerAdapter, } from "./claude-peer.js";
5
5
  import { startClaudeCompatibilityScratch, } from "./claude-compatibility-scratch.js";
6
- import { compatibilityProbeNames, sharesCompatibilityMajor, } from "./compatibility.js";
6
+ import { certifiedCompatibilityVersions, compatibilityProbeNames, evaluateCompatibilityAttestation, sharesCompatibilityMajor, } from "./compatibility.js";
7
7
  import { CODEX_APP_SERVER_WRITABLE_VERSIONS, CodexAppServerConnector, CodexConnectorError, } from "./codex-app-server.js";
8
8
  import { LocalCodexTransportError } from "./codex-local-transport.js";
9
9
  import { ClaudeNativeHelperSupervisor, } from "./claude-helper-supervisor.js";
10
10
  import { isDashboardLocale } from "./locale.js";
11
+ import { composeProvenanceEnvelope } from "./provenance-envelope.js";
11
12
  const LOCAL_HOST = "this-mac";
12
13
  const NATIVE_CLAUDE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
13
14
  const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
@@ -22,10 +23,45 @@ const DEFAULT_CODEX_CLEANUP_TIMEOUT_MS = 5_000;
22
23
  const DEFAULT_CODEX_CLEANUP_POLL_MS = 25;
23
24
  const DEFAULT_CODEX_RECOVERY_INITIAL_MS = 250;
24
25
  const DEFAULT_CODEX_RECOVERY_MAX_MS = 5_000;
26
+ const MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES = 3;
25
27
  const COMPATIBILITY_PROBE_THREAD_ID = "00000000-0000-7000-8000-000000000000";
26
28
  const COMPATIBILITY_TURN_TIMEOUT_MS = 60_000;
27
29
  const CLAUDE_CERTIFICATION_TIMEOUT_MS = 10_000;
28
30
  const CLAUDE_CERTIFICATION_DISCOVERY_POLL_MS = 100;
31
+ const CLAUDE_CLEAN_PREWRITE_RETRY_CODES = new Set([
32
+ "CLAUDE_PEER_TARGET_UNKNOWN",
33
+ "CLAUDE_PEER_TARGET_STALE",
34
+ "CLAUDE_PEER_TARGET_CHANGED",
35
+ "CLAUDE_PEER_WORKSPACE_UNATTESTED",
36
+ ]);
37
+ function claudeCleanPrewriteResult(error) {
38
+ if (error instanceof BridgeError &&
39
+ error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
40
+ return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
41
+ }
42
+ if (error instanceof BridgeError &&
43
+ CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
44
+ return { state: "deferred", safeErrorCode: error.code };
45
+ }
46
+ const systemCode = typeof error === "object" &&
47
+ error !== null &&
48
+ "code" in error &&
49
+ typeof error.code === "string"
50
+ ? error.code
51
+ : undefined;
52
+ return {
53
+ state: "failed",
54
+ safeErrorCode: error instanceof BridgeError
55
+ ? error.code
56
+ : systemCode === "ENOENT"
57
+ ? "CLAUDE_DISPATCH_PREWRITE_PATH_MISSING"
58
+ : systemCode === "EACCES" || systemCode === "EPERM"
59
+ ? "CLAUDE_DISPATCH_PREWRITE_ACCESS_DENIED"
60
+ : systemCode === "ETIMEDOUT"
61
+ ? "CLAUDE_DISPATCH_PREWRITE_TIMEOUT"
62
+ : "CLAUDE_DISPATCH_PREWRITE_FAILED",
63
+ };
64
+ }
29
65
  function passedProbe(name) {
30
66
  return { name, outcome: "pass" };
31
67
  }
@@ -835,7 +871,9 @@ export class LocalClaudeGatewayProvider {
835
871
  const selectedAlias = this.selected.get(input.binding.routeHandle);
836
872
  const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
837
873
  if (input.authorization === "selected_route" &&
838
- (selectedAlias === undefined || stateRoot === undefined)) {
874
+ (selectedAlias === undefined ||
875
+ selectedAlias !== input.targetAlias ||
876
+ stateRoot === undefined)) {
839
877
  return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
840
878
  }
841
879
  if (input.authorization === "selected_route") {
@@ -895,8 +933,20 @@ export class LocalClaudeGatewayProvider {
895
933
  this.nativeInbound.delete(input.binding.routeHandle);
896
934
  return { state: "failed", safeErrorCode: "CLAUDE_NATIVE_REPLY_STALE" };
897
935
  }
936
+ if (observedRoute.alias !== input.targetAlias) {
937
+ return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
938
+ }
898
939
  }
899
- else if (this.selected.get(input.binding.routeHandle) === undefined) {
940
+ else {
941
+ const selectedAlias = this.selected.get(input.binding.routeHandle);
942
+ const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
943
+ if (selectedAlias === undefined ||
944
+ selectedAlias !== input.targetAlias ||
945
+ stateRoot === undefined) {
946
+ return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
947
+ }
948
+ }
949
+ if (activeListener.registration?.alias !== input.sourceAlias) {
900
950
  return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
901
951
  }
902
952
  if (this.activeNativeCodexListener !== activeListener ||
@@ -912,15 +962,44 @@ export class LocalClaudeGatewayProvider {
912
962
  this.providerIdByGatewayId.has(input.messageId)) {
913
963
  return { state: "failed", safeErrorCode: "CLAUDE_RECEIPT_CAPACITY" };
914
964
  }
915
- this.providerIdByGatewayId.set(input.messageId, "");
916
- this.pendingTargetByGatewayId.set(input.messageId, input.binding.routeHandle);
965
+ let content;
966
+ try {
967
+ content = composeProvenanceEnvelope({
968
+ direction: "claude",
969
+ sourceAlias: input.sourceAlias,
970
+ targetAlias: input.targetAlias,
971
+ conversationId: input.conversationId,
972
+ body: input.text,
973
+ });
974
+ }
975
+ catch (error) {
976
+ if (error instanceof BridgeError &&
977
+ (error.code === "PROVENANCE_ENVELOPE_INVALID" ||
978
+ error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")) {
979
+ return { state: "failed", safeErrorCode: error.code };
980
+ }
981
+ throw error;
982
+ }
917
983
  if (input.authorization === "selected_route") {
984
+ const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
985
+ if (stateRoot === undefined) {
986
+ return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
987
+ }
918
988
  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.
989
+ // The exact workspace attestation is refreshed before every send. A
990
+ // clean failure keeps the route busy until a later authoritative idle
991
+ // observation wakes the one bounded retry.
921
992
  this.selectedObservationDirty.add(input.binding.routeHandle);
922
993
  this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
994
+ try {
995
+ await this.peer.assertTargetWorkspaceDisjoint(input.binding.routeHandle, stateRoot);
996
+ }
997
+ catch (error) {
998
+ return claudeCleanPrewriteResult(error);
999
+ }
923
1000
  }
1001
+ this.providerIdByGatewayId.set(input.messageId, "");
1002
+ this.pendingTargetByGatewayId.set(input.messageId, input.binding.routeHandle);
924
1003
  let providerId;
925
1004
  let trackingFailed = false;
926
1005
  let terminalDuringSend = false;
@@ -962,7 +1041,7 @@ export class LocalClaudeGatewayProvider {
962
1041
  // listener's bounded timeout is authoritative and may settle it later.
963
1042
  };
964
1043
  try {
965
- const result = await this.peer.send(input.binding.routeHandle, input.text, {
1044
+ const result = await this.peer.send(input.binding.routeHandle, content, {
966
1045
  listener: activeListener.listener,
967
1046
  receiptDeadlineAt: deadline,
968
1047
  onTransportStatus,
@@ -1017,11 +1096,7 @@ export class LocalClaudeGatewayProvider {
1017
1096
  }
1018
1097
  this.providerIdByGatewayId.delete(input.messageId);
1019
1098
  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" };
1099
+ return claudeCleanPrewriteResult(error);
1025
1100
  }
1026
1101
  }
1027
1102
  async releaseRoute(routeHandle) {
@@ -1585,13 +1660,12 @@ function validateCodexFactory(factory) {
1585
1660
  const schema = factory.schemaCompatibility;
1586
1661
  const write = factory.writeCompatibility;
1587
1662
  const certified = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(factory.appServerVersion);
1588
- const observedCandidate = factory.compatibilityPolicy === "observed" &&
1589
- schema.observedSchemaCandidate === true &&
1663
+ const schemaCandidate = schema.observedSchemaCandidate === true &&
1590
1664
  CODEX_APP_SERVER_WRITABLE_VERSIONS.some((version) => sharesCompatibilityMajor(version, factory.appServerVersion));
1591
1665
  if (factory.hostId !== LOCAL_HOST ||
1592
1666
  factory.protocol !== "codex-app-server" ||
1593
1667
  factory.protocolVersion !== factory.appServerVersion ||
1594
- (!certified && !observedCandidate) ||
1668
+ (!certified && !schemaCandidate) ||
1595
1669
  schema.appServerVersion !== factory.appServerVersion ||
1596
1670
  schema.endpointGeneration !== factory.endpointGeneration ||
1597
1671
  schema.protocol !== "app-server-v2-stable" ||
@@ -1638,10 +1712,9 @@ function codexRouteSafeCode(observation) {
1638
1712
  return undefined;
1639
1713
  }
1640
1714
  export class LocalCodexGatewayProvider {
1641
- identity;
1642
- protocol;
1643
- protocolVersion;
1644
1715
  factory;
1716
+ refreshFactory;
1717
+ compatibilityPolicy;
1645
1718
  maxCallbacks;
1646
1719
  maxRoutes;
1647
1720
  maxReplyBytes;
@@ -1651,32 +1724,41 @@ export class LocalCodexGatewayProvider {
1651
1724
  recoveryMaxMs;
1652
1725
  now;
1653
1726
  routes = new Map();
1727
+ /** Exact public alias authority retained across internal route recovery. */
1728
+ routeAliases = new Map();
1654
1729
  routeCreations = new Map();
1655
1730
  routeReleases = new Map();
1656
1731
  routeRecoveryAttempts = new Map();
1657
1732
  routeRecoveryTimers = new Map();
1658
1733
  routeRecoveries = new Map();
1659
1734
  routesRequiringRecovery = new Set();
1735
+ trackedRoutes = new Set();
1736
+ retiredEndpointGeneration;
1660
1737
  expectsReply = new Map();
1661
1738
  compatibilityCertifyingRoutes = new Set();
1662
1739
  compatibilityTurns = new Map();
1663
1740
  callbackQueue = [];
1664
1741
  callbackScheduled = false;
1665
1742
  callbacks;
1743
+ endpointRefresh;
1744
+ endpointRefreshDelivery;
1745
+ endpointActivationRetry;
1746
+ endpointActivationRetryTimer;
1747
+ callbackDrainFrozen = false;
1666
1748
  compatibilityAttested;
1749
+ endpointUnavailable = false;
1750
+ pendingEndpointAttestation;
1751
+ pendingEndpointRefreshEvent;
1667
1752
  initialized = false;
1668
1753
  closing = false;
1669
1754
  closed = false;
1670
1755
  constructor(options) {
1671
1756
  validateCodexFactory(options.factory);
1672
1757
  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;
1758
+ this.refreshFactory = options.refreshFactory;
1759
+ this.compatibilityPolicy =
1760
+ options.compatibilityPolicy ?? options.factory.compatibilityPolicy;
1761
+ exactLocalHost(options.factory.hostId);
1680
1762
  this.compatibilityAttested = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(options.factory.appServerVersion);
1681
1763
  this.maxCallbacks = positiveBounded(options.maxCallbackEvents, MAX_CODEX_CALLBACKS, 4_096);
1682
1764
  this.maxRoutes = positiveBounded(options.maxRoutes, MAX_CODEX_ROUTES, 256);
@@ -1693,30 +1775,60 @@ export class LocalCodexGatewayProvider {
1693
1775
  }
1694
1776
  this.now = options.now ?? (() => new Date());
1695
1777
  }
1778
+ get identity() {
1779
+ return {
1780
+ provider: "codex",
1781
+ hostId: exactLocalHost(this.factory.hostId),
1782
+ endpointGeneration: this.factory.endpointGeneration,
1783
+ };
1784
+ }
1785
+ get protocol() {
1786
+ return this.factory.protocol;
1787
+ }
1788
+ get protocolVersion() {
1789
+ return this.factory.protocolVersion;
1790
+ }
1696
1791
  compatibilitySurface() {
1697
1792
  return { surface: "codex", version: this.factory.appServerVersion };
1698
1793
  }
1699
1794
  acceptCompatibilityAttestation(attestation) {
1795
+ const pending = this.pendingEndpointAttestation;
1700
1796
  if (attestation.surface !== "codex" ||
1701
1797
  attestation.version !== this.factory.appServerVersion ||
1702
- attestation.tier === "incompatible") {
1798
+ attestation.tier === "incompatible" ||
1799
+ (this.endpointUnavailable && pending === undefined) ||
1800
+ (pending !== undefined &&
1801
+ (attestation.checkedAt !== pending.checkedAt ||
1802
+ attestation.tier !== pending.tier ||
1803
+ attestation.safeErrorCode !== pending.safeErrorCode ||
1804
+ attestation.probes.length !== pending.probes.length ||
1805
+ attestation.probes.some((probe, index) => probe.name !== pending.probes[index]?.name ||
1806
+ probe.outcome !== pending.probes[index]?.outcome ||
1807
+ probe.safeErrorCode !== pending.probes[index]?.safeErrorCode)))) {
1703
1808
  throw new BridgeError("CODEX_COMPATIBILITY_ATTESTATION_MISMATCH", "The Codex compatibility attestation does not match this endpoint.");
1704
1809
  }
1810
+ this.pendingEndpointAttestation = undefined;
1811
+ this.pendingEndpointRefreshEvent = undefined;
1812
+ this.clearEndpointActivationRetry();
1705
1813
  this.compatibilityAttested = true;
1814
+ this.endpointUnavailable = false;
1706
1815
  }
1707
1816
  async runCompatibilityProbes() {
1817
+ return await this.runCompatibilityProbesFor(this.factory);
1818
+ }
1819
+ async runCompatibilityProbesFor(factory) {
1708
1820
  const [installation, control, initialize, threadList] = compatibilityProbeNames.codex;
1709
1821
  let transport;
1710
1822
  let connector;
1711
1823
  let stage = "transport";
1712
1824
  try {
1713
- transport = await this.factory.connectTransport();
1825
+ transport = await factory.connectTransport();
1714
1826
  stage = "initialize";
1715
1827
  connector = await CodexAppServerConnector.connect({
1716
- compatibility: this.factory.schemaCompatibility,
1828
+ compatibility: factory.schemaCompatibility,
1717
1829
  writesEnabled: false,
1718
1830
  route: {
1719
- endpointGeneration: this.identity.endpointGeneration,
1831
+ endpointGeneration: factory.endpointGeneration,
1720
1832
  threadId: COMPATIBILITY_PROBE_THREAD_ID,
1721
1833
  },
1722
1834
  transport,
@@ -1727,7 +1839,6 @@ export class LocalCodexGatewayProvider {
1727
1839
  });
1728
1840
  stage = "thread_list";
1729
1841
  await connector.observeLoadedThread(connector.guard());
1730
- this.compatibilityAttested = true;
1731
1842
  return [
1732
1843
  passedProbe(installation),
1733
1844
  passedProbe(control),
@@ -1736,6 +1847,8 @@ export class LocalCodexGatewayProvider {
1736
1847
  ];
1737
1848
  }
1738
1849
  catch (error) {
1850
+ if (this.isEndpointGenerationChanged(error))
1851
+ throw error;
1739
1852
  if (stage === "transport") {
1740
1853
  const controlFailure = error instanceof LocalCodexTransportError &&
1741
1854
  [
@@ -1888,7 +2001,9 @@ export class LocalCodexGatewayProvider {
1888
2001
  : { health: "healthy", compatibility: "compatible" };
1889
2002
  }
1890
2003
  async selectRoute(input) {
1891
- this.assertReady();
2004
+ if (!this.initialized || this.closing || this.closed) {
2005
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2006
+ }
1892
2007
  if (!PUBLIC_ALIAS.test(input.alias) ||
1893
2008
  !input.alias.endsWith(`@${this.identity.hostId}`)) {
1894
2009
  throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex alias is outside the exact local provider boundary.");
@@ -1896,25 +2011,116 @@ export class LocalCodexGatewayProvider {
1896
2011
  if (!OPAQUE_ROUTE.test(input.routeHandle)) {
1897
2012
  throw new BridgeError("CODEX_ROUTE_INVALID", "The Codex task identifier is outside the exact provider boundary.");
1898
2013
  }
2014
+ const retainedAlias = this.routeAliases.get(input.routeHandle);
2015
+ if (retainedAlias !== undefined && retainedAlias !== input.alias) {
2016
+ throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex route is already bound to another exact public alias.");
2017
+ }
1899
2018
  this.cancelRouteRecovery(input.routeHandle);
1900
- const route = await this.ensureRoute(input.routeHandle);
2019
+ let refreshResult;
2020
+ let selectorRefreshReserved = false;
2021
+ let route;
2022
+ const stageRefreshedRoute = async (result) => {
2023
+ if (result === undefined ||
2024
+ result.event.outcome !== "compatible") {
2025
+ throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex endpoint replacement was not compatibility-attested.");
2026
+ }
2027
+ refreshResult = result;
2028
+ selectorRefreshReserved = this.reserveSelectorRefresh(result);
2029
+ if (!selectorRefreshReserved) {
2030
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
2031
+ }
2032
+ try {
2033
+ return await this.ensureRoute(input.routeHandle, false, true);
2034
+ }
2035
+ catch (selectionError) {
2036
+ this.emitSelectorRefreshFallback(result, input.routeHandle);
2037
+ throw selectionError;
2038
+ }
2039
+ };
2040
+ if (this.endpointRefresh !== undefined) {
2041
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
2042
+ }
2043
+ else if (this.endpointUnavailable &&
2044
+ this.pendingEndpointAttestation === undefined) {
2045
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
2046
+ }
2047
+ else {
2048
+ this.assertReady();
2049
+ try {
2050
+ route = await this.ensureRoute(input.routeHandle);
2051
+ }
2052
+ catch (error) {
2053
+ const refreshPending = error instanceof BridgeError &&
2054
+ error.code === "CODEX_ENDPOINT_REFRESH_PENDING";
2055
+ if (!this.isEndpointGenerationChanged(error) && !refreshPending) {
2056
+ throw error;
2057
+ }
2058
+ if (this.isEndpointGenerationChanged(error)) {
2059
+ this.endpointUnavailable = true;
2060
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
2061
+ }
2062
+ else if (this.endpointRefresh !== undefined) {
2063
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
2064
+ }
2065
+ else {
2066
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
2067
+ }
2068
+ }
2069
+ if (refreshResult === undefined &&
2070
+ (route.endpointGeneration !== this.factory.endpointGeneration ||
2071
+ this.endpointUnavailable ||
2072
+ this.endpointRefresh !== undefined)) {
2073
+ if (this.endpointRefresh === undefined) {
2074
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
2075
+ }
2076
+ route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
2077
+ }
2078
+ }
1901
2079
  const observation = route.connector.observation();
1902
2080
  const state = codexRouteState(observation);
1903
2081
  if (state === "stale") {
1904
- await this.releaseRoute(input.routeHandle);
2082
+ if (refreshResult !== undefined && selectorRefreshReserved) {
2083
+ // Publish the transition before any asynchronous connector cleanup so
2084
+ // the controller can accept or durably reject the exact replacement
2085
+ // generation. A route which closed after refresh staging is not valid
2086
+ // re-anchoring evidence and must be removed from both the callback and
2087
+ // its retained retry authority.
2088
+ this.emitSelectorRefreshFallback(refreshResult, input.routeHandle);
2089
+ }
2090
+ this.routesRequiringRecovery.add(input.routeHandle);
2091
+ await this.releaseRouteInternal(input.routeHandle);
2092
+ if (this.trackedRoutes.has(input.routeHandle)) {
2093
+ this.scheduleRouteRecovery(input.routeHandle);
2094
+ }
1905
2095
  throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task connection closed during route selection.");
1906
2096
  }
1907
- this.queueRouteObservation(route, observation);
2097
+ this.trackedRoutes.add(input.routeHandle);
2098
+ this.routeAliases.set(input.routeHandle, input.alias);
2099
+ if (refreshResult === undefined) {
2100
+ this.queueRouteObservation(route, observation);
2101
+ }
2102
+ const endpointRefresh = refreshResult === undefined || !selectorRefreshReserved
2103
+ ? undefined
2104
+ : refreshResult.event;
2105
+ if (endpointRefresh !== undefined) {
2106
+ this.armEndpointActivationRetry(endpointRefresh);
2107
+ }
1908
2108
  return {
1909
2109
  routeHandle: input.routeHandle,
1910
2110
  state,
2111
+ ...(endpointRefresh === undefined ? {} : { endpointRefresh }),
1911
2112
  };
1912
2113
  }
1913
2114
  observeRouteSuccessionBarrier(routeHandle) {
1914
- this.assertReady();
2115
+ if (!this.initialized || this.closing || this.closed) {
2116
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2117
+ }
1915
2118
  const route = this.routes.get(routeHandle);
1916
2119
  const observation = route?.connector.observation();
1917
- const routeCreationInFlight = this.routeCreations.has(routeHandle);
2120
+ const routeCreationInFlight = this.routeCreations.has(routeHandle) ||
2121
+ this.routeRecoveries.has(routeHandle) ||
2122
+ (this.endpointRefresh !== undefined && this.trackedRoutes.has(routeHandle)) ||
2123
+ this.pendingEndpointActivationClaims(routeHandle);
1918
2124
  const routeReleaseInFlight = this.routeReleases.has(routeHandle);
1919
2125
  const pendingReplyCorrelations = this.expectsReply.size;
1920
2126
  const pendingCallbacks = Math.max(this.callbackQueue.length, this.callbackScheduled ? 1 : 0);
@@ -1951,6 +2157,8 @@ export class LocalCodexGatewayProvider {
1951
2157
  if (!this.initialized ||
1952
2158
  this.closing ||
1953
2159
  this.closed ||
2160
+ this.endpointUnavailable ||
2161
+ this.endpointRefresh !== undefined ||
1954
2162
  !this.compatibilityAttested ||
1955
2163
  input.authorization !== "selected_route" ||
1956
2164
  !sameEndpoint(input.binding, this.identity)) {
@@ -1958,7 +2166,10 @@ export class LocalCodexGatewayProvider {
1958
2166
  }
1959
2167
  const route = this.routes.get(input.binding.routeHandle);
1960
2168
  if (route === undefined ||
1961
- this.routeReleases.has(input.binding.routeHandle)) {
2169
+ this.routeAliases.get(input.binding.routeHandle) !== input.targetAlias ||
2170
+ this.routeReleases.has(input.binding.routeHandle) ||
2171
+ route.endpointGeneration !== input.binding.endpointGeneration ||
2172
+ route.endpointGeneration !== this.factory.endpointGeneration) {
1962
2173
  return { state: "failed", safeErrorCode: "CODEX_ROUTE_UNAVAILABLE" };
1963
2174
  }
1964
2175
  if (this.compatibilityCertifyingRoutes.has(input.binding.routeHandle)) {
@@ -1978,13 +2189,31 @@ export class LocalCodexGatewayProvider {
1978
2189
  if (this.expectsReply.has(input.messageId)) {
1979
2190
  return { state: "failed", safeErrorCode: "CODEX_MESSAGE_DUPLICATE" };
1980
2191
  }
2192
+ let content;
2193
+ try {
2194
+ content = composeProvenanceEnvelope({
2195
+ direction: "codex",
2196
+ sourceAlias: input.sourceAlias,
2197
+ targetAlias: input.targetAlias,
2198
+ conversationId: input.conversationId,
2199
+ body: input.text,
2200
+ });
2201
+ }
2202
+ catch (error) {
2203
+ const safeErrorCode = error instanceof BridgeError &&
2204
+ (error.code === "PROVENANCE_ENVELOPE_INVALID" ||
2205
+ error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")
2206
+ ? error.code
2207
+ : "PROVENANCE_ENVELOPE_INVALID";
2208
+ return { state: "failed", safeErrorCode };
2209
+ }
1981
2210
  this.expectsReply.set(input.messageId, input.expectsReply);
1982
2211
  try {
1983
2212
  const disposition = await route.connector.submitMessage(guard, {
1984
2213
  deadlineAt: input.deadlineAt,
1985
2214
  messageId: input.messageId,
1986
2215
  ...(input.steer === true ? { steer: true } : {}),
1987
- text: input.text,
2216
+ text: content,
1988
2217
  });
1989
2218
  if (disposition.disposition === "deferred") {
1990
2219
  this.expectsReply.delete(input.messageId);
@@ -2019,11 +2248,22 @@ export class LocalCodexGatewayProvider {
2019
2248
  }
2020
2249
  }
2021
2250
  async releaseRoute(routeHandle) {
2251
+ if (this.pendingEndpointActivationClaims(routeHandle)) {
2252
+ throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route still belongs to an unaccepted endpoint transition.", true);
2253
+ }
2254
+ this.trackedRoutes.delete(routeHandle);
2022
2255
  this.cancelRouteRecovery(routeHandle);
2023
2256
  const recovery = this.routeRecoveries.get(routeHandle);
2024
2257
  if (recovery !== undefined)
2025
2258
  await recovery;
2259
+ const endpointRefresh = this.endpointRefresh;
2260
+ if (endpointRefresh !== undefined)
2261
+ await endpointRefresh;
2262
+ if (this.pendingEndpointActivationClaims(routeHandle)) {
2263
+ throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route acquired endpoint-transition authority during release.", true);
2264
+ }
2026
2265
  await this.releaseRouteInternal(routeHandle);
2266
+ this.routeAliases.delete(routeHandle);
2027
2267
  }
2028
2268
  async releaseRouteInternal(routeHandle) {
2029
2269
  const pending = this.routeReleases.get(routeHandle);
@@ -2053,6 +2293,12 @@ export class LocalCodexGatewayProvider {
2053
2293
  clearTimeout(timer);
2054
2294
  this.routeRecoveryTimers.clear();
2055
2295
  this.routesRequiringRecovery.clear();
2296
+ this.clearEndpointActivationRetry();
2297
+ this.pendingEndpointRefreshEvent = undefined;
2298
+ this.pendingEndpointAttestation = undefined;
2299
+ const refreshResult = this.endpointRefresh === undefined
2300
+ ? []
2301
+ : await Promise.allSettled([this.endpointRefresh]);
2056
2302
  const entries = [...this.routes.entries()];
2057
2303
  const routeResults = await Promise.allSettled(entries.map(async ([routeHandle]) => this.releaseRoute(routeHandle)));
2058
2304
  await Promise.allSettled([
@@ -2060,7 +2306,8 @@ export class LocalCodexGatewayProvider {
2060
2306
  ...this.routeCreations.values(),
2061
2307
  ]);
2062
2308
  this.drainCallbackQueue();
2063
- if (routeResults.some((result) => result.status === "rejected")) {
2309
+ if (refreshResult.some((result) => result.status === "rejected") ||
2310
+ routeResults.some((result) => result.status === "rejected")) {
2064
2311
  this.closing = false;
2065
2312
  throw new BridgeError("CODEX_PROVIDER_CLEANUP_FAILED", "An exact owned Codex turn did not confirm termination.");
2066
2313
  }
@@ -2085,6 +2332,10 @@ export class LocalCodexGatewayProvider {
2085
2332
  }
2086
2333
  this.compatibilityTurns.clear();
2087
2334
  this.compatibilityCertifyingRoutes.clear();
2335
+ this.pendingEndpointAttestation = undefined;
2336
+ this.pendingEndpointRefreshEvent = undefined;
2337
+ this.trackedRoutes.clear();
2338
+ this.routeAliases.clear();
2088
2339
  this.expectsReply.clear();
2089
2340
  this.callbacks = undefined;
2090
2341
  }
@@ -2092,16 +2343,25 @@ export class LocalCodexGatewayProvider {
2092
2343
  if (!this.initialized ||
2093
2344
  this.closing ||
2094
2345
  this.closed ||
2346
+ this.endpointUnavailable ||
2347
+ this.endpointRefresh !== undefined ||
2095
2348
  !this.compatibilityAttested) {
2096
2349
  throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2097
2350
  }
2098
2351
  }
2099
- async ensureRoute(threadId) {
2100
- this.assertReady();
2352
+ async ensureRoute(threadId, queueInitialObservation = true, allowPendingEndpointAttestation = false) {
2353
+ if (allowPendingEndpointAttestation) {
2354
+ if (!this.initialized || this.closing || this.closed) {
2355
+ throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2356
+ }
2357
+ }
2358
+ else {
2359
+ this.assertReady();
2360
+ }
2101
2361
  const pendingRelease = this.routeReleases.get(threadId);
2102
2362
  if (pendingRelease !== undefined) {
2103
2363
  await pendingRelease;
2104
- return await this.ensureRoute(threadId);
2364
+ return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
2105
2365
  }
2106
2366
  const existing = this.routes.get(threadId);
2107
2367
  if (existing !== undefined) {
@@ -2113,7 +2373,7 @@ export class LocalCodexGatewayProvider {
2113
2373
  // a fresh one. The identity-checked release lets automatic recovery and
2114
2374
  // an explicit selector join the same cleanup safely.
2115
2375
  await this.releaseRouteInternal(threadId);
2116
- return await this.ensureRoute(threadId);
2376
+ return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
2117
2377
  }
2118
2378
  const pending = this.routeCreations.get(threadId);
2119
2379
  if (pending !== undefined)
@@ -2121,42 +2381,51 @@ export class LocalCodexGatewayProvider {
2121
2381
  if (this.routes.size + this.routeCreations.size >= this.maxRoutes) {
2122
2382
  throw new BridgeError("CODEX_ROUTE_CAPACITY", "The bounded local Codex route table is full.", true);
2123
2383
  }
2124
- const creation = this.createRoute(threadId);
2125
- this.routeCreations.set(threadId, creation);
2126
- try {
2127
- const route = await creation;
2384
+ const admittedFactory = this.factory;
2385
+ const creation = (async () => {
2386
+ const route = await this.createRoute(threadId, admittedFactory, queueInitialObservation);
2128
2387
  if (this.closing || this.closed) {
2129
2388
  await this.closeRoute(route);
2130
2389
  throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider closed during route creation.");
2131
2390
  }
2391
+ if (!allowPendingEndpointAttestation &&
2392
+ (this.endpointUnavailable ||
2393
+ this.endpointRefresh !== undefined ||
2394
+ admittedFactory !== this.factory ||
2395
+ route.endpointGeneration !== this.factory.endpointGeneration)) {
2396
+ await this.closeRoute(route);
2397
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The Codex endpoint moved during exact route creation.", true);
2398
+ }
2132
2399
  this.routes.set(threadId, route);
2133
2400
  return route;
2401
+ })();
2402
+ this.routeCreations.set(threadId, creation);
2403
+ try {
2404
+ return await creation;
2134
2405
  }
2135
2406
  finally {
2136
2407
  this.routeCreations.delete(threadId);
2137
2408
  }
2138
2409
  }
2139
- async createRoute(threadId) {
2410
+ async createRoute(threadId, factory, queueInitialObservation) {
2140
2411
  let transport;
2141
2412
  let connector;
2142
2413
  const generation = Symbol(threadId);
2143
2414
  try {
2144
- transport = await this.factory.connectTransport();
2145
- const writesEnabled = this.factory.writableReady &&
2146
- this.factory.writeCompatibility !== null;
2415
+ transport = await factory.connectTransport();
2416
+ const writesEnabled = factory.writableReady && factory.writeCompatibility !== null;
2147
2417
  connector = await CodexAppServerConnector.connect({
2148
- compatibility: this.factory.writeCompatibility ??
2149
- this.factory.schemaCompatibility,
2418
+ compatibility: factory.writeCompatibility ?? factory.schemaCompatibility,
2150
2419
  writesEnabled,
2151
2420
  route: {
2152
- endpointGeneration: this.identity.endpointGeneration,
2421
+ endpointGeneration: factory.endpointGeneration,
2153
2422
  threadId,
2154
2423
  },
2155
2424
  transport,
2156
2425
  maxReplyBytes: this.maxReplyBytes,
2157
2426
  now: this.now,
2158
2427
  onEvent: (event) => this.onConnectorEvent(threadId, generation, event),
2159
- onTurnResult: (result) => this.onTurnResult(result),
2428
+ onTurnResult: (result) => this.onTurnResult(factory.endpointGeneration, result),
2160
2429
  });
2161
2430
  const loaded = await connector.observeLoadedThread(connector.guard());
2162
2431
  if (!loaded.selectedThreadLoaded) {
@@ -2165,11 +2434,14 @@ export class LocalCodexGatewayProvider {
2165
2434
  await connector.resumeThread(connector.guard());
2166
2435
  const route = {
2167
2436
  connector,
2437
+ endpointGeneration: factory.endpointGeneration,
2168
2438
  generation,
2169
2439
  threadId,
2170
2440
  transport,
2171
2441
  };
2172
- this.queueRouteObservation(route, connector.observation());
2442
+ if (queueInitialObservation) {
2443
+ this.queueRouteObservation(route, connector.observation());
2444
+ }
2173
2445
  return route;
2174
2446
  }
2175
2447
  catch (error) {
@@ -2179,6 +2451,8 @@ export class LocalCodexGatewayProvider {
2179
2451
  else if (transport !== undefined) {
2180
2452
  await transport.close().catch(() => undefined);
2181
2453
  }
2454
+ if (this.isEndpointGenerationChanged(error))
2455
+ throw error;
2182
2456
  if (error instanceof BridgeError)
2183
2457
  throw error;
2184
2458
  if (error instanceof CodexConnectorError) {
@@ -2236,6 +2510,19 @@ export class LocalCodexGatewayProvider {
2236
2510
  return;
2237
2511
  }
2238
2512
  try {
2513
+ if (this.endpointUnavailable) {
2514
+ if (this.pendingEndpointAttestation !== undefined) {
2515
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is awaiting controller activation.", true);
2516
+ }
2517
+ const refreshed = await this.refreshEndpoint("callback");
2518
+ if (refreshed?.event.outcome === "compatible" &&
2519
+ refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
2520
+ this.routesRequiringRecovery.delete(threadId);
2521
+ this.routeRecoveryAttempts.delete(threadId);
2522
+ return;
2523
+ }
2524
+ throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is not ready.", true);
2525
+ }
2239
2526
  const current = this.routes.get(threadId);
2240
2527
  if (current !== undefined &&
2241
2528
  codexRouteState(current.connector.observation()) === "stale") {
@@ -2255,7 +2542,17 @@ export class LocalCodexGatewayProvider {
2255
2542
  this.routeRecoveryAttempts.delete(threadId);
2256
2543
  this.queueRouteObservation(replacement, replacement.connector.observation());
2257
2544
  }
2258
- catch {
2545
+ catch (error) {
2546
+ if (this.isEndpointGenerationChanged(error)) {
2547
+ this.endpointUnavailable = true;
2548
+ const refreshed = await this.refreshEndpoint("callback").catch(() => undefined);
2549
+ if (refreshed?.event.outcome === "compatible" &&
2550
+ refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
2551
+ this.routesRequiringRecovery.delete(threadId);
2552
+ this.routeRecoveryAttempts.delete(threadId);
2553
+ return;
2554
+ }
2555
+ }
2259
2556
  this.routeRecoveryAttempts.set(threadId, (this.routeRecoveryAttempts.get(threadId) ?? 0) + 1);
2260
2557
  }
2261
2558
  }
@@ -2267,7 +2564,323 @@ export class LocalCodexGatewayProvider {
2267
2564
  clearTimeout(timer);
2268
2565
  this.routeRecoveryTimers.delete(threadId);
2269
2566
  }
2270
- onTurnResult(result) {
2567
+ isEndpointGenerationChanged(error) {
2568
+ return (error instanceof LocalCodexTransportError &&
2569
+ error.code === "ENDPOINT_GENERATION_CHANGED");
2570
+ }
2571
+ pendingEndpointActivationClaims(routeHandle) {
2572
+ const event = this.pendingEndpointRefreshEvent;
2573
+ return (event !== undefined &&
2574
+ event.current.endpointGeneration === this.factory.endpointGeneration &&
2575
+ event.routes.some((route) => route.routeHandle === routeHandle));
2576
+ }
2577
+ async refreshEndpoint(delivery) {
2578
+ if (this.refreshFactory === undefined || this.closing || this.closed) {
2579
+ return undefined;
2580
+ }
2581
+ const existing = this.endpointRefresh;
2582
+ if (existing !== undefined) {
2583
+ // A selector already running inside the controller lock takes ownership
2584
+ // of the transition result. This avoids waiting on a callback that needs
2585
+ // that same lock while still ensuring exactly one activation path.
2586
+ if (delivery === "selector")
2587
+ this.endpointRefreshDelivery = "selector";
2588
+ return await existing;
2589
+ }
2590
+ this.endpointRefreshDelivery = delivery;
2591
+ const refresh = this.performEndpointRefresh();
2592
+ this.endpointRefresh = refresh;
2593
+ try {
2594
+ return await refresh;
2595
+ }
2596
+ finally {
2597
+ if (this.endpointRefresh === refresh) {
2598
+ this.endpointRefresh = undefined;
2599
+ this.endpointRefreshDelivery = undefined;
2600
+ }
2601
+ }
2602
+ }
2603
+ async performEndpointRefresh() {
2604
+ const refreshFactory = this.refreshFactory;
2605
+ if (refreshFactory === undefined)
2606
+ return undefined;
2607
+ const previousFactory = this.factory;
2608
+ const previous = this.identity;
2609
+ let candidate;
2610
+ try {
2611
+ const resolved = await this.resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous);
2612
+ candidate = resolved.factory;
2613
+ const { attestation, identity: candidateIdentity } = resolved;
2614
+ await this.retireEndpointGeneration(previousFactory, previous.endpointGeneration);
2615
+ if (attestation.tier === "incompatible") {
2616
+ await candidate.close();
2617
+ candidate = undefined;
2618
+ this.compatibilityAttested = false;
2619
+ this.pendingEndpointAttestation = undefined;
2620
+ this.pendingEndpointRefreshEvent = undefined;
2621
+ for (const routeHandle of this.trackedRoutes) {
2622
+ this.routesRequiringRecovery.add(routeHandle);
2623
+ }
2624
+ if (this.closing || this.closed)
2625
+ return undefined;
2626
+ const result = {
2627
+ event: {
2628
+ outcome: "incompatible",
2629
+ previous,
2630
+ candidate: candidateIdentity,
2631
+ attestation,
2632
+ },
2633
+ delivery: "callback",
2634
+ selectorClaimed: true,
2635
+ };
2636
+ this.emitEndpointRefresh(result.event);
2637
+ for (const routeHandle of this.routesRequiringRecovery) {
2638
+ if (this.trackedRoutes.has(routeHandle)) {
2639
+ this.scheduleRouteRecovery(routeHandle);
2640
+ }
2641
+ }
2642
+ return result;
2643
+ }
2644
+ if (this.closing || this.closed) {
2645
+ await candidate.close();
2646
+ candidate = undefined;
2647
+ return undefined;
2648
+ }
2649
+ this.factory = candidate;
2650
+ candidate = undefined;
2651
+ this.retiredEndpointGeneration = undefined;
2652
+ this.compatibilityAttested = false;
2653
+ this.endpointUnavailable = true;
2654
+ this.pendingEndpointAttestation = attestation;
2655
+ const refreshedRoutes = [];
2656
+ for (const routeHandle of [...this.trackedRoutes]) {
2657
+ if (this.closing || this.closed)
2658
+ break;
2659
+ try {
2660
+ const route = await this.createRoute(routeHandle, this.factory, false);
2661
+ if (this.closing || this.closed) {
2662
+ await this.closeRoute(route);
2663
+ break;
2664
+ }
2665
+ const state = codexRouteState(route.connector.observation());
2666
+ if (state === "stale") {
2667
+ await this.closeRoute(route);
2668
+ throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task was stale on the replacement endpoint.");
2669
+ }
2670
+ this.routes.set(routeHandle, route);
2671
+ this.routesRequiringRecovery.delete(routeHandle);
2672
+ this.routeRecoveryAttempts.delete(routeHandle);
2673
+ refreshedRoutes.push({ routeHandle, state });
2674
+ }
2675
+ catch {
2676
+ this.routesRequiringRecovery.add(routeHandle);
2677
+ }
2678
+ }
2679
+ if (this.closing || this.closed)
2680
+ return undefined;
2681
+ const result = {
2682
+ event: {
2683
+ outcome: "compatible",
2684
+ previous,
2685
+ current: this.identity,
2686
+ attestation,
2687
+ routes: refreshedRoutes,
2688
+ },
2689
+ delivery: this.endpointRefreshDelivery ?? "callback",
2690
+ selectorClaimed: false,
2691
+ };
2692
+ if (result.event.outcome === "compatible") {
2693
+ this.pendingEndpointRefreshEvent = result.event;
2694
+ }
2695
+ if (result.delivery === "callback") {
2696
+ result.selectorClaimed = true;
2697
+ this.publishEndpointRefresh(result.event);
2698
+ }
2699
+ for (const routeHandle of this.routesRequiringRecovery) {
2700
+ if (this.trackedRoutes.has(routeHandle)) {
2701
+ this.scheduleRouteRecovery(routeHandle);
2702
+ }
2703
+ }
2704
+ return result;
2705
+ }
2706
+ catch (error) {
2707
+ if (candidate !== undefined && candidate !== previousFactory) {
2708
+ await candidate.close().catch(() => undefined);
2709
+ }
2710
+ throw error;
2711
+ }
2712
+ finally {
2713
+ if (this.callbackDrainFrozen) {
2714
+ this.callbackDrainFrozen = false;
2715
+ this.drainCallbackQueue();
2716
+ }
2717
+ }
2718
+ }
2719
+ async resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous) {
2720
+ for (let attempt = 0; attempt < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES; attempt += 1) {
2721
+ let candidate;
2722
+ try {
2723
+ candidate = await refreshFactory();
2724
+ validateCodexFactory(candidate);
2725
+ if (candidate.hostId !== previous.hostId ||
2726
+ candidate.protocol !== previousFactory.protocol ||
2727
+ candidate.compatibilityPolicy !== this.compatibilityPolicy) {
2728
+ throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The replacement Codex factory moved outside its pinned boundary.");
2729
+ }
2730
+ if (candidate === previousFactory ||
2731
+ candidate.endpointGeneration === previous.endpointGeneration) {
2732
+ if (candidate !== previousFactory)
2733
+ await candidate.close();
2734
+ candidate = undefined;
2735
+ if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
2736
+ continue;
2737
+ }
2738
+ throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2739
+ }
2740
+ // Endpoint generations never inherit cached compatibility. Every
2741
+ // candidate independently proves all four read-only wire probes.
2742
+ const probes = await this.runCompatibilityProbesFor(candidate);
2743
+ const attestation = evaluateCompatibilityAttestation({
2744
+ surface: "codex",
2745
+ version: candidate.appServerVersion,
2746
+ checkedAt: this.now().toISOString(),
2747
+ policy: this.compatibilityPolicy,
2748
+ certifiedVersions: certifiedCompatibilityVersions.codex,
2749
+ probes,
2750
+ });
2751
+ return {
2752
+ factory: candidate,
2753
+ identity: {
2754
+ provider: "codex",
2755
+ hostId: previous.hostId,
2756
+ endpointGeneration: candidate.endpointGeneration,
2757
+ },
2758
+ attestation,
2759
+ };
2760
+ }
2761
+ catch (error) {
2762
+ if (candidate !== undefined && candidate !== previousFactory) {
2763
+ await candidate.close();
2764
+ }
2765
+ if (this.isEndpointGenerationChanged(error)) {
2766
+ if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
2767
+ continue;
2768
+ }
2769
+ throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2770
+ }
2771
+ throw error;
2772
+ }
2773
+ }
2774
+ throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2775
+ }
2776
+ async retireEndpointGeneration(factory, endpointGeneration) {
2777
+ this.callbackDrainFrozen = true;
2778
+ this.drainCallbackQueue(true);
2779
+ // Fence every operation admitted on the previous immutable factory.
2780
+ // Ordinary route creation is blocked by endpointRefresh; joining these
2781
+ // exact promises ensures none can be inserted after the close sweep.
2782
+ await Promise.allSettled([...this.routeCreations.values()]);
2783
+ await Promise.allSettled([...this.routeReleases.values()]);
2784
+ this.drainCallbackQueue(true);
2785
+ for (const [routeHandle, route] of [...this.routes]) {
2786
+ if (route.endpointGeneration !== endpointGeneration)
2787
+ continue;
2788
+ await this.closeRoute(route);
2789
+ if (this.routes.get(routeHandle) === route) {
2790
+ this.routes.delete(routeHandle);
2791
+ }
2792
+ }
2793
+ this.drainCallbackQueue(true);
2794
+ await factory.close();
2795
+ this.drainCallbackQueue(true);
2796
+ this.retiredEndpointGeneration = endpointGeneration;
2797
+ }
2798
+ reserveSelectorRefresh(result) {
2799
+ if (result.delivery !== "selector" || result.selectorClaimed) {
2800
+ return false;
2801
+ }
2802
+ result.selectorClaimed = true;
2803
+ return true;
2804
+ }
2805
+ emitSelectorRefreshFallback(result, staleRouteHandle) {
2806
+ if (result.delivery !== "selector" || !result.selectorClaimed)
2807
+ return;
2808
+ let event = result.event;
2809
+ if (staleRouteHandle !== undefined &&
2810
+ event.outcome === "compatible" &&
2811
+ event.routes.some((route) => route.routeHandle === staleRouteHandle)) {
2812
+ event = {
2813
+ ...event,
2814
+ routes: event.routes.filter((route) => route.routeHandle !== staleRouteHandle),
2815
+ };
2816
+ result.event = event;
2817
+ this.pendingEndpointRefreshEvent = event;
2818
+ }
2819
+ this.publishEndpointRefresh(event);
2820
+ }
2821
+ publishEndpointRefresh(event) {
2822
+ this.emitEndpointRefresh(event);
2823
+ if (event.outcome === "compatible") {
2824
+ this.armEndpointActivationRetry(event);
2825
+ }
2826
+ }
2827
+ armEndpointActivationRetry(event) {
2828
+ this.clearEndpointActivationRetry();
2829
+ this.endpointActivationRetry = { event, attempt: 0 };
2830
+ this.scheduleEndpointActivationRetry();
2831
+ }
2832
+ scheduleEndpointActivationRetry() {
2833
+ const pending = this.endpointActivationRetry;
2834
+ if (pending === undefined ||
2835
+ this.closing ||
2836
+ this.closed ||
2837
+ pending.attempt >= 3 ||
2838
+ (pending.event.outcome === "compatible" &&
2839
+ this.pendingEndpointAttestation === undefined)) {
2840
+ this.clearEndpointActivationRetry();
2841
+ return;
2842
+ }
2843
+ const delay = Math.max(DEFAULT_CODEX_RECOVERY_INITIAL_MS, Math.min(this.recoveryMaxMs, this.recoveryInitialMs * 2 ** pending.attempt));
2844
+ const timer = setTimeout(() => {
2845
+ if (this.endpointActivationRetryTimer !== timer)
2846
+ return;
2847
+ this.endpointActivationRetryTimer = undefined;
2848
+ const current = this.endpointActivationRetry;
2849
+ if (current === undefined || current.event !== pending.event)
2850
+ return;
2851
+ if (current.event.outcome === "compatible" &&
2852
+ this.pendingEndpointAttestation === undefined) {
2853
+ this.clearEndpointActivationRetry();
2854
+ return;
2855
+ }
2856
+ this.emitEndpointRefresh(current.event);
2857
+ if (this.endpointActivationRetry === undefined ||
2858
+ this.endpointActivationRetry.event !== current.event) {
2859
+ return;
2860
+ }
2861
+ this.endpointActivationRetry = {
2862
+ event: current.event,
2863
+ attempt: current.attempt + 1,
2864
+ };
2865
+ this.scheduleEndpointActivationRetry();
2866
+ }, delay);
2867
+ timer.unref();
2868
+ this.endpointActivationRetryTimer = timer;
2869
+ }
2870
+ clearEndpointActivationRetry() {
2871
+ if (this.endpointActivationRetryTimer !== undefined) {
2872
+ clearTimeout(this.endpointActivationRetryTimer);
2873
+ }
2874
+ this.endpointActivationRetryTimer = undefined;
2875
+ this.endpointActivationRetry = undefined;
2876
+ }
2877
+ emitEndpointRefresh(event) {
2878
+ const callbacks = this.callbacks;
2879
+ if (callbacks?.onEndpointRefresh === undefined)
2880
+ return;
2881
+ invokeCallback(() => callbacks.onEndpointRefresh?.(event));
2882
+ }
2883
+ onTurnResult(endpointGeneration, result) {
2271
2884
  const certification = this.compatibilityTurns.get(result.messageId);
2272
2885
  if (certification !== undefined) {
2273
2886
  clearTimeout(certification.timer);
@@ -2306,6 +2919,7 @@ export class LocalCodexGatewayProvider {
2306
2919
  : "CODEX_DELIVERY_AMBIGUOUS";
2307
2920
  this.enqueueCodexCallback({
2308
2921
  type: "delivery",
2922
+ endpointGeneration,
2309
2923
  value: {
2310
2924
  messageId: result.messageId,
2311
2925
  state,
@@ -2317,6 +2931,7 @@ export class LocalCodexGatewayProvider {
2317
2931
  queueRouteObservation(route, observation) {
2318
2932
  this.enqueueCodexCallback({
2319
2933
  type: "route",
2934
+ endpointGeneration: route.endpointGeneration,
2320
2935
  generation: route.generation,
2321
2936
  routeHandle: route.threadId,
2322
2937
  state: codexRouteState(observation),
@@ -2340,6 +2955,8 @@ export class LocalCodexGatewayProvider {
2340
2955
  }
2341
2956
  }
2342
2957
  this.callbackQueue.push(event);
2958
+ if (this.callbackDrainFrozen)
2959
+ return;
2343
2960
  if (this.callbackScheduled)
2344
2961
  return;
2345
2962
  this.callbackScheduled = true;
@@ -2353,6 +2970,10 @@ export class LocalCodexGatewayProvider {
2353
2970
  const callbacks = this.callbacks;
2354
2971
  if (event === undefined || callbacks === undefined)
2355
2972
  return;
2973
+ if (event.endpointGeneration !== this.factory.endpointGeneration ||
2974
+ this.retiredEndpointGeneration === event.endpointGeneration) {
2975
+ return;
2976
+ }
2356
2977
  if (event.type === "delivery") {
2357
2978
  invokeCallback(() => callbacks.onDelivery(event.value));
2358
2979
  }
@@ -2361,7 +2982,11 @@ export class LocalCodexGatewayProvider {
2361
2982
  return;
2362
2983
  }
2363
2984
  invokeCallback(() => callbacks.onRouteState({
2364
- endpoint: callbackEndpoint(this.identity, event.routeHandle),
2985
+ endpoint: callbackEndpoint({
2986
+ provider: "codex",
2987
+ hostId: this.identity.hostId,
2988
+ endpointGeneration: event.endpointGeneration,
2989
+ }, event.routeHandle),
2365
2990
  state: event.state,
2366
2991
  ...(event.safeErrorCode === undefined
2367
2992
  ? {}
@@ -2369,7 +2994,9 @@ export class LocalCodexGatewayProvider {
2369
2994
  }));
2370
2995
  }
2371
2996
  }
2372
- drainCallbackQueue() {
2997
+ drainCallbackQueue(force = false) {
2998
+ if (this.callbackDrainFrozen && !force)
2999
+ return;
2373
3000
  while (this.callbackQueue.length > 0)
2374
3001
  this.drainOneCallback();
2375
3002
  }