agent-embassy 1.7.0 → 1.8.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 (118) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +51 -47
  3. package/README.md +26 -9
  4. package/README.zh-CN.md +22 -9
  5. package/SECURITY.md +48 -35
  6. package/dist/src/gateway/acp-client.d.ts +12 -19
  7. package/dist/src/gateway/acp-client.js +90 -65
  8. package/dist/src/gateway/acp-client.js.map +1 -1
  9. package/dist/src/gateway/acp-provider.d.ts +11 -10
  10. package/dist/src/gateway/acp-provider.js +165 -53
  11. package/dist/src/gateway/acp-provider.js.map +1 -1
  12. package/dist/src/gateway/claude-helper-client.d.ts +2 -43
  13. package/dist/src/gateway/claude-helper-client.js +1 -211
  14. package/dist/src/gateway/claude-helper-client.js.map +1 -1
  15. package/dist/src/gateway/claude-helper-protocol.d.ts +16 -74
  16. package/dist/src/gateway/claude-helper-protocol.js +99 -378
  17. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  18. package/dist/src/gateway/claude-helper-supervisor.d.ts +56 -44
  19. package/dist/src/gateway/claude-helper-supervisor.js +265 -456
  20. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  21. package/dist/src/gateway/claude-helper.js +184 -210
  22. package/dist/src/gateway/claude-helper.js.map +1 -1
  23. package/dist/src/gateway/claude-peer.d.ts +20 -87
  24. package/dist/src/gateway/claude-peer.js +310 -1009
  25. package/dist/src/gateway/claude-peer.js.map +1 -1
  26. package/dist/src/gateway/claude-runtime.d.ts +13 -15
  27. package/dist/src/gateway/claude-runtime.js +14 -168
  28. package/dist/src/gateway/claude-runtime.js.map +1 -1
  29. package/dist/src/gateway/cli-copy.en.d.ts +2 -2
  30. package/dist/src/gateway/cli-copy.en.js +4 -2
  31. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  32. package/dist/src/gateway/cli-copy.zh-CN.d.ts +2 -2
  33. package/dist/src/gateway/cli-copy.zh-CN.js +4 -2
  34. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  35. package/dist/src/gateway/cli.d.ts +7 -2
  36. package/dist/src/gateway/cli.js +277 -575
  37. package/dist/src/gateway/cli.js.map +1 -1
  38. package/dist/src/gateway/codex-app-server.d.ts +5 -227
  39. package/dist/src/gateway/codex-app-server.js +18 -1463
  40. package/dist/src/gateway/codex-app-server.js.map +1 -1
  41. package/dist/src/gateway/codex-doctor.d.ts +36 -0
  42. package/dist/src/gateway/codex-doctor.js +127 -0
  43. package/dist/src/gateway/codex-doctor.js.map +1 -0
  44. package/dist/src/gateway/codex-local-transport.d.ts +6 -6
  45. package/dist/src/gateway/codex-local-transport.js +13 -10
  46. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  47. package/dist/src/gateway/codex-stateless-transport.d.ts +114 -0
  48. package/dist/src/gateway/codex-stateless-transport.js +1108 -0
  49. package/dist/src/gateway/codex-stateless-transport.js.map +1 -0
  50. package/dist/src/gateway/config.d.ts +2 -9
  51. package/dist/src/gateway/config.js +62 -100
  52. package/dist/src/gateway/config.js.map +1 -1
  53. package/dist/src/gateway/control.d.ts +36 -90
  54. package/dist/src/gateway/control.js +465 -1123
  55. package/dist/src/gateway/control.js.map +1 -1
  56. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  57. package/dist/src/gateway/dashboard-copy.en.d.ts +8 -26
  58. package/dist/src/gateway/dashboard-copy.en.js +19 -37
  59. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  60. package/dist/src/gateway/dashboard-copy.js +8 -26
  61. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  62. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +8 -26
  63. package/dist/src/gateway/dashboard-copy.zh-CN.js +19 -37
  64. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  65. package/dist/src/gateway/dashboard-model.d.ts +8 -22
  66. package/dist/src/gateway/dashboard-model.js +44 -130
  67. package/dist/src/gateway/dashboard-model.js.map +1 -1
  68. package/dist/src/gateway/dashboard.d.ts +0 -6
  69. package/dist/src/gateway/dashboard.js +3 -7
  70. package/dist/src/gateway/dashboard.js.map +1 -1
  71. package/dist/src/gateway/live-dashboard-app/app.js +20 -34
  72. package/dist/src/gateway/live-dashboard-command.js +2 -2
  73. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  74. package/dist/src/gateway/live-dashboard-http.d.ts +1 -1
  75. package/dist/src/gateway/live-dashboard-http.js +2 -2
  76. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  77. package/dist/src/gateway/progress-watch-machine.d.ts +1 -37
  78. package/dist/src/gateway/progress-watch-machine.js +4 -15
  79. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  80. package/dist/src/gateway/providers.d.ts +58 -191
  81. package/dist/src/gateway/providers.js +637 -2434
  82. package/dist/src/gateway/providers.js.map +1 -1
  83. package/dist/src/gateway/server.d.ts +16 -25
  84. package/dist/src/gateway/server.js +165 -240
  85. package/dist/src/gateway/server.js.map +1 -1
  86. package/dist/src/gateway/service.d.ts +149 -427
  87. package/dist/src/gateway/service.js +1639 -5662
  88. package/dist/src/gateway/service.js.map +1 -1
  89. package/dist/src/gateway/state-v2-to-v3.d.ts +23 -0
  90. package/dist/src/gateway/state-v2-to-v3.js +994 -0
  91. package/dist/src/gateway/state-v2-to-v3.js.map +1 -0
  92. package/dist/src/gateway/store.d.ts +54 -309
  93. package/dist/src/gateway/store.js +1560 -3510
  94. package/dist/src/gateway/store.js.map +1 -1
  95. package/dist/src/gateway/types.d.ts +181 -225
  96. package/dist/src/gateway/types.js +115 -251
  97. package/dist/src/gateway/types.js.map +1 -1
  98. package/docs/CONFIGURATION.md +22 -9
  99. package/docs/CONFIGURATION.zh-CN.md +21 -9
  100. package/docs/DASHBOARD.md +10 -10
  101. package/docs/DASHBOARD.zh-CN.md +3 -3
  102. package/docs/DELIVERY.md +4 -4
  103. package/docs/DELIVERY.zh-CN.md +4 -4
  104. package/docs/GATEWAY-ARCHITECTURE.md +158 -187
  105. package/package.json +1 -1
  106. package/skills/embassy-peer/SKILL.md +28 -22
  107. package/dist/src/gateway/codex-registration-generation.d.ts +0 -5
  108. package/dist/src/gateway/codex-registration-generation.js +0 -19
  109. package/dist/src/gateway/codex-registration-generation.js.map +0 -1
  110. package/dist/src/gateway/codex-registration-succession.d.ts +0 -209
  111. package/dist/src/gateway/codex-registration-succession.js +0 -588
  112. package/dist/src/gateway/codex-registration-succession.js.map +0 -1
  113. package/dist/src/gateway/compatibility.d.ts +0 -22
  114. package/dist/src/gateway/compatibility.js +0 -23
  115. package/dist/src/gateway/compatibility.js.map +0 -1
  116. package/dist/src/gateway/delivery-machine.d.ts +0 -235
  117. package/dist/src/gateway/delivery-machine.js +0 -540
  118. package/dist/src/gateway/delivery-machine.js.map +0 -1
@@ -1,63 +1,46 @@
1
+ import { createHash } from "node:crypto";
1
2
  import path from "node:path";
2
3
  import { BridgeError } from "../errors.js";
3
4
  import { CLAUDE_PEER_COMPATIBILITY, ClaudePeerAdapter, } from "./claude-peer.js";
4
- import { CodexAppServerConnector, CodexConnectorError, } from "./codex-app-server.js";
5
- import { LocalCodexTransportError } from "./codex-local-transport.js";
6
5
  import { ClaudeNativeHelperSupervisor, } from "./claude-helper-supervisor.js";
7
6
  import { isDashboardLocale } from "./locale.js";
8
7
  import { composeProvenanceEnvelope } from "./provenance-envelope.js";
9
- import { gatewayRegistrationIngressPrefixes } from "./types.js";
10
8
  const LOCAL_HOST = "this-mac";
11
- const STABLE_CLAUDE_ENDPOINT_GENERATION = "claude_local_endpoint";
12
9
  const NATIVE_CLAUDE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
13
10
  const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
14
11
  const OPAQUE_ROUTE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
15
12
  const MAX_CLAUDE_PENDING = 1_024;
16
13
  const DEFAULT_CLAUDE_DISCOVERY_POLL_MS = 1_000;
17
- const CLAUDE_REJECTION_RECEIPT_RETRY_DELAYS_MS = [25, 100, 250];
18
- const MAX_CODEX_ROUTES = 128;
19
- const MAX_CODEX_CALLBACKS = 512;
20
- const MAX_TRANSIENT_REPLY_BYTES = 64 * 1024;
21
- const DEFAULT_CODEX_CLEANUP_TIMEOUT_MS = 5_000;
22
- const DEFAULT_CODEX_CLEANUP_POLL_MS = 25;
23
- const DEFAULT_CODEX_RECOVERY_INITIAL_MS = 250;
24
- const DEFAULT_CODEX_RECOVERY_MAX_MS = 5_000;
25
- const MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES = 3;
14
+ const DEFAULT_CODEX_OBSERVATION_POLL_MS = 15_000;
15
+ const DEFAULT_CODEX_OBSERVATION_TIMEOUT_MS = 15_000;
16
+ const DEFAULT_CODEX_OBSERVATION_BACKOFF_MAX_MS = 60_000;
17
+ const MAX_CODEX_OBSERVED_ROUTES = 128;
26
18
  const CLAUDE_CLEAN_PREWRITE_RETRY_CODES = new Set([
27
19
  "CLAUDE_PEER_TARGET_UNKNOWN",
28
20
  "CLAUDE_PEER_TARGET_STALE",
29
21
  "CLAUDE_PEER_TARGET_CHANGED",
30
22
  "CLAUDE_PEER_WORKSPACE_UNATTESTED",
31
23
  ]);
32
- const CODEX_TRUST_FAILURE_CODES = new Set([
33
- "HOME_INVALID",
34
- "MANAGED_CODEX_INVALID",
35
- "LOCAL_APP_SERVER_ENDPOINT_UNSAFE",
36
- "APP_SERVER_VERSION_MISMATCH",
24
+ const CODEX_CLEAN_RETRY_CODES = new Set([
25
+ "THREAD_NOT_OBSERVED",
26
+ "ROUTE_BUSY",
27
+ "APPROVAL_REQUIRED",
28
+ "MANAGED_CODEX_UNAVAILABLE",
29
+ "LOCAL_APP_SERVER_NOT_RUNNING",
37
30
  "ENDPOINT_GENERATION_CHANGED",
38
- "CLEANUP_FAILED",
39
- "FACTORY_CLOSED",
40
- "INVALID_CONFIGURATION",
41
- "CODEX_ROUTE_CLEANUP_FAILED",
42
- "CODEX_PROVIDER_CLEANUP_FAILED",
43
- "CODEX_PROVIDER_CLEANUP_AMBIGUOUS",
44
- "CODEX_PROVIDER_CLEANUP_TIMEOUT",
31
+ "SPAWN_FAILED",
32
+ "SPAWN_TIMEOUT",
33
+ "TRANSPORT_CONNECT_FAILED",
34
+ "REQUEST_TIMEOUT",
45
35
  ]);
46
- function isCodexTrustFailure(error) {
47
- return (typeof error === "object" &&
48
- error !== null &&
49
- "code" in error &&
50
- typeof error.code === "string" &&
51
- CODEX_TRUST_FAILURE_CODES.has(error.code));
52
- }
53
36
  function claudeCleanPrewriteResult(error) {
54
37
  if (error instanceof BridgeError &&
55
38
  error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
56
- return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
39
+ return { state: "expired", safeErrorCode: "MESSAGE_EXPIRED" };
57
40
  }
58
41
  if (error instanceof BridgeError &&
59
42
  CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
60
- return { state: "deferred", safeErrorCode: error.code };
43
+ return { state: "deferred", safeErrorCode: "ROUTE_BUSY" };
61
44
  }
62
45
  const systemCode = typeof error === "object" &&
63
46
  error !== null &&
@@ -78,6 +61,32 @@ function claudeCleanPrewriteResult(error) {
78
61
  : "CLAUDE_DISPATCH_PREWRITE_FAILED",
79
62
  };
80
63
  }
64
+ function claudePostAuthorizationResult(error) {
65
+ if (error instanceof BridgeError && error.recoverable) {
66
+ return {
67
+ state: "failed",
68
+ safeErrorCode: error.code === "CLAUDE_PEER_MESSAGE_EXPIRED"
69
+ ? "MESSAGE_EXPIRED"
70
+ : error.code,
71
+ };
72
+ }
73
+ return {
74
+ state: "ambiguous",
75
+ safeErrorCode: error instanceof BridgeError &&
76
+ /^[A-Z][A-Z0-9_]{0,95}$/.test(error.code)
77
+ ? error.code
78
+ : "CLAUDE_DISPATCH_OUTCOME_AMBIGUOUS",
79
+ };
80
+ }
81
+ async function cancelPreparedClaude(prepared) {
82
+ try {
83
+ await prepared.cancel();
84
+ }
85
+ catch {
86
+ // Cancellation is a no-write cleanup path. Its failure cannot turn a
87
+ // denied or uncertain authorization into permission to perform.
88
+ }
89
+ }
81
90
  function exactLocalHost(hostId) {
82
91
  if ((hostId ?? LOCAL_HOST) !== LOCAL_HOST) {
83
92
  throw new BridgeError("GATEWAY_REMOTE_PROVIDER_DISABLED", "This provider is restricted to the exact local host boundary.");
@@ -93,22 +102,9 @@ function positiveBounded(value, fallback, maximum) {
93
102
  }
94
103
  return resolved;
95
104
  }
96
- function strictDeadline(value, now) {
97
- const parsed = Date.parse(value);
98
- if (!Number.isFinite(parsed) ||
99
- new Date(parsed).toISOString() !== value ||
100
- parsed <= now) {
101
- return undefined;
102
- }
103
- return parsed;
104
- }
105
105
  function sameEndpoint(binding, identity) {
106
106
  return (binding.provider === identity.provider &&
107
- binding.hostId === identity.hostId &&
108
- binding.endpointGeneration === identity.endpointGeneration);
109
- }
110
- function callbackEndpoint(identity, routeHandle) {
111
- return { ...identity, routeHandle };
107
+ binding.hostId === identity.hostId);
112
108
  }
113
109
  function invokeCallback(operation) {
114
110
  try {
@@ -118,15 +114,8 @@ function invokeCallback(operation) {
118
114
  // Provider observation consumers cannot change a transport outcome.
119
115
  }
120
116
  }
121
- function validateAttestedClaudeRuntime(runtime) {
122
- validateClaudeRuntimePathEvidence(runtime);
123
- }
124
117
  function validateClaudeRuntimePathEvidence(runtime) {
125
- if ([
126
- runtime.claudeExecutable,
127
- runtime.sessionsDir,
128
- runtime.socketDir,
129
- ].some((value) => !path.isAbsolute(value) ||
118
+ if ([runtime.sessionsDir, runtime.socketDir].some((value) => !path.isAbsolute(value) ||
130
119
  path.normalize(value) !== value ||
131
120
  value.includes("\0"))) {
132
121
  throw new BridgeError("CLAUDE_RUNTIME_ATTESTATION_INVALID", "The local Claude provider requires bounded absolute runtime evidence.");
@@ -158,15 +147,6 @@ function unavailableClaudeRegistryObservation() {
158
147
  ]),
159
148
  });
160
149
  }
161
- function isClaudeRegistryAvailabilityError(error) {
162
- if (typeof error !== "object" || error === null || !("code" in error)) {
163
- return false;
164
- }
165
- return (error.code === "ENOENT" ||
166
- error.code === "EACCES" ||
167
- error.code === "EPERM" ||
168
- error.code === "UNSAFE_PEER_DIRECTORY");
169
- }
170
150
  export class LocalClaudeGatewayProvider {
171
151
  identity;
172
152
  protocol = "claude-peer";
@@ -175,48 +155,18 @@ export class LocalClaudeGatewayProvider {
175
155
  maxPending;
176
156
  discoveryPollMs;
177
157
  now;
178
- selectedStateRoots = new Map();
179
158
  nativeHelpers;
180
- nativeHelperSourceProvider;
181
159
  discovered = new Map();
160
+ /** Exact UUID selection plus its display-only observation bookkeeping. */
182
161
  selected = new Map();
183
- /** Exact live same-UID sessions observed through native inbound frames. */
184
- nativeInbound = new Map();
185
- /** Exact listener owner for every receipt capability handed to the service. */
186
- nativeInboundReceiptOwners = new Map();
187
- selectedObservations = new Map();
188
- /** Invalidates any discovery that overlaps a dispatch on this route. */
189
- selectedDispatchEpoch = new Map();
190
- /**
191
- * A dispatch invalidates the pre-dispatch registry observation. The first
192
- * subsequent successful discovery must therefore be published even when
193
- * the native peer completed between polls and is idle again.
194
- */
195
- selectedObservationDirty = new Set();
196
- pendingByProviderId = new Map();
197
- providerIdByGatewayId = new Map();
198
- /** Includes the pre-write reservation through exact terminal settlement. */
199
- pendingTargetByGatewayId = new Map();
200
- /** Provider-owned terminal rejections that never enter the service queue. */
201
- rejectedNativeInbound = new Map();
202
162
  discoveryInFlight;
203
163
  registryObservation;
204
164
  monitorTimer;
205
- /** Invocation-ordered registry mutations; the tail itself never rejects. */
206
- nativeCodexRegistrationTail = Promise.resolve();
207
165
  callbacks;
208
- activeNativeCodexListener;
209
- preparedNativeCodexListener;
210
- nativeCodexPreparationInFlight;
211
- retiredNativeCodexListener;
212
- /** Fences discovery callbacks and monitor restarts across listener rotation. */
213
- nativeCodexSuccessionFreeze;
214
- /** External registry trust/availability failures degrade Claude only. */
215
- unavailableCode;
216
166
  initialized = false;
217
167
  closed = false;
218
168
  constructor(options) {
219
- validateAttestedClaudeRuntime(options.runtime);
169
+ validateClaudeRuntimePathEvidence(options.runtime);
220
170
  if (options.locale !== undefined && !isDashboardLocale(options.locale)) {
221
171
  throw new BridgeError("DASHBOARD_LOCALE_UNSUPPORTED", "The Claude provider notice locale is unsupported.");
222
172
  }
@@ -224,22 +174,15 @@ export class LocalClaudeGatewayProvider {
224
174
  this.identity = {
225
175
  provider: "claude",
226
176
  hostId,
227
- endpointGeneration: STABLE_CLAUDE_ENDPOINT_GENERATION,
228
177
  };
229
178
  this.maxPending = positiveBounded(options.maxPendingMessages, MAX_CLAUDE_PENDING, 4_096);
230
179
  this.discoveryPollMs = positiveBounded(options.discoveryPollMs, DEFAULT_CLAUDE_DISCOVERY_POLL_MS, 30_000);
231
180
  this.now = options.now ?? Date.now;
232
- const protocolRuntime = {
233
- claudeExecutable: options.runtime.claudeExecutable,
234
- claudeCodeVersion: options.runtime.claudeCodeVersion,
235
- sessionsDir: options.runtime.sessionsDir,
236
- socketDir: options.runtime.socketDir,
237
- };
181
+ const protocolRuntime = options.runtime;
238
182
  this.peer = (options.peerFactory ??
239
183
  ((runtime, locale, deliveryNotices) => new ClaudePeerAdapter({
240
184
  sessionsDir: runtime.sessionsDir,
241
185
  socketDir: runtime.socketDir,
242
- attestedClaudeCodeVersion: runtime.claudeCodeVersion,
243
186
  locale,
244
187
  deliveryNotices,
245
188
  })))(protocolRuntime, options.locale ?? "en", options.deliveryNotices ?? "merged");
@@ -258,16 +201,10 @@ export class LocalClaudeGatewayProvider {
258
201
  ? {}
259
202
  : { factory: options.nativeHelpers.factory }),
260
203
  });
261
- this.nativeHelperSourceProvider = options.nativeHelperSourceProvider;
262
204
  }
263
205
  latestRegistryObservation() {
264
206
  return this.registryObservation;
265
207
  }
266
- activateEndpointGeneration(endpointGeneration) {
267
- if (endpointGeneration !== this.identity.endpointGeneration) {
268
- throw new BridgeError("CLAUDE_ENDPOINT_ACTIVATION_MISMATCH", "Only the exact current Claude endpoint generation can be activated.");
269
- }
270
- }
271
208
  async initialize(callbacks) {
272
209
  if (this.closed) {
273
210
  throw new BridgeError("CLAUDE_PROVIDER_CLOSED", "The local Claude provider is closed.");
@@ -276,63 +213,8 @@ export class LocalClaudeGatewayProvider {
276
213
  throw new BridgeError("CLAUDE_PROVIDER_ALREADY_INITIALIZED", "The local Claude provider can be initialized only once.");
277
214
  }
278
215
  this.callbacks = callbacks;
279
- if (this.nativeHelpers !== undefined) {
280
- this.initialized = true;
281
- return { health: "healthy" };
282
- }
283
- try {
284
- let listenerGeneration;
285
- const listener = await this.peer.listen({
286
- onMessage: async (message) => {
287
- if (listenerGeneration !== undefined) {
288
- await this.onListenerMessage(listenerGeneration, message);
289
- }
290
- },
291
- onReceipt: (event) => {
292
- if (listenerGeneration !== undefined) {
293
- this.onReceipt(listenerGeneration, event);
294
- }
295
- },
296
- onProtocolNotice: (notice) => {
297
- this.callbacks?.onProtocolNotice?.({ code: notice.code });
298
- },
299
- });
300
- listenerGeneration = {
301
- generation: listener.generation,
302
- listener,
303
- lifecycle: "active",
304
- registrationProvisional: false,
305
- provisionalIngressForwarded: false,
306
- inboundQuiesced: false,
307
- publicationAttempted: false,
308
- publicationUncertain: false,
309
- };
310
- if (this.closed) {
311
- await listener.close();
312
- throw new BridgeError("CLAUDE_PROVIDER_CLOSED", "The local Claude provider closed during initialization.");
313
- }
314
- this.activeNativeCodexListener = listenerGeneration;
315
- this.initialized = true;
316
- return { health: "healthy" };
317
- }
318
- catch (error) {
319
- if (error instanceof BridgeError &&
320
- error.code === "UNSAFE_PEER_DIRECTORY") {
321
- this.unavailableCode = "CLAUDE_REGISTRY_UNAVAILABLE";
322
- this.registryObservation = unavailableClaudeRegistryObservation();
323
- this.initialized = true;
324
- return {
325
- health: "degraded",
326
- safeErrorCode: "CLAUDE_REGISTRY_UNAVAILABLE",
327
- };
328
- }
329
- this.callbacks = undefined;
330
- if (error instanceof BridgeError &&
331
- error.code === "CLAUDE_PEER_CALLBACK_UNSAFE") {
332
- throw error;
333
- }
334
- throw new BridgeError("CLAUDE_CALLBACK_UNAVAILABLE", "The private Claude callback listener could not be established.");
335
- }
216
+ this.initialized = true;
217
+ return { health: "healthy" };
336
218
  }
337
219
  async discoverClaudePeers() {
338
220
  this.assertReady();
@@ -341,7 +223,18 @@ export class LocalClaudeGatewayProvider {
341
223
  async assertWorkspaceDisjoint(routeHandle, stateRoot) {
342
224
  this.assertReady();
343
225
  await this.peer.assertTargetWorkspaceDisjoint(routeHandle, stateRoot);
344
- this.selectedStateRoots.set(routeHandle, stateRoot);
226
+ const route = this.selected.get(routeHandle);
227
+ const alias = route?.alias ?? this.discovered.get(routeHandle)?.alias;
228
+ if (alias !== undefined) {
229
+ this.selected.set(routeHandle, {
230
+ alias,
231
+ dispatchEpoch: route?.dispatchEpoch ?? 0,
232
+ observationDirty: route?.observationDirty ?? false,
233
+ ...(route?.registrationId === undefined ? {} : { registrationId: route.registrationId }),
234
+ ...(route?.observation === undefined ? {} : { observation: route.observation }),
235
+ stateRoot,
236
+ });
237
+ }
345
238
  }
346
239
  async selectRoute(input) {
347
240
  this.assertReady();
@@ -349,17 +242,47 @@ export class LocalClaudeGatewayProvider {
349
242
  if (candidate === undefined || candidate.alias !== input.alias) {
350
243
  throw new BridgeError("CLAUDE_ROUTE_MISMATCH", "The selected Claude alias no longer matches that exact peer generation.");
351
244
  }
352
- this.selected.set(input.routeHandle, input.alias);
353
- this.selectedDispatchEpoch.set(input.routeHandle, this.selectedDispatchEpoch.get(input.routeHandle) ?? 0);
354
- this.selectedObservationDirty.delete(input.routeHandle);
355
- this.selectedObservations.set(input.routeHandle, `${candidate.state}:`);
245
+ const route = this.selected.get(input.routeHandle);
246
+ this.selected.set(input.routeHandle, {
247
+ alias: input.alias,
248
+ dispatchEpoch: route?.dispatchEpoch ?? 0,
249
+ observation: `${candidate.state}:`,
250
+ observationDirty: false,
251
+ ...(route?.registrationId === undefined ? {} : { registrationId: route.registrationId }),
252
+ ...(route?.stateRoot === undefined ? {} : { stateRoot: route.stateRoot }),
253
+ });
356
254
  this.scheduleClaudeMonitor();
357
255
  return { routeHandle: input.routeHandle, state: candidate.state };
358
256
  }
257
+ observeLogicalRoute(input) {
258
+ if (this.closed ||
259
+ !PUBLIC_ALIAS.test(input.alias) ||
260
+ !input.alias.endsWith(`@${this.identity.hostId}`) ||
261
+ !OPAQUE_ROUTE.test(input.routeHandle) ||
262
+ !OPAQUE_ROUTE.test(input.registrationId)) {
263
+ return;
264
+ }
265
+ const route = this.selected.get(input.routeHandle);
266
+ this.selected.set(input.routeHandle, {
267
+ alias: input.alias,
268
+ registrationId: input.registrationId,
269
+ dispatchEpoch: route?.dispatchEpoch ?? 0,
270
+ observationDirty: route?.observationDirty ?? false,
271
+ ...(route?.stateRoot === undefined ? {} : { stateRoot: route.stateRoot }),
272
+ ...(route?.observation === undefined ? {} : { observation: route.observation }),
273
+ });
274
+ }
275
+ forgetLogicalRoute(registrationId) {
276
+ for (const [routeHandle, route] of this.selected) {
277
+ if (route.registrationId === registrationId) {
278
+ this.selected.delete(routeHandle);
279
+ }
280
+ }
281
+ }
359
282
  async resolveReplyAddress(address) {
360
283
  this.assertReady();
361
284
  const resolved = await this.peer.resolveReplyAddress(address);
362
- const selectedAlias = this.selected.get(resolved.targetId);
285
+ const selectedAlias = this.selected.get(resolved.targetId)?.alias;
363
286
  if (selectedAlias === undefined ||
364
287
  resolved.kind !== "interactive" ||
365
288
  selectedAlias !== `${resolved.alias}@${this.identity.hostId}`) {
@@ -368,1219 +291,265 @@ export class LocalClaudeGatewayProvider {
368
291
  return { routeHandle: resolved.targetId };
369
292
  }
370
293
  async advertiseNativeSourcePeer(input) {
371
- if (this.nativeHelpers !== undefined) {
372
- this.assertReady();
373
- await this.nativeHelpers.advertise(input);
374
- return;
375
- }
376
- if (input.sourceProvider !== "codex" &&
377
- input.sourceProvider !== this.nativeHelperSourceProvider) {
378
- throw new BridgeError("CLAUDE_NATIVE_HELPER_UNAVAILABLE", "A non-Codex source requires a supervised native Claude helper.", true);
379
- }
380
- const registration = this.nativeCodexRegistration(input);
381
- await this.serializeNativeCodexRegistration(async () => {
382
- this.assertReady();
383
- const active = this.requireActiveNativeCodexListener();
384
- if (active.registration !== undefined &&
385
- active.registration.alias !== registration.alias) {
386
- throw new BridgeError("CODEX_PEER_ALREADY_ADVERTISED", "This listener generation already advertises another Codex peer.");
387
- }
388
- const installedProvisional = active.registration === undefined;
389
- if (installedProvisional) {
390
- active.registration = registration;
391
- active.registrationProvisional = true;
392
- active.provisionalIngressForwarded = false;
393
- }
394
- try {
395
- await active.listener.advertise(registration.name, registration.cwd);
396
- // Reassert exact ownership after publication. A preceding serialized
397
- // attempt may have removed its own clean provisional registration.
398
- active.registration = registration;
399
- active.registrationProvisional = false;
400
- active.provisionalIngressForwarded = false;
401
- }
402
- catch (error) {
403
- if (error instanceof BridgeError &&
404
- error.recoverable &&
405
- installedProvisional &&
406
- active.registration === registration &&
407
- !active.provisionalIngressForwarded) {
408
- delete active.registration;
409
- active.registrationProvisional = false;
410
- }
411
- throw error;
412
- }
413
- });
294
+ await this.helpers().advertise(input);
414
295
  }
415
- currentNativeCodexPeerGeneration(alias) {
416
- this.assertReady();
417
- if (this.nativeHelpers !== undefined) {
418
- return this.nativeHelpers.currentGeneration(alias);
419
- }
420
- const active = this.requireActiveNativeCodexListener();
421
- if (active.registration?.alias !== alias) {
422
- throw new BridgeError("CODEX_PEER_GENERATION_MISMATCH", "The requested Codex alias is not the active listener generation.");
423
- }
424
- return active.generation;
296
+ async unadvertiseNativeSourcePeer(alias) {
297
+ await this.helpers().unadvertise(alias);
425
298
  }
426
- /** Child-host initialization seam before its immutable advertisement exists. */
427
- currentUnadvertisedNativeCodexPeerGeneration() {
428
- this.assertReady();
429
- if (this.nativeHelpers !== undefined) {
430
- throw new BridgeError("CLAUDE_NATIVE_HELPER_NESTING_FORBIDDEN", "A supervised provider cannot itself act as a native helper host.");
431
- }
432
- const active = this.requireActiveNativeCodexListener();
433
- if (active.registration !== undefined) {
434
- throw new BridgeError("CODEX_PEER_ALREADY_ADVERTISED", "The native listener already owns an advertised Codex identity.");
435
- }
436
- return active.generation;
299
+ async updateNativeSourcePeerStatus(alias, status) {
300
+ await this.helpers().updateStatus(alias, status);
437
301
  }
438
- async prepareNativeCodexPeerGeneration(input) {
439
- this.assertReady();
440
- if (this.nativeHelpers !== undefined) {
441
- if (input.currentGeneration === undefined) {
442
- throw new BridgeError("CODEX_PEER_SUCCESSION_GENERATION_MISMATCH", "A supervised succession must name its exact current generation.");
443
- }
444
- await this.nativeHelpers.prepareGeneration({
445
- alias: input.alias,
446
- cwd: input.cwd,
447
- generation: input.generation,
448
- currentGeneration: input.currentGeneration,
449
- });
450
- return;
451
- }
452
- const active = this.requireActiveNativeCodexListener();
453
- if (input.currentGeneration !== undefined &&
454
- input.currentGeneration !== active.generation) {
455
- throw new BridgeError("CODEX_PEER_SUCCESSION_GENERATION_MISMATCH", "The prepared successor does not name the exact current generation.");
456
- }
457
- if (active.registration === undefined) {
458
- throw new BridgeError("CODEX_PEER_NOT_ADVERTISED", "A successor requires one active advertised Codex generation.");
302
+ async updateNativeInboundStatus(receiptHandle, status, diagnosticCode) {
303
+ await this.helpers().updateInboundStatus(receiptHandle, status, diagnosticCode);
304
+ }
305
+ async notifyNativeInboundProgress(receiptHandle, progress) {
306
+ await this.helpers().notifyInboundProgress(receiptHandle, progress);
307
+ }
308
+ async releaseNativeInboundReceipt(receiptHandle) {
309
+ return await this.helpers().releaseInboundReceipt(receiptHandle);
310
+ }
311
+ async dispatch(input) {
312
+ if (!this.initialized ||
313
+ this.closed ||
314
+ !sameEndpoint(input.binding, this.identity)) {
315
+ return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
459
316
  }
460
- if (input.generation === active.generation ||
461
- this.nativeCodexPreparationInFlight !== undefined ||
462
- this.preparedNativeCodexListener !== undefined ||
463
- this.retiredNativeCodexListener !== undefined) {
464
- throw new BridgeError("CODEX_PEER_SUCCESSION_CAPACITY", "Only one distinct prepared or retired Codex generation is allowed.");
317
+ if (input.authorization === "selected_route") {
318
+ const logical = this.selected.get(input.binding.routeHandle);
319
+ if (logical === undefined ||
320
+ logical.alias !== input.targetAlias ||
321
+ logical.registrationId !== input.binding.registrationId) {
322
+ return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
323
+ }
324
+ logical.dispatchEpoch += 1;
325
+ logical.observationDirty = true;
326
+ this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
465
327
  }
466
- const registration = this.nativeCodexRegistration({ ...input, sourceProvider: "codex" });
467
- this.nativeCodexPreparationInFlight = input.generation;
468
328
  let prepared;
469
329
  try {
470
- const listener = await this.peer.listenPrepared(input.generation, {
471
- onMessage: async (message) => {
472
- if (prepared !== undefined) {
473
- await this.onListenerMessage(prepared, message);
474
- }
475
- },
476
- onReceipt: (event) => {
477
- if (prepared !== undefined)
478
- this.onReceipt(prepared, event);
479
- },
330
+ const selected = this.selected.get(input.binding.routeHandle);
331
+ prepared = await this.helpers().prepareDispatch({
332
+ sourceAlias: input.sourceAlias,
333
+ sourceProvider: input.sourceProvider,
334
+ targetAlias: input.targetAlias,
335
+ conversationId: input.conversationId,
336
+ binding: input.binding,
337
+ authorization: input.authorization,
338
+ messageId: input.messageId,
339
+ text: input.text,
340
+ expectsReply: input.expectsReply,
341
+ deadlineAt: input.deadlineAt,
342
+ ...(selected?.alias === undefined ? {} : { selectedAlias: selected.alias }),
343
+ ...(input.authorization !== "selected_route" || selected?.stateRoot === undefined
344
+ ? {}
345
+ : { stateRoot: selected.stateRoot }),
346
+ ...(input.progressWatchActive === true ? { progressWatchActive: true } : {}),
480
347
  });
481
- prepared = {
482
- generation: listener.generation,
483
- listener,
484
- lifecycle: "prepared",
485
- registration,
486
- registrationProvisional: false,
487
- provisionalIngressForwarded: false,
488
- inboundQuiesced: true,
489
- publicationAttempted: false,
490
- publicationUncertain: false,
491
- };
492
- if (listener.generation !== input.generation || this.closed) {
493
- await listener.close().catch(() => undefined);
494
- throw new BridgeError("CLAUDE_CALLBACK_UNAVAILABLE", "The prepared Claude callback generation could not be established.");
495
- }
496
- this.preparedNativeCodexListener = prepared;
497
- }
498
- finally {
499
- if (this.nativeCodexPreparationInFlight === input.generation) {
500
- this.nativeCodexPreparationInFlight = undefined;
501
- }
502
- }
503
- }
504
- async quiesceNativeCodexPeerGeneration(generation) {
505
- this.assertReady();
506
- if (this.nativeHelpers !== undefined) {
507
- await this.nativeHelpers.quiesceGeneration(generation);
508
- return;
509
- }
510
- const active = this.requireNativeCodexListenerGeneration(this.activeNativeCodexListener, generation, "active");
511
- if (this.nativeCodexSuccessionFreeze !== undefined &&
512
- this.nativeCodexSuccessionFreeze !== generation) {
513
- throw new BridgeError("CODEX_PEER_SUCCESSION_GENERATION_MISMATCH", "Another native Codex generation already owns the succession freeze.");
514
- }
515
- this.nativeCodexSuccessionFreeze = generation;
516
- if (this.monitorTimer !== undefined) {
517
- clearTimeout(this.monitorTimer);
518
- this.monitorTimer = undefined;
519
- }
520
- try {
521
- await active.listener.quiesceInbound();
522
- active.inboundQuiesced = true;
523
348
  }
524
349
  catch (error) {
525
- if (this.nativeCodexSuccessionFreeze === generation) {
526
- this.nativeCodexSuccessionFreeze = undefined;
527
- }
528
- this.scheduleClaudeMonitor();
529
- throw error;
530
- }
531
- }
532
- async observeNativeCodexSuccessionBarrier(generation) {
533
- this.assertReady();
534
- if (this.nativeHelpers !== undefined) {
535
- return await this.nativeHelpers.observeBarrier(generation);
536
- }
537
- const active = this.activeNativeCodexListener;
538
- const activeGenerationMatched = active?.generation === generation;
539
- const pendingOutboundReceipts = this.providerIdByGatewayId.size;
540
- let pendingInboundReceipts = 0;
541
- for (const owner of this.nativeInboundReceiptOwners.values()) {
542
- if (owner.generation === generation)
543
- pendingInboundReceipts += 1;
544
- }
545
- let rejectedInboundSettlements = 0;
546
- for (const rejection of this.rejectedNativeInbound.values()) {
547
- if (rejection.listenerGeneration === generation) {
548
- rejectedInboundSettlements += 1;
350
+ if (error instanceof BridgeError &&
351
+ CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
352
+ this.emitClaudeRouteObservation(input.binding.routeHandle, "busy", error.code);
549
353
  }
354
+ return claudeCleanPrewriteResult(error);
550
355
  }
551
- const ingressQuiesced = activeGenerationMatched && active.inboundQuiesced;
552
- const monitorFrozen = activeGenerationMatched &&
553
- this.nativeCodexSuccessionFreeze === generation;
554
- const discoveryInFlight = this.discoveryInFlight !== undefined;
555
- return {
556
- generation,
557
- activeGenerationMatched,
558
- ingressQuiesced,
559
- monitorFrozen,
560
- discoveryInFlight,
561
- pendingOutboundReceipts,
562
- pendingInboundReceipts,
563
- rejectedInboundSettlements,
564
- clean: activeGenerationMatched &&
565
- ingressQuiesced &&
566
- monitorFrozen &&
567
- !discoveryInFlight &&
568
- pendingOutboundReceipts === 0 &&
569
- pendingInboundReceipts === 0 &&
570
- rejectedInboundSettlements === 0,
356
+ const evidence = {
357
+ attemptId: input.attemptId,
358
+ kind: "claude_mailbox",
359
+ bodyBytes: Buffer.byteLength(input.text, "utf8"),
360
+ bodySha256: createHash("sha256").update(input.text).digest("hex"),
361
+ frameBytes: prepared.frameBytes,
362
+ sha256: prepared.sha256,
571
363
  };
572
- }
573
- async publishPreparedNativeCodexPeer(input) {
574
- this.assertReady();
575
- if (this.nativeHelpers !== undefined) {
576
- return await this.nativeHelpers.publishPrepared(input);
577
- }
578
- const current = this.requireNativeCodexListenerGeneration(this.activeNativeCodexListener, input.currentGeneration, "active");
579
- const prepared = this.requireNativeCodexListenerGeneration(this.preparedNativeCodexListener, input.preparedGeneration, "prepared");
580
- if (!current.inboundQuiesced || prepared.registration === undefined) {
581
- throw new BridgeError("CODEX_PEER_SUCCESSION_NOT_QUIET", "The old listener must be quiesced before publication.");
582
- }
583
- if (!(await this.observeNativeCodexSuccessionBarrier(current.generation)).clean) {
584
- throw new BridgeError("CODEX_PEER_SUCCESSION_NOT_QUIET", "Native Codex succession requires a clean provider-wide barrier.", true);
364
+ let authorized;
365
+ try {
366
+ authorized = await input.authorizeWrite(evidence);
585
367
  }
586
- if (prepared.publicationOutcome === "published" ||
587
- prepared.publicationOutcome === "not_published") {
588
- return prepared.publicationOutcome;
368
+ catch {
369
+ await cancelPreparedClaude(prepared);
370
+ return {
371
+ state: "ambiguous",
372
+ safeErrorCode: "WRITE_AUTHORIZATION_UNCERTAIN",
373
+ };
589
374
  }
590
- if (prepared.publicationOperation !== undefined) {
591
- return await prepared.publicationOperation;
375
+ if (!authorized) {
376
+ await cancelPreparedClaude(prepared);
377
+ return {
378
+ state: "failed",
379
+ safeErrorCode: "WRITE_AUTHORIZATION_DENIED",
380
+ };
592
381
  }
593
- prepared.publicationAttempted = true;
594
- const operation = prepared.listener.publishReplacing(current.listener, prepared.registration.name, prepared.registration.cwd);
595
- prepared.publicationOperation = operation;
596
382
  try {
597
- const outcome = await operation;
598
- if (outcome === "published") {
599
- prepared.publicationOutcome = "published";
600
- return "published";
601
- }
602
- if (outcome === "unknown") {
603
- prepared.publicationUncertain = true;
604
- prepared.publicationOutcome = "unknown";
605
- return "unknown";
606
- }
607
- if (prepared.publicationUncertain) {
608
- prepared.publicationOutcome = "unknown";
609
- return "unknown";
383
+ // Authorization is the consent linearization point. Invoke the exact
384
+ // prepared mailbox write in the same continuation with no intervening
385
+ // await. Claude has no separate acceptance phase: confirmed mailbox
386
+ // completion is terminal delivered.
387
+ const operation = prepared.perform();
388
+ const result = await operation;
389
+ if (result.state === "deferred") {
390
+ return {
391
+ state: "ambiguous",
392
+ safeErrorCode: "CLAUDE_PREPARED_RESULT_INVALID",
393
+ };
610
394
  }
611
- prepared.publicationOutcome = "not_published";
612
- return "not_published";
395
+ return result.state === "expired"
396
+ ? { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" }
397
+ : result;
613
398
  }
614
399
  catch (error) {
615
- prepared.publicationUncertain = true;
616
- prepared.publicationOutcome = "unknown";
617
- throw error;
618
- }
619
- finally {
620
- if (prepared.publicationOperation === operation) {
621
- delete prepared.publicationOperation;
622
- }
400
+ return claudePostAuthorizationResult(error);
623
401
  }
624
402
  }
625
- async activatePreparedNativeCodexPeerGeneration(generation) {
626
- this.assertReady();
627
- if (this.nativeHelpers !== undefined) {
628
- await this.nativeHelpers.activatePrepared(generation);
629
- return;
630
- }
631
- const prepared = this.requireNativeCodexListenerGeneration(this.preparedNativeCodexListener, generation, "prepared");
632
- if (prepared.publicationOutcome !== "published" ||
633
- this.retiredNativeCodexListener !== undefined) {
634
- throw new BridgeError("CODEX_PEER_SUCCESSION_NOT_PUBLISHED", "Only a confirmed published generation can become active.");
635
- }
636
- const current = this.requireActiveNativeCodexListener();
637
- prepared.listener.grantSuccessionActivation();
638
- prepared.listener.resumeInbound();
639
- prepared.inboundQuiesced = false;
640
- current.lifecycle = "retired";
641
- prepared.lifecycle = "active";
642
- this.retiredNativeCodexListener = current;
643
- this.activeNativeCodexListener = prepared;
644
- this.preparedNativeCodexListener = undefined;
645
- if (this.nativeCodexSuccessionFreeze === current.generation) {
646
- this.nativeCodexSuccessionFreeze = undefined;
403
+ async releaseRoute(routeHandle) {
404
+ this.selected.delete(routeHandle);
405
+ this.discovered.delete(routeHandle);
406
+ if (this.selected.size === 0 && this.monitorTimer !== undefined) {
407
+ clearTimeout(this.monitorTimer);
408
+ this.monitorTimer = undefined;
647
409
  }
648
- await this.purgeNativeCodexPeerGenerationReplyCapabilities(current.generation);
649
- this.scheduleClaudeMonitor();
650
410
  }
651
- async cleanupPreparedNativeCodexPeerGeneration(generation) {
652
- this.assertReady();
653
- if (this.nativeHelpers !== undefined) {
654
- await this.nativeHelpers.cleanupPrepared(generation);
411
+ async close() {
412
+ if (this.closed)
655
413
  return;
414
+ this.closed = true;
415
+ try {
416
+ await this.nativeHelpers?.close();
417
+ await this.peer.close();
656
418
  }
657
- const prepared = this.requireNativeCodexListenerGeneration(this.preparedNativeCodexListener, generation, "prepared");
658
- if (prepared.publicationAttempted &&
659
- (prepared.publicationUncertain ||
660
- prepared.publicationOutcome !== "not_published")) {
661
- throw new BridgeError("CODEX_PEER_SUCCESSION_ROLLBACK_FORBIDDEN", "A possibly published Codex generation cannot be rolled back.");
419
+ finally {
420
+ this.selected.clear();
421
+ this.discovered.clear();
422
+ if (this.monitorTimer !== undefined)
423
+ clearTimeout(this.monitorTimer);
424
+ this.monitorTimer = undefined;
425
+ this.callbacks = undefined;
662
426
  }
663
- await prepared.listener.close();
664
- if (this.preparedNativeCodexListener === prepared) {
665
- this.preparedNativeCodexListener = undefined;
427
+ }
428
+ assertReady() {
429
+ if (!this.initialized || this.closed) {
430
+ throw new BridgeError("CLAUDE_PROVIDER_UNAVAILABLE", "The local Claude provider is not available.", true);
666
431
  }
667
432
  }
668
- async resumeNativeCodexPeerGeneration(generation) {
433
+ helpers() {
669
434
  this.assertReady();
670
- if (this.nativeHelpers !== undefined) {
671
- await this.nativeHelpers.resumeGeneration(generation);
672
- return;
435
+ if (this.nativeHelpers === undefined) {
436
+ throw new BridgeError("CLAUDE_NATIVE_HELPER_UNAVAILABLE", "The supervised native Claude helper is unavailable.", true);
673
437
  }
674
- const active = this.requireNativeCodexListenerGeneration(this.activeNativeCodexListener, generation, "active");
675
- if (this.preparedNativeCodexListener !== undefined) {
676
- throw new BridgeError("CODEX_PEER_SUCCESSION_CLEANUP_REQUIRED", "The prepared listener must be cleaned before resuming the old one.");
677
- }
678
- active.listener.resumeInbound();
679
- active.inboundQuiesced = false;
680
- if (this.nativeCodexSuccessionFreeze === generation) {
681
- this.nativeCodexSuccessionFreeze = undefined;
682
- }
683
- this.scheduleClaudeMonitor();
438
+ return this.nativeHelpers;
684
439
  }
685
- async rollbackPreparedNativeCodexPeerGeneration(input) {
686
- if (this.nativeHelpers !== undefined) {
687
- await this.nativeHelpers.rollbackPrepared(input);
688
- return;
440
+ async refreshClaudeDiscovery() {
441
+ if (this.discoveryInFlight !== undefined) {
442
+ return await this.discoveryInFlight;
689
443
  }
690
- await this.cleanupPreparedNativeCodexPeerGeneration(input.preparedGeneration);
691
- await this.resumeNativeCodexPeerGeneration(input.resumeGeneration);
692
- }
693
- async retireNativeCodexPeerGeneration(input) {
694
- this.assertReady();
695
- if (this.nativeHelpers !== undefined) {
696
- await this.nativeHelpers.retireGeneration(input);
697
- return;
444
+ const operation = this.refreshClaudeDiscoveryOnce();
445
+ this.discoveryInFlight = operation;
446
+ try {
447
+ return await operation;
698
448
  }
699
- const active = this.requireNativeCodexListenerGeneration(this.activeNativeCodexListener, input.protectedActiveGeneration, "active");
700
- const retired = this.requireNativeCodexListenerGeneration(this.retiredNativeCodexListener, input.retiredGeneration, "retired");
701
- if (active === retired) {
702
- throw new BridgeError("CODEX_PEER_SUCCESSION_GENERATION_MISMATCH", "The protected and retired listener generations must be distinct.");
449
+ finally {
450
+ if (this.discoveryInFlight === operation) {
451
+ this.discoveryInFlight = undefined;
452
+ }
703
453
  }
704
- const ownsPendingReceipt = [...this.nativeInboundReceiptOwners.values()].some((owner) => owner === retired);
705
- const ownsRejectedSettlement = [
706
- ...this.rejectedNativeInbound.values(),
707
- ].some((rejection) => rejection.listener === retired.listener);
708
- const ownsOutboundReceipt = [...this.pendingByProviderId.values()].some((pending) => pending.listener === retired.listener);
709
- if (ownsPendingReceipt || ownsRejectedSettlement || ownsOutboundReceipt) {
710
- throw new BridgeError("CODEX_PEER_SUCCESSION_NOT_QUIET", "A retired listener cannot close while it owns receipt capabilities.", true);
454
+ }
455
+ async refreshClaudeDiscoveryOnce() {
456
+ const dispatchEpochAtStart = new Map();
457
+ for (const [routeHandle, route] of this.selected) {
458
+ dispatchEpochAtStart.set(routeHandle, route.dispatchEpoch);
711
459
  }
712
- await retired.listener.close();
713
- if (this.retiredNativeCodexListener === retired) {
714
- this.retiredNativeCodexListener = undefined;
460
+ let discovery;
461
+ try {
462
+ discovery = await this.peer.discover();
715
463
  }
716
- }
717
- async purgeNativeCodexPeerGenerationReplyCapabilities(generation) {
718
- if (this.nativeHelpers !== undefined) {
719
- return await this.nativeHelpers.purgeGenerationReplies(generation);
464
+ catch (error) {
465
+ this.registryObservation = unavailableClaudeRegistryObservation();
466
+ throw error;
720
467
  }
721
- let purged = 0;
722
- for (const [routeHandle, route] of this.nativeInbound) {
723
- if (route.listenerGeneration !== generation)
468
+ const registry = claudeRegistryObservation(discovery);
469
+ this.registryObservation = registry;
470
+ this.discovered.clear();
471
+ const rows = [];
472
+ for (const peer of discovery.peers) {
473
+ if (peer.kind !== "interactive" ||
474
+ !NATIVE_CLAUDE_NAME.test(peer.alias)) {
475
+ continue;
476
+ }
477
+ const alias = `${peer.alias}@${this.identity.hostId}`;
478
+ if (!PUBLIC_ALIAS.test(alias))
724
479
  continue;
725
- this.nativeInbound.delete(routeHandle);
726
- purged += 1;
480
+ const row = {
481
+ alias,
482
+ routeHandle: peer.targetId,
483
+ kind: "interactive",
484
+ state: claudeRouteState(peer),
485
+ };
486
+ this.discovered.set(peer.targetId, row);
487
+ rows.push(row);
727
488
  }
728
- return purged;
729
- }
730
- async unadvertiseNativeSourcePeer(alias) {
731
- if (this.nativeHelpers !== undefined) {
732
- await this.nativeHelpers.unadvertise(alias);
733
- return;
489
+ for (const [routeHandle, selected] of this.selected) {
490
+ const row = this.discovered.get(routeHandle);
491
+ if (row === undefined) {
492
+ this.emitClaudeRouteObservation(routeHandle, "busy", "CLAUDE_PEER_NOT_OBSERVED", true);
493
+ }
494
+ else {
495
+ if (!discovery.truncated && row.alias !== selected.alias) {
496
+ // A Claude rename changes only the live name index. The native
497
+ // session UUID remains the selected logical route. An incomplete
498
+ // scan is display evidence only and cannot rename dispatch authority.
499
+ selected.alias = row.alias;
500
+ }
501
+ if (row.state === "idle" &&
502
+ (dispatchEpochAtStart.get(routeHandle) !==
503
+ selected.dispatchEpoch)) {
504
+ // Never publish an idle sample from a discovery that overlapped the
505
+ // exact mailbox write. A wholly post-terminal discovery may clear
506
+ // the dirty epoch.
507
+ continue;
508
+ }
509
+ this.emitClaudeRouteObservation(routeHandle, row.state, undefined, true);
510
+ }
734
511
  }
735
- await this.serializeNativeCodexRegistration(async () => {
736
- const active = this.activeNativeCodexListener;
737
- if (active?.registration?.alias !== alias)
738
- return;
739
- await active.listener.unadvertise(active.registration.name);
740
- await this.purgeNativeCodexPeerGenerationReplyCapabilities(active.generation);
741
- delete active.registration;
742
- active.registrationProvisional = false;
743
- active.provisionalIngressForwarded = false;
744
- });
512
+ return { peers: rows, complete: !discovery.truncated, registry };
745
513
  }
746
- async updateNativeSourcePeerStatus(alias, status) {
747
- if (this.nativeHelpers !== undefined) {
748
- await this.nativeHelpers.updateStatus(alias, status);
514
+ emitClaudeRouteObservation(routeHandle, state, safeErrorCode, authoritative = false) {
515
+ const signature = `${state}:${safeErrorCode ?? ""}`;
516
+ const selected = this.selected.get(routeHandle);
517
+ const mustPublish = authoritative && selected?.observationDirty === true;
518
+ if (selected !== undefined && mustPublish)
519
+ selected.observationDirty = false;
520
+ if (!mustPublish && selected?.observation === signature) {
749
521
  return;
750
522
  }
751
- const active = this.activeNativeCodexListener;
752
- if (active?.registration?.alias !== alias)
523
+ if (selected !== undefined)
524
+ selected.observation = signature;
525
+ const callbacks = this.callbacks;
526
+ const registrationId = selected?.registrationId;
527
+ if (callbacks === undefined ||
528
+ this.closed ||
529
+ registrationId === undefined) {
753
530
  return;
754
- await active.listener.updateAdvertisedStatus(status);
531
+ }
532
+ invokeCallback(() => callbacks.onRouteState({
533
+ route: {
534
+ ...this.identity,
535
+ routeHandle,
536
+ registrationId,
537
+ },
538
+ observedAt: new Date(this.now()).toISOString(),
539
+ state,
540
+ ...(safeErrorCode === undefined ? {} : { safeErrorCode }),
541
+ }));
755
542
  }
756
- async updateNativeInboundStatus(receiptHandle, status, diagnosticCode) {
757
- if (this.nativeHelpers !== undefined) {
758
- await this.nativeHelpers.updateInboundStatus(receiptHandle, status, diagnosticCode);
759
- return;
760
- }
761
- const owner = this.requireNativeInboundReceiptOwner(receiptHandle);
762
- try {
763
- await owner.listener.acknowledge(receiptHandle, status, diagnosticCode === undefined ? undefined : { code: diagnosticCode });
764
- if (status !== "held") {
765
- this.nativeInboundReceiptOwners.delete(receiptHandle);
766
- }
767
- }
768
- catch (error) {
769
- if (status !== "held" &&
770
- (!(error instanceof BridgeError) || !error.recoverable)) {
771
- this.nativeInboundReceiptOwners.delete(receiptHandle);
772
- }
773
- throw error;
774
- }
775
- }
776
- async notifyNativeInboundProgress(receiptHandle, progress) {
777
- if (this.nativeHelpers !== undefined) {
778
- await this.nativeHelpers.notifyInboundProgress(receiptHandle, progress);
779
- return;
780
- }
781
- const owner = this.requireNativeInboundReceiptOwner(receiptHandle);
782
- await owner.listener.notifyInboundProgress(receiptHandle, progress);
783
- }
784
- async releaseNativeInboundReceipt(receiptHandle) {
785
- if (this.nativeHelpers !== undefined) {
786
- return await this.nativeHelpers.releaseInboundReceipt(receiptHandle);
787
- }
788
- const owner = this.nativeInboundReceiptOwners.get(receiptHandle);
789
- if (owner === undefined)
790
- return false;
791
- this.nativeInboundReceiptOwners.delete(receiptHandle);
792
- return owner.listener.releaseInboundReceipt(receiptHandle);
793
- }
794
- async quiesceNativeInbound() {
795
- if (this.closed)
796
- return;
797
- if (this.nativeHelpers !== undefined) {
798
- await this.nativeHelpers.quiesceAll();
799
- return;
800
- }
801
- const active = this.activeNativeCodexListener;
802
- if (active === undefined)
803
- return;
804
- await active.listener.quiesceInbound();
805
- active.inboundQuiesced = true;
806
- }
807
- async dispatch(input) {
808
- if (this.unavailableCode !== undefined) {
809
- return {
810
- state: "failed",
811
- safeErrorCode: this.unavailableCode,
812
- };
813
- }
814
- if (this.nativeHelpers !== undefined) {
815
- if (input.sourceAlias === undefined ||
816
- !this.initialized ||
817
- this.closed ||
818
- !sameEndpoint(input.binding, this.identity)) {
819
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
820
- }
821
- const selectedAlias = this.selected.get(input.binding.routeHandle);
822
- const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
823
- if (input.authorization === "selected_route" &&
824
- (selectedAlias === undefined || stateRoot === undefined)) {
825
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
826
- }
827
- if (input.authorization === "selected_route") {
828
- this.selectedDispatchEpoch.set(input.binding.routeHandle, (this.selectedDispatchEpoch.get(input.binding.routeHandle) ?? 0) + 1);
829
- this.selectedObservationDirty.add(input.binding.routeHandle);
830
- this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
831
- }
832
- try {
833
- const sourceAlias = input.sourceAlias;
834
- const result = await this.nativeHelpers.dispatch({
835
- ...input,
836
- sourceAlias,
837
- ...(selectedAlias === undefined ? {} : { selectedAlias }),
838
- ...(stateRoot === undefined ? {} : { stateRoot }),
839
- });
840
- if (input.authorization === "selected_route" &&
841
- result.state === "pending") {
842
- this.selectedObservationDirty.delete(input.binding.routeHandle);
843
- this.emitClaudeRouteObservation(input.binding.routeHandle, "idle", undefined, true);
844
- }
845
- return result;
846
- }
847
- catch (error) {
848
- if (error instanceof BridgeError && error.recoverable) {
849
- return {
850
- state: "failed",
851
- safeErrorCode: "CLAUDE_NATIVE_HELPER_UNAVAILABLE",
852
- };
853
- }
854
- return {
855
- state: "ambiguous",
856
- safeErrorCode: "CLAUDE_NATIVE_HELPER_OUTCOME_UNKNOWN",
857
- };
858
- }
859
- }
860
- const activeListener = this.activeNativeCodexListener;
861
- if (!this.initialized ||
862
- this.closed ||
863
- activeListener === undefined ||
864
- activeListener.inboundQuiesced ||
865
- this.nativeCodexSuccessionFreeze !== undefined ||
866
- !sameEndpoint(input.binding, this.identity)) {
867
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
868
- }
869
- if (input.authorization === "native_reply") {
870
- const observedRoute = this.nativeInbound.get(input.binding.routeHandle);
871
- if (observedRoute === undefined ||
872
- observedRoute.listenerGeneration !== activeListener.generation) {
873
- this.nativeInbound.delete(input.binding.routeHandle);
874
- return { state: "failed", safeErrorCode: "CLAUDE_NATIVE_REPLY_STALE" };
875
- }
876
- }
877
- else {
878
- const selectedAlias = this.selected.get(input.binding.routeHandle);
879
- const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
880
- if (selectedAlias === undefined ||
881
- stateRoot === undefined) {
882
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
883
- }
884
- }
885
- if (activeListener.registration?.alias !== input.sourceAlias ||
886
- activeListener.registration.sourceProvider !== input.sourceProvider) {
887
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
888
- }
889
- if (this.activeNativeCodexListener !== activeListener ||
890
- activeListener.inboundQuiesced ||
891
- this.nativeCodexSuccessionFreeze !== undefined) {
892
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
893
- }
894
- const deadline = strictDeadline(input.deadlineAt, this.now());
895
- if (deadline === undefined) {
896
- return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
897
- }
898
- if (this.providerIdByGatewayId.size >= this.maxPending ||
899
- this.providerIdByGatewayId.has(input.messageId)) {
900
- return { state: "failed", safeErrorCode: "CLAUDE_RECEIPT_CAPACITY" };
901
- }
902
- let content;
903
- try {
904
- content = composeProvenanceEnvelope({
905
- sourceProvider: input.sourceProvider,
906
- recipientProvider: this.identity.provider,
907
- sourceAlias: input.sourceAlias,
908
- targetAlias: input.targetAlias,
909
- conversationId: input.conversationId,
910
- body: input.text,
911
- ...(input.progressWatchActive === true
912
- ? { progressWatchActive: true }
913
- : {}),
914
- });
915
- }
916
- catch (error) {
917
- if (error instanceof BridgeError &&
918
- (error.code === "PROVENANCE_ENVELOPE_INVALID" ||
919
- error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")) {
920
- return { state: "failed", safeErrorCode: error.code };
921
- }
922
- throw error;
923
- }
924
- if (input.authorization === "selected_route") {
925
- const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
926
- if (stateRoot === undefined) {
927
- return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
928
- }
929
- this.selectedDispatchEpoch.set(input.binding.routeHandle, (this.selectedDispatchEpoch.get(input.binding.routeHandle) ?? 0) + 1);
930
- // The exact workspace attestation is refreshed before every send. A
931
- // clean prewrite failure remains retryable within the message deadline;
932
- // retry timing does not depend on the model turn's observed state.
933
- this.selectedObservationDirty.add(input.binding.routeHandle);
934
- this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
935
- try {
936
- await this.peer.assertTargetWorkspaceDisjoint(input.binding.routeHandle, stateRoot);
937
- }
938
- catch (error) {
939
- return claudeCleanPrewriteResult(error);
940
- }
941
- }
942
- this.providerIdByGatewayId.set(input.messageId, "");
943
- this.pendingTargetByGatewayId.set(input.messageId, input.binding.routeHandle);
944
- let providerId;
945
- let trackingFailed = false;
946
- let terminalDuringSend = false;
947
- const onTransportStatus = (event) => {
948
- if (providerId === undefined) {
949
- providerId = event.messageId;
950
- trackingFailed = !this.trackClaudeMessage(event.messageId, input.messageId, input.binding.routeHandle, deadline, activeListener);
951
- }
952
- if (trackingFailed)
953
- return;
954
- if (providerId !== event.messageId) {
955
- terminalDuringSend = true;
956
- this.finishClaudeMessage(providerId, "ambiguous", "CLAUDE_RECEIPT_ID_MISMATCH");
957
- return;
958
- }
959
- if (event.status === "transport_written") {
960
- this.recordClaudeWriteEvidence(event.messageId, "transport_written");
961
- this.emitDelivery({
962
- messageId: input.messageId,
963
- state: "transport_written",
964
- });
965
- }
966
- else if (event.status === "write_started" ||
967
- event.status === "ambiguous") {
968
- const newlyUncertain = this.recordClaudeWriteEvidence(event.messageId, "transport_uncertain");
969
- if (newlyUncertain) {
970
- this.emitDelivery({
971
- messageId: input.messageId,
972
- state: "transport_uncertain",
973
- safeErrorCode: "CLAUDE_TRANSPORT_OUTCOME_UNCERTAIN",
974
- });
975
- }
976
- }
977
- else if (event.status === "not_written") {
978
- terminalDuringSend = true;
979
- this.finishClaudeMessage(event.messageId, "failed", "CLAUDE_TRANSPORT_NOT_WRITTEN");
980
- }
981
- // A post-write ambiguity remains tracked. The exact receipt or the
982
- // listener's bounded timeout is authoritative and may settle it later.
983
- };
984
- try {
985
- const result = await this.peer.send(input.binding.routeHandle, content, {
986
- listener: activeListener.listener,
987
- receiptDeadlineAt: deadline,
988
- onTransportStatus,
989
- });
990
- if (providerId === undefined ||
991
- providerId !== result.messageId ||
992
- result.receiptStatus !== "pending" ||
993
- trackingFailed) {
994
- if (trackingFailed) {
995
- this.abandonClaudeTracking(input.messageId, providerId);
996
- this.emitDelivery({
997
- messageId: input.messageId,
998
- state: "ambiguous",
999
- safeErrorCode: "CLAUDE_RECEIPT_TRACKING_FAILED",
1000
- });
1001
- return { state: "pending" };
1002
- }
1003
- if (providerId !== undefined) {
1004
- this.finishClaudeMessage(providerId, "ambiguous", "CLAUDE_RECEIPT_TRACKING_FAILED");
1005
- }
1006
- else {
1007
- this.providerIdByGatewayId.delete(input.messageId);
1008
- this.pendingTargetByGatewayId.delete(input.messageId);
1009
- }
1010
- return { state: "ambiguous", safeErrorCode: "CLAUDE_RECEIPT_TRACKING_FAILED" };
1011
- }
1012
- if (input.authorization === "selected_route") {
1013
- this.selectedObservationDirty.delete(input.binding.routeHandle);
1014
- this.emitClaudeRouteObservation(input.binding.routeHandle, "idle", undefined, true);
1015
- }
1016
- return { state: "pending" };
1017
- }
1018
- catch (error) {
1019
- if (trackingFailed) {
1020
- this.abandonClaudeTracking(input.messageId, providerId);
1021
- this.emitDelivery({
1022
- messageId: input.messageId,
1023
- state: "ambiguous",
1024
- safeErrorCode: "CLAUDE_RECEIPT_TRACKING_FAILED",
1025
- });
1026
- return { state: "pending" };
1027
- }
1028
- if (terminalDuringSend)
1029
- return { state: "pending" };
1030
- if (providerId !== undefined) {
1031
- if (error instanceof BridgeError &&
1032
- error.code === "CLAUDE_PEER_WRITE_AMBIGUOUS") {
1033
- return { state: "pending" };
1034
- }
1035
- this.finishClaudeMessage(providerId, "ambiguous", "CLAUDE_DISPATCH_OUTCOME_AMBIGUOUS");
1036
- return { state: "pending" };
1037
- }
1038
- this.providerIdByGatewayId.delete(input.messageId);
1039
- this.pendingTargetByGatewayId.delete(input.messageId);
1040
- return claudeCleanPrewriteResult(error);
1041
- }
1042
- }
1043
- async releaseRoute(routeHandle) {
1044
- this.selected.delete(routeHandle);
1045
- this.selectedDispatchEpoch.delete(routeHandle);
1046
- this.selectedObservationDirty.delete(routeHandle);
1047
- this.selectedObservations.delete(routeHandle);
1048
- this.discovered.delete(routeHandle);
1049
- this.selectedStateRoots.delete(routeHandle);
1050
- if (this.nativeHelpers !== undefined) {
1051
- await this.nativeHelpers.releaseRoute(routeHandle);
1052
- }
1053
- if (this.selected.size === 0 && this.monitorTimer !== undefined) {
1054
- clearTimeout(this.monitorTimer);
1055
- this.monitorTimer = undefined;
1056
- }
1057
- }
1058
- async close() {
1059
- if (this.closed)
1060
- return;
1061
- this.closed = true;
1062
- for (const rejection of this.rejectedNativeInbound.values()) {
1063
- this.releaseRejectedNativeInbound(rejection);
1064
- }
1065
- this.rejectedNativeInbound.clear();
1066
- try {
1067
- await this.nativeHelpers?.close();
1068
- await this.peer.close();
1069
- }
1070
- finally {
1071
- for (const pending of this.pendingByProviderId.values()) {
1072
- clearTimeout(pending.timer);
1073
- }
1074
- this.pendingByProviderId.clear();
1075
- this.providerIdByGatewayId.clear();
1076
- this.pendingTargetByGatewayId.clear();
1077
- this.selected.clear();
1078
- this.selectedStateRoots.clear();
1079
- this.nativeInbound.clear();
1080
- this.nativeInboundReceiptOwners.clear();
1081
- this.selectedDispatchEpoch.clear();
1082
- this.selectedObservationDirty.clear();
1083
- this.selectedObservations.clear();
1084
- this.discovered.clear();
1085
- if (this.monitorTimer !== undefined)
1086
- clearTimeout(this.monitorTimer);
1087
- this.monitorTimer = undefined;
1088
- this.activeNativeCodexListener = undefined;
1089
- this.preparedNativeCodexListener = undefined;
1090
- this.retiredNativeCodexListener = undefined;
1091
- this.nativeCodexPreparationInFlight = undefined;
1092
- this.nativeCodexSuccessionFreeze = undefined;
1093
- this.callbacks = undefined;
1094
- }
1095
- }
1096
- assertReady() {
1097
- if (!this.initialized || this.closed || this.unavailableCode !== undefined) {
1098
- throw new BridgeError("CLAUDE_PROVIDER_UNAVAILABLE", "The local Claude provider is not available.", true);
1099
- }
1100
- }
1101
- serializeNativeCodexRegistration(operation) {
1102
- const queued = this.nativeCodexRegistrationTail.then(operation);
1103
- this.nativeCodexRegistrationTail = queued.then(() => undefined, () => undefined);
1104
- return queued;
1105
- }
1106
- nativeCodexRegistration(input) {
1107
- const suffix = `@${this.identity.hostId}`;
1108
- if (!input.alias.endsWith(suffix)) {
1109
- throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "The native Codex peer alias must target this host.");
1110
- }
1111
- const name = input.alias.slice(0, -suffix.length);
1112
- const prefix = gatewayRegistrationIngressPrefixes[input.sourceProvider];
1113
- if (prefix === undefined || !name.startsWith(prefix) || !NATIVE_CLAUDE_NAME.test(name)) {
1114
- throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "The native source peer alias must use its provider prefix.");
1115
- }
1116
- if (!path.isAbsolute(input.cwd) || input.cwd.includes("\0")) {
1117
- throw new BridgeError("INVALID_CODEX_PEER_CWD", "A native Codex peer requires an absolute working directory.");
1118
- }
1119
- return { alias: input.alias, sourceProvider: input.sourceProvider, cwd: input.cwd, name };
1120
- }
1121
- requireActiveNativeCodexListener() {
1122
- this.assertReady();
1123
- const active = this.activeNativeCodexListener;
1124
- if (active === undefined || active.lifecycle !== "active") {
1125
- throw new BridgeError("CLAUDE_CALLBACK_UNAVAILABLE", "The private Claude callback listener is unavailable.", true);
1126
- }
1127
- return active;
1128
- }
1129
- requireNativeCodexListenerGeneration(listenerGeneration, generation, lifecycle) {
1130
- if (listenerGeneration === undefined ||
1131
- listenerGeneration.generation !== generation ||
1132
- listenerGeneration.listener.generation !== generation ||
1133
- listenerGeneration.lifecycle !== lifecycle) {
1134
- throw new BridgeError("CODEX_PEER_SUCCESSION_GENERATION_MISMATCH", "The requested native Codex listener generation is not the exact lifecycle owner.");
1135
- }
1136
- return listenerGeneration;
1137
- }
1138
- requireNativeInboundReceiptOwner(receiptHandle) {
1139
- this.assertReady();
1140
- const owner = this.nativeInboundReceiptOwners.get(receiptHandle);
1141
- if (owner === undefined || !this.ownsNativeCodexListener(owner)) {
1142
- throw new BridgeError("CLAUDE_PEER_RECEIPT_UNKNOWN", "The native Claude peer receipt is unknown or already settled.");
1143
- }
1144
- return owner;
1145
- }
1146
- ownsNativeCodexListener(listenerGeneration) {
1147
- return (this.activeNativeCodexListener === listenerGeneration ||
1148
- this.preparedNativeCodexListener === listenerGeneration ||
1149
- this.retiredNativeCodexListener === listenerGeneration);
1150
- }
1151
- async onListenerMessage(listenerGeneration, message) {
1152
- const reject = async (diagnosticCode) => {
1153
- if (message.receiptHandle !== undefined) {
1154
- await this.rejectNativeInboundReceipt(listenerGeneration, message.receiptHandle, diagnosticCode);
1155
- }
1156
- };
1157
- if (this.unavailableCode !== undefined ||
1158
- this.closed ||
1159
- listenerGeneration.listener.closed ||
1160
- !this.ownsNativeCodexListener(listenerGeneration)) {
1161
- try {
1162
- if (message.receiptHandle !== undefined) {
1163
- listenerGeneration.listener.releaseInboundReceipt(message.receiptHandle);
1164
- }
1165
- }
1166
- catch {
1167
- // A detached generation has no safe callback or settlement authority.
1168
- }
1169
- return;
1170
- }
1171
- if (this.activeNativeCodexListener !== listenerGeneration ||
1172
- listenerGeneration.lifecycle !== "active" ||
1173
- listenerGeneration.inboundQuiesced ||
1174
- this.nativeCodexSuccessionFreeze !== undefined) {
1175
- await reject("CLAUDE_NATIVE_GENERATION_STALE");
1176
- return;
1177
- }
1178
- const registrationAtIngress = listenerGeneration.registration;
1179
- if (message.sourceTargetId === undefined ||
1180
- message.sourceAlias === undefined ||
1181
- !message.replySupported) {
1182
- await reject("CLAUDE_SOURCE_ROUTE_INVALID");
1183
- return;
1184
- }
1185
- const sourceTargetId = message.sourceTargetId;
1186
- let sourceAlias;
1187
- try {
1188
- await this.refreshClaudeDiscovery();
1189
- const current = this.discovered.get(sourceTargetId);
1190
- const expectedAlias = `${message.sourceAlias}@${this.identity.hostId}`;
1191
- if (current?.alias === expectedAlias)
1192
- sourceAlias = expectedAlias;
1193
- }
1194
- catch {
1195
- sourceAlias = undefined;
1196
- }
1197
- // Discovery is asynchronous. Recheck exact generation ownership before
1198
- // giving the body or its receipt capability to a service callback.
1199
- if (this.activeNativeCodexListener !== listenerGeneration ||
1200
- listenerGeneration.lifecycle !== "active" ||
1201
- listenerGeneration.inboundQuiesced ||
1202
- this.nativeCodexSuccessionFreeze !== undefined ||
1203
- listenerGeneration.registration !== registrationAtIngress) {
1204
- await reject("CLAUDE_NATIVE_GENERATION_STALE");
1205
- return;
1206
- }
1207
- if (sourceAlias === undefined) {
1208
- await reject("CLAUDE_SOURCE_ROUTE_STALE");
1209
- return;
1210
- }
1211
- const callbacks = this.callbacks;
1212
- const registration = registrationAtIngress;
1213
- if (registration !== undefined) {
1214
- if (callbacks?.onClaudeMessage === undefined ||
1215
- (!this.nativeInbound.has(sourceTargetId) &&
1216
- this.nativeInbound.size >= this.maxPending) ||
1217
- (message.receiptHandle !== undefined &&
1218
- !this.nativeInboundReceiptOwners.has(message.receiptHandle) &&
1219
- this.nativeInboundReceiptOwners.size >= this.maxPending)) {
1220
- await reject("CLAUDE_NATIVE_INGRESS_CAPACITY");
1221
- return;
1222
- }
1223
- if (message.receiptHandle !== undefined &&
1224
- this.nativeInboundReceiptOwners.has(message.receiptHandle)) {
1225
- await reject("CLAUDE_NATIVE_RECEIPT_COLLISION");
1226
- return;
1227
- }
1228
- this.nativeInbound.set(sourceTargetId, {
1229
- alias: sourceAlias,
1230
- listenerGeneration: listenerGeneration.generation,
1231
- });
1232
- if (message.receiptHandle !== undefined) {
1233
- this.nativeInboundReceiptOwners.set(message.receiptHandle, listenerGeneration);
1234
- }
1235
- if (listenerGeneration.registrationProvisional) {
1236
- listenerGeneration.provisionalIngressForwarded = true;
1237
- }
1238
- invokeCallback(() => callbacks.onClaudeMessage?.({
1239
- endpoint: callbackEndpoint(this.identity, sourceTargetId),
1240
- sourceAlias,
1241
- targetAlias: registration.alias,
1242
- text: message.content,
1243
- ...(message.receiptHandle === undefined
1244
- ? {}
1245
- : { receiptHandle: message.receiptHandle }),
1246
- }));
1247
- return;
1248
- }
1249
- if (callbacks !== undefined && this.selected.get(sourceTargetId) === sourceAlias) {
1250
- invokeCallback(() => callbacks.onClaudeReply({
1251
- endpoint: callbackEndpoint(this.identity, sourceTargetId),
1252
- text: message.content,
1253
- }));
1254
- return;
1255
- }
1256
- await reject("CLAUDE_ROUTE_UNAVAILABLE");
1257
- }
1258
- rejectNativeInboundReceipt(listenerGeneration, receiptHandle, diagnosticCode) {
1259
- const rejectionKey = `${listenerGeneration.generation}\0${receiptHandle}`;
1260
- const existing = this.rejectedNativeInbound.get(rejectionKey);
1261
- if (existing !== undefined)
1262
- return existing.operation;
1263
- const listener = listenerGeneration.listener;
1264
- if (this.closed ||
1265
- listener.closed ||
1266
- !this.ownsNativeCodexListener(listenerGeneration)) {
1267
- try {
1268
- listener.releaseInboundReceipt(receiptHandle);
1269
- }
1270
- catch {
1271
- // A missing/closing listener leaves no safe terminal write path.
1272
- }
1273
- return Promise.resolve();
1274
- }
1275
- const rejection = {
1276
- receiptHandle,
1277
- listener,
1278
- listenerGeneration: listenerGeneration.generation,
1279
- released: false,
1280
- operation: Promise.resolve(),
1281
- };
1282
- this.rejectedNativeInbound.set(rejectionKey, rejection);
1283
- rejection.operation = this.settleRejectedNativeInbound(rejection, diagnosticCode).finally(() => {
1284
- if (this.rejectedNativeInbound.get(rejectionKey) === rejection) {
1285
- this.rejectedNativeInbound.delete(rejectionKey);
1286
- }
1287
- });
1288
- return rejection.operation;
1289
- }
1290
- async settleRejectedNativeInbound(rejection, diagnosticCode) {
1291
- for (let attempt = 0; attempt <= CLAUDE_REJECTION_RECEIPT_RETRY_DELAYS_MS.length; attempt += 1) {
1292
- if (this.closed ||
1293
- rejection.released ||
1294
- rejection.listener.closed ||
1295
- !this.ownsNativeCodexListenerGeneration(rejection.listenerGeneration, rejection.listener)) {
1296
- this.releaseRejectedNativeInbound(rejection);
1297
- return;
1298
- }
1299
- try {
1300
- await rejection.listener.acknowledge(rejection.receiptHandle, "expired", { code: diagnosticCode });
1301
- return;
1302
- }
1303
- catch (error) {
1304
- if (rejection.released)
1305
- return;
1306
- const cleanPrewrite = error instanceof BridgeError && error.recoverable;
1307
- if (!cleanPrewrite ||
1308
- attempt === CLAUDE_REJECTION_RECEIPT_RETRY_DELAYS_MS.length) {
1309
- // Ambiguous/nonrecoverable writes are never replayed. Explicit
1310
- // release is idempotent even if the listener already consumed the
1311
- // terminal capability after a possibly-written attempt.
1312
- this.releaseRejectedNativeInbound(rejection);
1313
- return;
1314
- }
1315
- }
1316
- await this.waitForRejectedNativeInboundRetry(rejection, CLAUDE_REJECTION_RECEIPT_RETRY_DELAYS_MS[attempt]);
1317
- }
1318
- }
1319
- ownsNativeCodexListenerGeneration(generation, listener) {
1320
- return [
1321
- this.activeNativeCodexListener,
1322
- this.preparedNativeCodexListener,
1323
- this.retiredNativeCodexListener,
1324
- ].some((candidate) => candidate?.generation === generation && candidate.listener === listener);
1325
- }
1326
- async waitForRejectedNativeInboundRetry(rejection, delayMs) {
1327
- await new Promise((resolve) => {
1328
- let settled = false;
1329
- const finish = () => {
1330
- if (settled)
1331
- return;
1332
- settled = true;
1333
- clearTimeout(timer);
1334
- if (rejection.cancelRetry === finish) {
1335
- delete rejection.cancelRetry;
1336
- }
1337
- resolve();
1338
- };
1339
- const timer = setTimeout(finish, delayMs);
1340
- rejection.cancelRetry = finish;
1341
- if (this.closed || rejection.released)
1342
- finish();
1343
- });
1344
- }
1345
- releaseRejectedNativeInbound(rejection) {
1346
- if (rejection.released)
1347
- return;
1348
- rejection.released = true;
1349
- rejection.cancelRetry?.();
1350
- try {
1351
- rejection.listener.releaseInboundReceipt(rejection.receiptHandle);
1352
- }
1353
- catch {
1354
- // The listener owns a bounded process-lifetime capability. Closing must
1355
- // remain best-effort even if its explicit release path is unavailable.
1356
- }
1357
- }
1358
- trackClaudeMessage(providerId, gatewayMessageId, targetId, deadline, listenerGeneration) {
1359
- if (this.pendingByProviderId.has(providerId) ||
1360
- this.providerIdByGatewayId.get(gatewayMessageId) !== "" ||
1361
- this.pendingByProviderId.size >= this.maxPending) {
1362
- return false;
1363
- }
1364
- const timer = setTimeout(() => {
1365
- const pending = this.pendingByProviderId.get(providerId);
1366
- if (pending === undefined)
1367
- return;
1368
- pending.listener.untrack(providerId);
1369
- if (pending.writeEvidence === "transport_written") {
1370
- this.finishClaudeMessage(providerId, "unconfirmed", "CLAUDE_RECEIPT_UNCONFIRMED");
1371
- }
1372
- else if (pending.writeEvidence === "transport_uncertain") {
1373
- this.finishClaudeMessage(providerId, "ambiguous", "CLAUDE_DISPATCH_OUTCOME_AMBIGUOUS");
1374
- }
1375
- else {
1376
- this.finishClaudeMessage(providerId, "expired", "MESSAGE_EXPIRED");
1377
- }
1378
- }, Math.max(1, deadline - this.now()));
1379
- timer.unref();
1380
- this.pendingByProviderId.set(providerId, {
1381
- gatewayMessageId,
1382
- listener: listenerGeneration.listener,
1383
- listenerGeneration: listenerGeneration.generation,
1384
- targetId,
1385
- writeEvidence: "none",
1386
- timer,
1387
- });
1388
- this.providerIdByGatewayId.set(gatewayMessageId, providerId);
1389
- return true;
1390
- }
1391
- abandonClaudeTracking(gatewayMessageId, providerId) {
1392
- if (providerId !== undefined) {
1393
- const pending = this.pendingByProviderId.get(providerId);
1394
- if (pending?.gatewayMessageId === gatewayMessageId) {
1395
- clearTimeout(pending.timer);
1396
- this.pendingByProviderId.delete(providerId);
1397
- pending.listener.untrack(providerId);
1398
- }
1399
- }
1400
- this.providerIdByGatewayId.delete(gatewayMessageId);
1401
- this.pendingTargetByGatewayId.delete(gatewayMessageId);
1402
- }
1403
- onReceipt(listenerGeneration, event) {
1404
- const pending = this.pendingByProviderId.get(event.messageId);
1405
- if (pending === undefined ||
1406
- pending.listenerGeneration !== listenerGeneration.generation ||
1407
- pending.listener !== listenerGeneration.listener) {
1408
- return;
1409
- }
1410
- if (event.status === "held") {
1411
- pending.writeEvidence = "transport_written";
1412
- this.emitDelivery({ messageId: pending.gatewayMessageId, state: "held" });
1413
- return;
1414
- }
1415
- // `released` is Claude's native approval-gate terminal: the frame reached
1416
- // the recipient queue. It does not claim a model read or task completion.
1417
- const state = event.status === "released"
1418
- ? "released"
1419
- : event.status === "denied"
1420
- ? "denied"
1421
- : event.status === "expired"
1422
- ? "expired"
1423
- : event.status === "unconfirmed"
1424
- ? "unconfirmed"
1425
- : "ambiguous";
1426
- this.finishClaudeMessage(event.messageId, state, event.status === "unconfirmed"
1427
- ? "CLAUDE_RECEIPT_UNCONFIRMED"
1428
- : undefined);
1429
- }
1430
- recordClaudeWriteEvidence(providerId, evidence) {
1431
- const pending = this.pendingByProviderId.get(providerId);
1432
- if (pending === undefined)
1433
- return false;
1434
- const previous = pending.writeEvidence;
1435
- if (pending.writeEvidence !== "transport_written" ||
1436
- evidence === "transport_written") {
1437
- pending.writeEvidence = evidence;
1438
- }
1439
- return pending.writeEvidence !== previous;
1440
- }
1441
- finishClaudeMessage(providerId, state, safeErrorCode) {
1442
- const pending = this.pendingByProviderId.get(providerId);
1443
- if (pending === undefined)
1444
- return;
1445
- clearTimeout(pending.timer);
1446
- this.pendingByProviderId.delete(providerId);
1447
- this.providerIdByGatewayId.delete(pending.gatewayMessageId);
1448
- this.pendingTargetByGatewayId.delete(pending.gatewayMessageId);
1449
- this.emitDelivery({
1450
- messageId: pending.gatewayMessageId,
1451
- state,
1452
- ...(safeErrorCode === undefined ? {} : { safeErrorCode }),
1453
- });
1454
- }
1455
- emitDelivery(event) {
1456
- const callbacks = this.callbacks;
1457
- if (callbacks === undefined || this.closed)
1458
- return;
1459
- invokeCallback(() => callbacks.onDelivery(event));
1460
- }
1461
- async refreshClaudeDiscovery() {
1462
- if (this.discoveryInFlight !== undefined) {
1463
- return await this.discoveryInFlight;
1464
- }
1465
- const operation = this.refreshClaudeDiscoveryOnce();
1466
- this.discoveryInFlight = operation;
1467
- try {
1468
- return await operation;
1469
- }
1470
- finally {
1471
- if (this.discoveryInFlight === operation) {
1472
- this.discoveryInFlight = undefined;
1473
- }
1474
- }
1475
- }
1476
- async refreshClaudeDiscoveryOnce() {
1477
- const dispatchEpochAtStart = new Map();
1478
- for (const routeHandle of this.selected.keys()) {
1479
- dispatchEpochAtStart.set(routeHandle, this.selectedDispatchEpoch.get(routeHandle) ?? 0);
1480
- }
1481
- const pendingAtStart = new Set(this.pendingTargetByGatewayId.values());
1482
- let discovery;
1483
- try {
1484
- discovery = await this.peer.discover();
1485
- }
1486
- catch (error) {
1487
- this.registryObservation = unavailableClaudeRegistryObservation();
1488
- throw error;
1489
- }
1490
- const registry = claudeRegistryObservation(discovery);
1491
- this.registryObservation = registry;
1492
- this.discovered.clear();
1493
- const rows = [];
1494
- for (const peer of discovery.peers) {
1495
- if (peer.kind !== "interactive" ||
1496
- !NATIVE_CLAUDE_NAME.test(peer.alias)) {
1497
- continue;
1498
- }
1499
- const alias = `${peer.alias}@${this.identity.hostId}`;
1500
- if (!PUBLIC_ALIAS.test(alias))
1501
- continue;
1502
- const row = {
1503
- alias,
1504
- routeHandle: peer.targetId,
1505
- kind: "interactive",
1506
- state: claudeRouteState(peer),
1507
- };
1508
- this.discovered.set(peer.targetId, row);
1509
- rows.push(row);
1510
- }
1511
- for (const [routeHandle, selectedAlias] of this.selected) {
1512
- const row = this.discovered.get(routeHandle);
1513
- if (row === undefined) {
1514
- this.emitClaudeRouteObservation(routeHandle, "busy", "CLAUDE_PEER_NOT_OBSERVED", true);
1515
- }
1516
- else {
1517
- if (!discovery.truncated && row.alias !== selectedAlias) {
1518
- // A Claude rename changes only the live name index. The native
1519
- // session UUID remains the selected logical route. An incomplete
1520
- // scan is display evidence only and cannot rename dispatch authority.
1521
- this.selected.set(routeHandle, row.alias);
1522
- }
1523
- if (row.state === "idle" &&
1524
- (pendingAtStart.has(routeHandle) ||
1525
- this.hasPendingClaudeReceipt(routeHandle) ||
1526
- dispatchEpochAtStart.get(routeHandle) !==
1527
- (this.selectedDispatchEpoch.get(routeHandle) ?? 0))) {
1528
- // Never publish an idle sample from a discovery that overlapped a
1529
- // dispatch or its receipt lifetime. The dirty epoch remains set for
1530
- // a wholly post-terminal discovery.
1531
- continue;
1532
- }
1533
- this.emitClaudeRouteObservation(routeHandle, row.state, undefined, true);
1534
- }
1535
- }
1536
- return { peers: rows, complete: !discovery.truncated, registry };
1537
- }
1538
- emitClaudeRouteObservation(routeHandle, state, safeErrorCode, authoritative = false) {
1539
- if (this.nativeCodexSuccessionFreeze !== undefined)
1540
- return;
1541
- if (authoritative &&
1542
- state === "idle" &&
1543
- this.hasPendingClaudeReceipt(routeHandle)) {
1544
- // A registry peer can still report idle after our write but before it
1545
- // consumes the inbox frame. Preserve the dirty dispatch epoch so only a
1546
- // fresh discovery after exact terminal receipt can unlock this route.
1547
- return;
1548
- }
1549
- const signature = `${state}:${safeErrorCode ?? ""}`;
1550
- const mustPublish = authoritative && this.selectedObservationDirty.delete(routeHandle);
1551
- if (!mustPublish && this.selectedObservations.get(routeHandle) === signature) {
1552
- return;
1553
- }
1554
- this.selectedObservations.set(routeHandle, signature);
1555
- const callbacks = this.callbacks;
1556
- if (callbacks === undefined || this.closed)
1557
- return;
1558
- invokeCallback(() => callbacks.onRouteState({
1559
- endpoint: callbackEndpoint(this.identity, routeHandle),
1560
- state,
1561
- ...(safeErrorCode === undefined ? {} : { safeErrorCode }),
1562
- }));
1563
- }
1564
- hasPendingClaudeReceipt(routeHandle) {
1565
- for (const targetId of this.pendingTargetByGatewayId.values()) {
1566
- if (targetId === routeHandle)
1567
- return true;
1568
- }
1569
- return false;
1570
- }
1571
- scheduleClaudeMonitor() {
1572
- if (this.closed ||
1573
- this.nativeCodexSuccessionFreeze !== undefined ||
1574
- this.selected.size === 0 ||
1575
- this.monitorTimer !== undefined) {
543
+ scheduleClaudeMonitor() {
544
+ if (this.closed ||
545
+ this.selected.size === 0 ||
546
+ this.monitorTimer !== undefined) {
1576
547
  return;
1577
548
  }
1578
549
  this.monitorTimer = setTimeout(() => {
1579
550
  this.monitorTimer = undefined;
1580
551
  void this.refreshClaudeDiscovery()
1581
552
  .catch(() => {
1582
- if (this.nativeCodexSuccessionFreeze !== undefined)
1583
- return;
1584
553
  for (const routeHandle of this.selected.keys()) {
1585
554
  this.emitClaudeRouteObservation(routeHandle, "busy", "CLAUDE_DISCOVERY_UNAVAILABLE");
1586
555
  }
@@ -1593,328 +562,167 @@ export class LocalClaudeGatewayProvider {
1593
562
  export function createLocalClaudeGatewayProvider(options) {
1594
563
  return new LocalClaudeGatewayProvider(options);
1595
564
  }
1596
- function validateCodexFactory(factory) {
1597
- if (factory.hostId !== LOCAL_HOST ||
1598
- factory.protocol !== "codex-app-server" ||
1599
- factory.protocolVersion !== factory.appServerVersion ||
1600
- factory.appServerVersion.length === 0 ||
1601
- factory.endpointGeneration.length === 0) {
1602
- throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The local Codex provider requires exact managed endpoint identity.");
1603
- }
565
+ function boundedProviderOption(value, fallback, maximum) {
566
+ const candidate = value ?? fallback;
567
+ if (!Number.isSafeInteger(candidate) ||
568
+ candidate <= 0 ||
569
+ candidate > maximum) {
570
+ throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "The local Codex provider has an invalid bounded option.");
571
+ }
572
+ return candidate;
1604
573
  }
1605
- function codexRouteState(observation) {
1606
- if (observation.connection !== "ready")
1607
- return "stale";
1608
- if (observation.routeStatus === "idle")
1609
- return "idle";
1610
- if (observation.routeStatus === "waiting_approval") {
1611
- return "awaiting_approval";
1612
- }
1613
- if (observation.routeStatus === "starting" ||
1614
- observation.routeStatus === "active" ||
1615
- observation.routeStatus === "interrupting") {
1616
- return "busy";
574
+ function cleanCodexResult(result) {
575
+ if (result.safeErrorCode === "MESSAGE_EXPIRED") {
576
+ return { state: "expired", safeErrorCode: "MESSAGE_EXPIRED" };
1617
577
  }
1618
- // Unknown, not-loaded, uncertain, system-error, and explicit stale states
1619
- // are never cosmetic busy. They cannot authorize another write.
1620
- return "stale";
1621
- }
1622
- function codexRouteSafeCode(observation) {
1623
- if (codexRouteState(observation) === "stale") {
1624
- return "CODEX_ROUTE_STALE";
578
+ if (CODEX_CLEAN_RETRY_CODES.has(result.safeErrorCode)) {
579
+ return { state: "deferred", safeErrorCode: result.safeErrorCode };
1625
580
  }
1626
- return undefined;
581
+ return { state: "failed", safeErrorCode: result.safeErrorCode };
1627
582
  }
1628
- export class LocalCodexGatewayProvider {
1629
- factory;
1630
- refreshFactory;
1631
- maxCallbacks;
1632
- maxRoutes;
1633
- maxReplyBytes;
1634
- cleanupPollMs;
1635
- cleanupTimeoutMs;
1636
- recoveryInitialMs;
1637
- recoveryMaxMs;
1638
- now;
1639
- routes = new Map();
1640
- /** Exact public alias authority retained across internal route recovery. */
1641
- routeAliases = new Map();
1642
- routeCreations = new Map();
1643
- routeReleases = new Map();
1644
- routeRecoveryAttempts = new Map();
1645
- routeRecoveryTimers = new Map();
1646
- routeRecoveries = new Map();
1647
- routesRequiringRecovery = new Set();
1648
- trackedRoutes = new Set();
1649
- retiredEndpointGeneration;
1650
- expectsReply = new Map();
1651
- callbackQueue = [];
1652
- callbackScheduled = false;
1653
- callbacks;
1654
- endpointRefresh;
1655
- endpointRefreshDelivery;
1656
- endpointActivationRetry;
1657
- endpointActivationRetryTimer;
1658
- callbackDrainFrozen = false;
1659
- endpointUnavailable = false;
1660
- pendingEndpointRefreshEvent;
1661
- pendingEndpointRefreshSelectorClaimed = false;
1662
- initialized = false;
1663
- closing = false;
1664
- closed = false;
1665
- constructor(options) {
1666
- validateCodexFactory(options.factory);
1667
- this.factory = options.factory;
1668
- this.refreshFactory = options.refreshFactory;
1669
- exactLocalHost(options.factory.hostId);
1670
- this.maxCallbacks = positiveBounded(options.maxCallbackEvents, MAX_CODEX_CALLBACKS, 4_096);
1671
- this.maxRoutes = positiveBounded(options.maxRoutes, MAX_CODEX_ROUTES, 256);
1672
- this.maxReplyBytes = positiveBounded(options.maxReplyBytes, MAX_TRANSIENT_REPLY_BYTES, 1024 * 1024);
1673
- this.cleanupPollMs = positiveBounded(options.cleanupPollMs, DEFAULT_CODEX_CLEANUP_POLL_MS, 1_000);
1674
- this.cleanupTimeoutMs = positiveBounded(options.cleanupTimeoutMs, DEFAULT_CODEX_CLEANUP_TIMEOUT_MS, 60_000);
1675
- if (this.cleanupPollMs >= this.cleanupTimeoutMs) {
1676
- throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "The cleanup polling interval must be shorter than its deadline.");
1677
- }
1678
- this.recoveryInitialMs = positiveBounded(options.recoveryInitialMs, DEFAULT_CODEX_RECOVERY_INITIAL_MS, 60_000);
1679
- this.recoveryMaxMs = positiveBounded(options.recoveryMaxMs, DEFAULT_CODEX_RECOVERY_MAX_MS, 5 * 60_000);
1680
- if (this.recoveryInitialMs > this.recoveryMaxMs) {
1681
- throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "The Codex recovery initial delay cannot exceed its capped delay.");
1682
- }
1683
- this.now = options.now ?? (() => new Date());
583
+ function terminalCodexResult(result, expectsReply) {
584
+ if (result.outcome === "failed") {
585
+ return { state: "failed", safeErrorCode: "CODEX_TURN_FAILED" };
1684
586
  }
1685
- get identity() {
1686
- return {
1687
- provider: "codex",
1688
- hostId: exactLocalHost(this.factory.hostId),
1689
- endpointGeneration: this.factory.endpointGeneration,
1690
- };
1691
- }
1692
- get protocol() {
1693
- return this.factory.protocol;
1694
- }
1695
- get protocolVersion() {
1696
- return this.factory.protocolVersion;
1697
- }
1698
- activateEndpointGeneration(endpointGeneration) {
1699
- const event = this.pendingEndpointRefreshEvent;
1700
- if (event === undefined ||
1701
- event.current.endpointGeneration !== endpointGeneration ||
1702
- this.factory.endpointGeneration !== endpointGeneration) {
1703
- throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_MISMATCH", "Only the exact pending managed Codex generation can be activated.");
1704
- }
1705
- this.pendingEndpointRefreshEvent = undefined;
1706
- this.pendingEndpointRefreshSelectorClaimed = false;
1707
- this.clearEndpointActivationRetry();
1708
- this.endpointUnavailable = false;
1709
- }
1710
- releaseEndpointRefreshSelectorClaim(endpointGeneration) {
1711
- const event = this.pendingEndpointRefreshEvent;
1712
- if (event?.current.endpointGeneration === endpointGeneration) {
1713
- this.pendingEndpointRefreshSelectorClaimed = false;
1714
- }
587
+ if (result.outcome === "interrupted") {
588
+ return { state: "cancelled", safeErrorCode: "CODEX_TURN_INTERRUPTED" };
1715
589
  }
1716
- rearmEndpointRefreshActivation(endpointGeneration) {
1717
- const event = this.pendingEndpointRefreshEvent;
1718
- if (event?.current.endpointGeneration === endpointGeneration &&
1719
- !this.closing &&
1720
- !this.closed) {
1721
- this.publishEndpointRefresh(event);
1722
- }
1723
- }
1724
- async initialize(callbacks) {
1725
- if (this.closed || this.initialized) {
1726
- throw new BridgeError("CODEX_PROVIDER_INITIALIZATION_REJECTED", "The local Codex provider cannot be initialized in this state.");
1727
- }
1728
- this.callbacks = callbacks;
1729
- this.initialized = true;
1730
- return { health: "healthy" };
590
+ return {
591
+ state: "delivered",
592
+ ...(result.replyCode === "REPLY_TOO_LARGE"
593
+ ? { safeErrorCode: "CODEX_REPLY_TOO_LARGE" }
594
+ : {}),
595
+ ...(expectsReply && result.replyText !== null
596
+ ? { replyText: result.replyText }
597
+ : {}),
598
+ };
599
+ }
600
+ function mapCodexStartResult(result, expectsReply) {
601
+ if (result.phase === "clean")
602
+ return cleanCodexResult(result);
603
+ if (result.phase === "armed") {
604
+ return { state: "ambiguous", safeErrorCode: result.safeErrorCode };
1731
605
  }
1732
- async selectRoute(input) {
1733
- if (!this.initialized || this.closing || this.closed) {
1734
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
1735
- }
1736
- if (!PUBLIC_ALIAS.test(input.alias) ||
1737
- !input.alias.endsWith(`@${this.identity.hostId}`)) {
1738
- throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex alias is outside the exact local provider boundary.");
1739
- }
1740
- if (!OPAQUE_ROUTE.test(input.routeHandle)) {
1741
- throw new BridgeError("CODEX_ROUTE_INVALID", "The Codex task identifier is outside the exact provider boundary.");
1742
- }
1743
- const retainedAlias = this.routeAliases.get(input.routeHandle);
1744
- if (retainedAlias !== undefined && retainedAlias !== input.alias) {
1745
- throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex route is already bound to another exact public alias.");
1746
- }
1747
- this.cancelRouteRecovery(input.routeHandle);
1748
- let refreshResult;
1749
- let selectorRefreshReserved = false;
1750
- let route;
1751
- const stageRefreshedRoute = async (result) => {
1752
- if (result === undefined) {
1753
- throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex endpoint replacement could not be established.");
1754
- }
1755
- refreshResult = result;
1756
- selectorRefreshReserved = this.reserveSelectorRefresh(result);
1757
- if (!selectorRefreshReserved) {
1758
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
1759
- }
1760
- try {
1761
- return await this.ensureRoute(input.routeHandle, false, true);
1762
- }
1763
- catch (selectionError) {
1764
- this.emitSelectorRefreshFallback(result, input.routeHandle);
1765
- throw selectionError;
1766
- }
1767
- };
1768
- const retainedEndpointRefresh = this.retainedEndpointRefreshResult();
1769
- if (this.endpointRefresh !== undefined) {
1770
- route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1771
- }
1772
- else if (retainedEndpointRefresh !== undefined) {
1773
- route = await stageRefreshedRoute(retainedEndpointRefresh);
1774
- }
1775
- else if (this.endpointUnavailable &&
1776
- this.pendingEndpointRefreshEvent === undefined) {
1777
- route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1778
- }
1779
- else {
1780
- this.assertReady();
1781
- try {
1782
- route = await this.ensureRoute(input.routeHandle);
1783
- }
1784
- catch (error) {
1785
- const refreshPending = error instanceof BridgeError &&
1786
- error.code === "CODEX_ENDPOINT_REFRESH_PENDING";
1787
- if (!this.isEndpointGenerationChanged(error) && !refreshPending) {
1788
- throw error;
1789
- }
1790
- if (this.isEndpointGenerationChanged(error)) {
1791
- this.endpointUnavailable = true;
1792
- route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1793
- }
1794
- else if (this.endpointRefresh !== undefined) {
1795
- route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1796
- }
1797
- else {
1798
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
1799
- }
1800
- }
1801
- if (refreshResult === undefined &&
1802
- (route.endpointGeneration !== this.factory.endpointGeneration ||
1803
- this.endpointUnavailable ||
1804
- this.endpointRefresh !== undefined)) {
1805
- if (this.endpointRefresh === undefined) {
1806
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
1807
- }
1808
- route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
1809
- }
1810
- }
1811
- const observation = route.connector.observation();
1812
- const state = codexRouteState(observation);
1813
- if (state === "stale") {
1814
- if (refreshResult !== undefined && selectorRefreshReserved) {
1815
- // Publish the transition before any asynchronous connector cleanup so
1816
- // the controller can accept or durably reject the exact replacement
1817
- // generation. A route which closed after refresh staging is not valid
1818
- // re-anchoring evidence and must be removed from both the callback and
1819
- // its retained retry authority.
1820
- this.emitSelectorRefreshFallback(refreshResult, input.routeHandle);
1821
- }
1822
- this.routesRequiringRecovery.add(input.routeHandle);
1823
- await this.releaseRouteInternal(input.routeHandle);
1824
- if (this.trackedRoutes.has(input.routeHandle)) {
1825
- this.scheduleRouteRecovery(input.routeHandle);
1826
- }
1827
- throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task connection closed during route selection.");
606
+ if (result.phase === "accepted") {
607
+ return { state: "unconfirmed", safeErrorCode: result.safeErrorCode };
608
+ }
609
+ return terminalCodexResult(result, expectsReply);
610
+ }
611
+ function mapCodexSteerResult(result) {
612
+ if (result.phase === "clean")
613
+ return cleanCodexResult(result);
614
+ if (result.phase === "armed") {
615
+ return { state: "ambiguous", safeErrorCode: result.safeErrorCode };
616
+ }
617
+ return { state: "delivered" };
618
+ }
619
+ /**
620
+ * Thin logical Codex adapter. Registration and construction perform no App
621
+ * Server I/O. Each semantic operation owns a fresh attested transport, while
622
+ * the independent observer publishes only best-effort in-memory freshness.
623
+ */
624
+ export class LocalCodexGatewayProvider {
625
+ hostId;
626
+ operation;
627
+ observationPollMs;
628
+ observationTimeoutMs;
629
+ observationBackoffMaxMs;
630
+ maxObservedRoutes;
631
+ now;
632
+ timers;
633
+ observedRoutes = new Map();
634
+ activeTurns = new Map();
635
+ inFlightOperations = new Set();
636
+ operationControllers = new Set();
637
+ callbacks;
638
+ observationTimer;
639
+ observationInFlight;
640
+ observationController;
641
+ initialized = false;
642
+ closing = false;
643
+ closed = false;
644
+ constructor(options) {
645
+ if (options.hostId !== LOCAL_HOST ||
646
+ options.operation === undefined ||
647
+ typeof options.operation.execute !== "function") {
648
+ throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "The stateless Codex provider requires the exact local host.");
649
+ }
650
+ this.hostId = options.hostId;
651
+ this.operation = options.operation;
652
+ this.observationPollMs = boundedProviderOption(options.observationPollMs, DEFAULT_CODEX_OBSERVATION_POLL_MS, 60_000);
653
+ this.observationTimeoutMs = boundedProviderOption(options.observationTimeoutMs, DEFAULT_CODEX_OBSERVATION_TIMEOUT_MS, 60_000);
654
+ this.observationBackoffMaxMs = boundedProviderOption(options.observationBackoffMaxMs, DEFAULT_CODEX_OBSERVATION_BACKOFF_MAX_MS, 5 * 60_000);
655
+ if (this.observationPollMs > this.observationBackoffMaxMs) {
656
+ throw new BridgeError("INVALID_GATEWAY_PROVIDER_CONFIGURATION", "Codex observer backoff must cover its polling interval.");
657
+ }
658
+ this.maxObservedRoutes = boundedProviderOption(options.maxObservedRoutes, MAX_CODEX_OBSERVED_ROUTES, 256);
659
+ this.now = options.now ?? (() => new Date());
660
+ this.timers = options.timers ?? {
661
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
662
+ clearTimeout: (timer) => clearTimeout(timer),
663
+ };
664
+ }
665
+ get identity() {
666
+ return { provider: "codex", hostId: this.hostId };
667
+ }
668
+ get protocol() {
669
+ return "codex-app-server";
670
+ }
671
+ get protocolVersion() {
672
+ return "stateless-v1";
673
+ }
674
+ async initialize(callbacks) {
675
+ if (this.initialized || this.closing || this.closed) {
676
+ throw new BridgeError("CODEX_PROVIDER_INITIALIZATION_REJECTED", "The stateless Codex provider cannot initialize in this state.");
1828
677
  }
1829
- this.trackedRoutes.add(input.routeHandle);
1830
- this.routeAliases.set(input.routeHandle, input.alias);
1831
- if (refreshResult === undefined) {
1832
- this.queueRouteObservation(route, observation);
678
+ this.callbacks = callbacks;
679
+ this.initialized = true;
680
+ this.scheduleObservation();
681
+ return { health: "healthy" };
682
+ }
683
+ observeLogicalRoute(input) {
684
+ if (!this.initialized ||
685
+ this.closing ||
686
+ this.closed ||
687
+ !PUBLIC_ALIAS.test(input.alias) ||
688
+ !input.alias.endsWith("@" + this.hostId) ||
689
+ !OPAQUE_ROUTE.test(input.routeHandle) ||
690
+ !OPAQUE_ROUTE.test(input.registrationId)) {
691
+ return;
1833
692
  }
1834
- const endpointRefresh = refreshResult === undefined || !selectorRefreshReserved
1835
- ? undefined
1836
- : refreshResult.event;
1837
- if (endpointRefresh !== undefined) {
1838
- this.armEndpointActivationRetry(endpointRefresh);
693
+ const existing = this.observedRoutes.get(input.registrationId);
694
+ if (existing === undefined &&
695
+ this.observedRoutes.size >= this.maxObservedRoutes) {
696
+ return;
1839
697
  }
1840
- return {
1841
- routeHandle: input.routeHandle,
1842
- state,
1843
- ...(endpointRefresh === undefined ? {} : { endpointRefresh }),
1844
- };
698
+ this.observedRoutes.set(input.registrationId, {
699
+ ...input,
700
+ attempt: 0,
701
+ nextAt: this.now().getTime(),
702
+ });
703
+ this.scheduleObservation();
1845
704
  }
1846
- observeRouteSuccessionBarrier(routeHandle) {
1847
- if (!this.initialized || this.closing || this.closed) {
1848
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
1849
- }
1850
- const route = this.routes.get(routeHandle);
1851
- const observation = route?.connector.observation();
1852
- const routeCreationInFlight = this.routeCreations.has(routeHandle) ||
1853
- this.routeRecoveries.has(routeHandle) ||
1854
- (this.endpointRefresh !== undefined && this.trackedRoutes.has(routeHandle)) ||
1855
- this.pendingEndpointActivationClaims(routeHandle);
1856
- const routeReleaseInFlight = this.routeReleases.has(routeHandle);
1857
- const pendingReplyCorrelations = this.expectsReply.size;
1858
- const pendingCallbacks = Math.max(this.callbackQueue.length, this.callbackScheduled ? 1 : 0);
1859
- const routePresent = route !== undefined;
1860
- const connection = observation?.connection ?? "absent";
1861
- const routeStatus = observation?.routeStatus ?? "absent";
1862
- const queueDepth = observation?.queueDepth ?? 0;
1863
- const hasActiveTurn = observation?.hasActiveTurn ?? false;
1864
- const requestInFlight = observation?.requestInFlight ?? false;
1865
- return {
1866
- routePresent,
1867
- connection,
1868
- routeStatus,
1869
- queueDepth,
1870
- hasActiveTurn,
1871
- requestInFlight,
1872
- routeCreationInFlight,
1873
- routeReleaseInFlight,
1874
- pendingReplyCorrelations,
1875
- pendingCallbacks,
1876
- clean: routePresent &&
1877
- connection === "ready" &&
1878
- routeStatus === "idle" &&
1879
- queueDepth === 0 &&
1880
- !hasActiveTurn &&
1881
- !requestInFlight &&
1882
- !routeCreationInFlight &&
1883
- !routeReleaseInFlight &&
1884
- pendingReplyCorrelations === 0 &&
1885
- pendingCallbacks === 0,
1886
- };
705
+ forgetLogicalRoute(registrationId) {
706
+ this.observedRoutes.delete(registrationId);
707
+ this.activeTurns.delete(registrationId);
708
+ this.scheduleObservation();
1887
709
  }
1888
710
  async dispatch(input) {
1889
711
  if (!this.initialized ||
1890
712
  this.closing ||
1891
713
  this.closed ||
1892
- this.endpointUnavailable ||
1893
- this.endpointRefresh !== undefined ||
1894
714
  input.authorization !== "selected_route" ||
1895
- !sameEndpoint(input.binding, this.identity)) {
1896
- return { state: "failed", safeErrorCode: "CODEX_ROUTE_UNAVAILABLE" };
1897
- }
1898
- const route = this.routes.get(input.binding.routeHandle);
1899
- if (route === undefined ||
1900
- this.routeAliases.get(input.binding.routeHandle) !== input.targetAlias ||
1901
- this.routeReleases.has(input.binding.routeHandle) ||
1902
- route.endpointGeneration !== input.binding.endpointGeneration ||
1903
- route.endpointGeneration !== this.factory.endpointGeneration) {
715
+ input.binding.provider !== "codex" ||
716
+ input.binding.hostId !== this.hostId ||
717
+ !PUBLIC_ALIAS.test(input.targetAlias) ||
718
+ !input.targetAlias.endsWith("@" + this.hostId)) {
1904
719
  return { state: "failed", safeErrorCode: "CODEX_ROUTE_UNAVAILABLE" };
1905
720
  }
1906
- const guard = route.connector.guard();
1907
- if (strictDeadline(input.deadlineAt, this.now().getTime()) === undefined) {
1908
- return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
1909
- }
1910
- if (this.expectsReply.has(input.messageId)) {
1911
- return { state: "failed", safeErrorCode: "CODEX_MESSAGE_DUPLICATE" };
1912
- }
1913
721
  let content;
1914
722
  try {
1915
723
  content = composeProvenanceEnvelope({
1916
724
  sourceProvider: input.sourceProvider,
1917
- recipientProvider: this.identity.provider,
725
+ recipientProvider: "codex",
1918
726
  sourceAlias: input.sourceAlias,
1919
727
  targetAlias: input.targetAlias,
1920
728
  conversationId: input.conversationId,
@@ -1928,838 +736,233 @@ export class LocalCodexGatewayProvider {
1928
736
  });
1929
737
  }
1930
738
  catch (error) {
1931
- const safeErrorCode = error instanceof BridgeError &&
1932
- (error.code === "PROVENANCE_ENVELOPE_INVALID" ||
1933
- error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")
1934
- ? error.code
1935
- : "PROVENANCE_ENVELOPE_INVALID";
1936
- return { state: "failed", safeErrorCode };
739
+ return {
740
+ state: "failed",
741
+ safeErrorCode: error instanceof BridgeError &&
742
+ error.code === "PROVENANCE_ENVELOPE_TOO_LARGE"
743
+ ? error.code
744
+ : "PROVENANCE_ENVELOPE_INVALID",
745
+ };
1937
746
  }
1938
- this.expectsReply.set(input.messageId, input.expectsReply);
1939
- try {
1940
- const disposition = await route.connector.submitMessage(guard, {
747
+ if (input.steer === true) {
748
+ const active = this.activeTurns.get(input.binding.registrationId);
749
+ if (active === undefined ||
750
+ active.alias !== input.targetAlias ||
751
+ active.routeHandle !== input.binding.routeHandle) {
752
+ return { state: "deferred", safeErrorCode: "ROUTE_BUSY" };
753
+ }
754
+ const steering = active.accepted.steer({
755
+ attemptId: input.attemptId,
756
+ authorizeWrite: async (evidence) => await input.authorizeWrite({
757
+ attemptId: input.attemptId,
758
+ kind: "codex_turn_steer",
759
+ bodyBytes: Buffer.byteLength(input.text, "utf8"),
760
+ bodySha256: createHash("sha256").update(input.text).digest("hex"),
761
+ frameBytes: evidence.frameBytes,
762
+ sha256: evidence.sha256,
763
+ }),
1941
764
  deadlineAt: input.deadlineAt,
1942
- messageId: input.messageId,
1943
- ...(input.steer === true ? { steer: true } : {}),
1944
765
  text: content,
1945
766
  });
1946
- if (disposition.disposition === "deferred") {
1947
- this.expectsReply.delete(input.messageId);
1948
- return { state: "deferred", safeErrorCode: "CODEX_ROUTE_HELD" };
1949
- }
1950
- if (disposition.disposition === "steered") {
1951
- this.expectsReply.delete(input.messageId);
1952
- return { state: "delivered" };
1953
- }
1954
- return { state: "accepted" };
1955
- }
1956
- catch (error) {
1957
- if (!this.expectsReply.has(input.messageId)) {
1958
- // The connector synchronously handed off an exact terminal result.
1959
- return { state: "pending" };
1960
- }
1961
- this.expectsReply.delete(input.messageId);
1962
- if (error instanceof CodexConnectorError && error.ambiguous) {
1963
- return {
1964
- state: "ambiguous",
1965
- safeErrorCode: "CODEX_DISPATCH_OUTCOME_AMBIGUOUS",
1966
- };
1967
- }
1968
- if (error instanceof CodexConnectorError &&
1969
- [
1970
- "ROUTE_NOT_READY",
1971
- "ROUTE_BUSY",
1972
- ].includes(error.code)) {
1973
- return { state: "deferred", safeErrorCode: "CODEX_ROUTE_HELD" };
1974
- }
1975
- return { state: "failed", safeErrorCode: "CODEX_DISPATCH_REJECTED" };
1976
- }
1977
- }
1978
- async releaseRoute(routeHandle) {
1979
- if (this.pendingEndpointActivationClaims(routeHandle)) {
1980
- throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route still belongs to an unaccepted endpoint transition.", true);
1981
- }
1982
- this.trackedRoutes.delete(routeHandle);
1983
- this.cancelRouteRecovery(routeHandle);
1984
- const recovery = this.routeRecoveries.get(routeHandle);
1985
- if (recovery !== undefined)
1986
- await recovery;
1987
- const endpointRefresh = this.endpointRefresh;
1988
- if (endpointRefresh !== undefined)
1989
- await endpointRefresh;
1990
- if (this.pendingEndpointActivationClaims(routeHandle)) {
1991
- throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route acquired endpoint-transition authority during release.", true);
1992
- }
1993
- await this.releaseRouteInternal(routeHandle);
1994
- this.routeAliases.delete(routeHandle);
1995
- }
1996
- async releaseRouteInternal(routeHandle) {
1997
- const pending = this.routeReleases.get(routeHandle);
1998
- if (pending !== undefined)
1999
- return await pending;
2000
- const route = this.routes.get(routeHandle);
2001
- if (route === undefined)
2002
- return;
2003
- const release = this.closeRoute(route).then(() => {
2004
- if (this.routes.get(routeHandle) === route) {
2005
- this.routes.delete(routeHandle);
2006
- }
767
+ this.inFlightOperations.add(steering);
768
+ try {
769
+ return mapCodexSteerResult(await steering);
770
+ }
771
+ finally {
772
+ this.inFlightOperations.delete(steering);
773
+ }
774
+ }
775
+ if (this.activeTurns.has(input.binding.registrationId)) {
776
+ return { state: "deferred", safeErrorCode: "ROUTE_BUSY" };
777
+ }
778
+ let accepted;
779
+ const controller = new AbortController();
780
+ this.operationControllers.add(controller);
781
+ const operation = this.operation.execute({
782
+ attemptId: input.attemptId,
783
+ authorizeWrite: async (evidence) => await input.authorizeWrite({
784
+ attemptId: input.attemptId,
785
+ kind: "codex_turn_start",
786
+ bodyBytes: Buffer.byteLength(input.text, "utf8"),
787
+ bodySha256: createHash("sha256").update(input.text).digest("hex"),
788
+ frameBytes: evidence.frameBytes,
789
+ sha256: evidence.sha256,
790
+ }),
791
+ deadlineAt: input.deadlineAt,
792
+ kind: "start",
793
+ signal: controller.signal,
794
+ route: {
795
+ alias: input.targetAlias,
796
+ hostId: input.binding.hostId,
797
+ registrationId: input.binding.registrationId,
798
+ threadId: input.binding.routeHandle,
799
+ },
800
+ text: content,
801
+ onAccepted: async (current) => {
802
+ if (this.closing ||
803
+ this.closed ||
804
+ current.attemptId !== input.attemptId ||
805
+ this.activeTurns.has(input.binding.registrationId)) {
806
+ throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The exact Codex attempt cannot acquire its active-turn slot.");
807
+ }
808
+ await input.onAccepted({ attemptId: input.attemptId });
809
+ accepted = current;
810
+ this.activeTurns.set(input.binding.registrationId, {
811
+ attemptId: input.attemptId,
812
+ accepted: current,
813
+ alias: input.targetAlias,
814
+ routeHandle: input.binding.routeHandle,
815
+ });
816
+ },
2007
817
  });
2008
- this.routeReleases.set(routeHandle, release);
818
+ this.inFlightOperations.add(operation);
2009
819
  try {
2010
- await release;
820
+ const result = await operation;
821
+ this.publishOperationObservation(input, result);
822
+ return mapCodexStartResult(result, input.expectsReply);
2011
823
  }
2012
824
  finally {
2013
- this.routeReleases.delete(routeHandle);
825
+ this.inFlightOperations.delete(operation);
826
+ this.operationControllers.delete(controller);
827
+ if (accepted !== undefined &&
828
+ this.activeTurns.get(input.binding.registrationId)?.attemptId ===
829
+ input.attemptId) {
830
+ this.activeTurns.delete(input.binding.registrationId);
831
+ }
2014
832
  }
2015
833
  }
2016
834
  async close() {
2017
835
  if (this.closed || this.closing)
2018
836
  return;
2019
837
  this.closing = true;
2020
- for (const timer of this.routeRecoveryTimers.values())
2021
- clearTimeout(timer);
2022
- this.routeRecoveryTimers.clear();
2023
- this.routesRequiringRecovery.clear();
2024
- this.clearEndpointActivationRetry();
2025
- this.pendingEndpointRefreshEvent = undefined;
2026
- this.pendingEndpointRefreshSelectorClaimed = false;
2027
- const refreshResult = this.endpointRefresh === undefined
2028
- ? []
2029
- : await Promise.allSettled([this.endpointRefresh]);
2030
- const entries = [...this.routes.entries()];
2031
- const routeResults = await Promise.allSettled(entries.map(async ([routeHandle]) => this.releaseRoute(routeHandle)));
2032
- await Promise.allSettled([
2033
- ...this.routeRecoveries.values(),
2034
- ...this.routeCreations.values(),
2035
- ]);
2036
- this.drainCallbackQueue();
2037
- if (refreshResult.some((result) => result.status === "rejected") ||
2038
- routeResults.some((result) => result.status === "rejected")) {
2039
- this.closing = false;
2040
- throw new BridgeError("CODEX_PROVIDER_CLEANUP_FAILED", "An exact owned Codex turn did not confirm termination.");
2041
- }
2042
- try {
2043
- await this.factory.close();
2044
- }
2045
- catch {
2046
- this.closing = false;
2047
- throw new BridgeError("CODEX_PROVIDER_CLEANUP_FAILED", "An exact owned Codex provider process did not confirm cleanup.");
2048
- }
838
+ if (this.observationTimer !== undefined) {
839
+ this.timers.clearTimeout(this.observationTimer);
840
+ this.observationTimer = undefined;
841
+ }
842
+ this.observationController?.abort();
843
+ for (const controller of this.operationControllers)
844
+ controller.abort();
845
+ await this.observationInFlight?.catch(() => undefined);
846
+ for (const operation of this.inFlightOperations) {
847
+ void operation.catch(() => undefined);
848
+ }
849
+ this.operationControllers.clear();
850
+ this.activeTurns.clear();
851
+ this.observedRoutes.clear();
852
+ this.callbacks = undefined;
853
+ this.initialized = false;
2049
854
  this.closed = true;
2050
855
  this.closing = false;
2051
- this.callbackQueue.length = 0;
2052
- this.pendingEndpointRefreshEvent = undefined;
2053
- this.pendingEndpointRefreshSelectorClaimed = false;
2054
- this.trackedRoutes.clear();
2055
- this.routeAliases.clear();
2056
- this.expectsReply.clear();
2057
- this.callbacks = undefined;
2058
856
  }
2059
- assertReady() {
857
+ publishOperationObservation(input, result) {
858
+ const state = !result.cleanupConfirmed
859
+ ? { state: "unobserved", safeErrorCode: "CLEANUP_FAILED" }
860
+ : result.phase === "terminal"
861
+ ? { state: "idle" }
862
+ : result.safeErrorCode === "ROUTE_BUSY"
863
+ ? { state: "busy" }
864
+ : result.safeErrorCode === "APPROVAL_REQUIRED"
865
+ ? { state: "awaiting_approval" }
866
+ : result.safeErrorCode === "THREAD_NOT_OBSERVED"
867
+ ? {
868
+ state: "unobserved",
869
+ safeErrorCode: "THREAD_NOT_OBSERVED",
870
+ }
871
+ : {
872
+ state: "unobserved",
873
+ safeErrorCode: "CODEX_OBSERVER_UNAVAILABLE",
874
+ };
875
+ this.emitObservation({
876
+ alias: input.targetAlias,
877
+ registrationId: input.binding.registrationId,
878
+ routeHandle: input.binding.routeHandle,
879
+ }, state);
880
+ }
881
+ scheduleObservation() {
2060
882
  if (!this.initialized ||
2061
883
  this.closing ||
2062
884
  this.closed ||
2063
- this.endpointUnavailable ||
2064
- this.endpointRefresh !== undefined) {
2065
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2066
- }
2067
- }
2068
- async ensureRoute(threadId, queueInitialObservation = true, allowPendingEndpointActivation = false) {
2069
- if (allowPendingEndpointActivation) {
2070
- if (!this.initialized || this.closing || this.closed) {
2071
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
2072
- }
2073
- }
2074
- else {
2075
- this.assertReady();
2076
- }
2077
- const pendingRelease = this.routeReleases.get(threadId);
2078
- if (pendingRelease !== undefined) {
2079
- await pendingRelease;
2080
- return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointActivation);
2081
- }
2082
- const existing = this.routes.get(threadId);
2083
- if (existing !== undefined) {
2084
- if (existing.connector.observation().connection === "ready") {
2085
- return existing;
2086
- }
2087
- // A cached connector cannot recover after its transport closes or the
2088
- // protocol faults. Fully release that exact connector before constructing
2089
- // a fresh one. The identity-checked release lets automatic recovery and
2090
- // an explicit selector join the same cleanup safely.
2091
- await this.releaseRouteInternal(threadId);
2092
- return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointActivation);
2093
- }
2094
- const pending = this.routeCreations.get(threadId);
2095
- if (pending !== undefined)
2096
- return await pending;
2097
- if (this.routes.size + this.routeCreations.size >= this.maxRoutes) {
2098
- throw new BridgeError("CODEX_ROUTE_CAPACITY", "The bounded local Codex route table is full.", true);
2099
- }
2100
- const admittedFactory = this.factory;
2101
- const creation = (async () => {
2102
- const route = await this.createRoute(threadId, admittedFactory, queueInitialObservation);
2103
- if (this.closing || this.closed) {
2104
- await this.closeRoute(route);
2105
- throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider closed during route creation.");
2106
- }
2107
- if (!allowPendingEndpointActivation &&
2108
- (this.endpointUnavailable ||
2109
- this.endpointRefresh !== undefined ||
2110
- admittedFactory !== this.factory ||
2111
- route.endpointGeneration !== this.factory.endpointGeneration)) {
2112
- await this.closeRoute(route);
2113
- throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The Codex endpoint moved during exact route creation.", true);
2114
- }
2115
- this.routes.set(threadId, route);
2116
- return route;
2117
- })();
2118
- this.routeCreations.set(threadId, creation);
2119
- try {
2120
- return await creation;
2121
- }
2122
- finally {
2123
- this.routeCreations.delete(threadId);
2124
- }
2125
- }
2126
- async createRoute(threadId, factory, queueInitialObservation) {
2127
- let transport;
2128
- let connector;
2129
- const generation = Symbol(threadId);
2130
- try {
2131
- transport = await factory.connectTransport();
2132
- connector = await CodexAppServerConnector.connect({
2133
- compatibility: { endpointGeneration: factory.endpointGeneration },
2134
- route: {
2135
- endpointGeneration: factory.endpointGeneration,
2136
- threadId,
2137
- },
2138
- transport,
2139
- maxReplyBytes: this.maxReplyBytes,
2140
- now: this.now,
2141
- onEvent: (event) => this.onConnectorEvent(threadId, generation, event),
2142
- onTurnResult: (result) => this.onTurnResult(factory.endpointGeneration, result),
2143
- });
2144
- const loaded = await connector.observeLoadedThread(connector.guard());
2145
- if (!loaded.selectedThreadLoaded) {
2146
- throw new BridgeError("CODEX_THREAD_NOT_OBSERVED", "The exact opted-in Codex task is not loaded on this endpoint.");
2147
- }
2148
- await connector.resumeThread(connector.guard());
2149
- const route = {
2150
- connector,
2151
- endpointGeneration: factory.endpointGeneration,
2152
- generation,
2153
- threadId,
2154
- transport,
2155
- };
2156
- if (queueInitialObservation) {
2157
- this.queueRouteObservation(route, connector.observation());
2158
- }
2159
- return route;
2160
- }
2161
- catch (error) {
2162
- let cleanupFailure;
2163
- if (connector !== undefined) {
2164
- await connector.close().catch((closeError) => {
2165
- cleanupFailure = closeError;
2166
- });
2167
- }
2168
- else if (transport !== undefined) {
2169
- await transport.close().catch((closeError) => {
2170
- cleanupFailure = closeError;
2171
- });
2172
- }
2173
- if (cleanupFailure !== undefined) {
2174
- if (isCodexTrustFailure(cleanupFailure))
2175
- throw cleanupFailure;
2176
- throw new BridgeError("CODEX_ROUTE_CLEANUP_FAILED", "Codex route setup could not prove exact provider-process cleanup.");
2177
- }
2178
- if (isCodexTrustFailure(error))
2179
- throw error;
2180
- if (error instanceof BridgeError)
2181
- throw error;
2182
- if (error instanceof CodexConnectorError) {
2183
- throw new BridgeError(error.ambiguous
2184
- ? "CODEX_ROUTE_SETUP_AMBIGUOUS"
2185
- : "CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task could not be safely observed and resumed.");
2186
- }
2187
- throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task could not be safely observed and resumed.");
2188
- }
2189
- }
2190
- onConnectorEvent(threadId, generation, event) {
2191
- const route = this.routes.get(threadId);
2192
- if (route === undefined || route.generation !== generation)
885
+ this.observationInFlight !== undefined) {
2193
886
  return;
2194
- this.queueRouteObservation(route, event);
2195
- if (event.kind === "protocol_fault" ||
2196
- (event.kind === "connection_closed" &&
2197
- event.details?.errorCode !== undefined) ||
2198
- (event.kind === "route_status_changed" &&
2199
- codexRouteState(event) === "stale")) {
2200
- this.routesRequiringRecovery.add(threadId);
2201
- this.scheduleRouteRecovery(threadId);
2202
887
  }
2203
- }
2204
- scheduleRouteRecovery(threadId) {
2205
- if (this.closed ||
2206
- this.closing ||
2207
- !this.routesRequiringRecovery.has(threadId) ||
2208
- this.routeRecoveryTimers.has(threadId) ||
2209
- this.routeRecoveries.has(threadId)) {
2210
- return;
888
+ if (this.observationTimer !== undefined) {
889
+ this.timers.clearTimeout(this.observationTimer);
890
+ this.observationTimer = undefined;
2211
891
  }
2212
- const attempt = this.routeRecoveryAttempts.get(threadId) ?? 0;
2213
- const delay = Math.min(this.recoveryMaxMs, this.recoveryInitialMs * 2 ** Math.min(attempt, 16));
2214
- const timer = setTimeout(() => {
2215
- if (this.routeRecoveryTimers.get(threadId) !== timer)
892
+ const next = [...this.observedRoutes.values()].reduce((selected, candidate) => selected === undefined || candidate.nextAt < selected.nextAt
893
+ ? candidate
894
+ : selected, undefined);
895
+ if (next === undefined)
896
+ return;
897
+ const delay = Math.max(0, next.nextAt - this.now().getTime());
898
+ const timer = this.timers.setTimeout(() => {
899
+ if (this.observationTimer !== timer)
2216
900
  return;
2217
- this.routeRecoveryTimers.delete(threadId);
2218
- const recovery = this.recoverRoute(threadId);
2219
- this.routeRecoveries.set(threadId, recovery);
2220
- void recovery.finally(() => {
2221
- if (this.routeRecoveries.get(threadId) === recovery) {
2222
- this.routeRecoveries.delete(threadId);
2223
- }
2224
- this.scheduleRouteRecovery(threadId);
901
+ this.observationTimer = undefined;
902
+ const operation = this.runObservation(next);
903
+ this.observationInFlight = operation;
904
+ void operation.finally(() => {
905
+ if (this.observationInFlight === operation) {
906
+ this.observationInFlight = undefined;
907
+ }
908
+ this.scheduleObservation();
2225
909
  });
2226
910
  }, delay);
2227
911
  timer.unref();
2228
- this.routeRecoveryTimers.set(threadId, timer);
2229
- }
2230
- async recoverRoute(threadId) {
2231
- if (this.closed ||
2232
- this.closing ||
2233
- !this.routesRequiringRecovery.has(threadId)) {
2234
- return;
2235
- }
2236
- try {
2237
- if (this.endpointUnavailable) {
2238
- if (this.pendingEndpointRefreshEvent !== undefined) {
2239
- throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is awaiting controller activation.", true);
2240
- }
2241
- const refreshed = await this.refreshEndpoint("callback");
2242
- if (refreshed?.event.routes.some((route) => route.routeHandle === threadId)) {
2243
- this.routesRequiringRecovery.delete(threadId);
2244
- this.routeRecoveryAttempts.delete(threadId);
2245
- return;
2246
- }
2247
- throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is not ready.", true);
2248
- }
2249
- const current = this.routes.get(threadId);
2250
- if (current !== undefined &&
2251
- codexRouteState(current.connector.observation()) === "stale") {
2252
- await this.releaseRouteInternal(threadId);
2253
- }
2254
- if (!this.routesRequiringRecovery.has(threadId))
2255
- return;
2256
- const replacement = await this.ensureRoute(threadId);
2257
- if (!this.routesRequiringRecovery.has(threadId)) {
2258
- // An explicit selector can join this exact creation after cancelling
2259
- // automatic recovery. In that case the replacement is now operator-
2260
- // owned and must remain live. Explicit release waits for this recovery
2261
- // promise before closing any late-created replacement.
2262
- return;
2263
- }
2264
- this.routesRequiringRecovery.delete(threadId);
2265
- this.routeRecoveryAttempts.delete(threadId);
2266
- this.queueRouteObservation(replacement, replacement.connector.observation());
2267
- }
2268
- catch (error) {
2269
- if (this.isEndpointGenerationChanged(error)) {
2270
- this.endpointUnavailable = true;
2271
- const refreshed = await this.refreshEndpoint("callback").catch(() => undefined);
2272
- if (refreshed?.event.routes.some((route) => route.routeHandle === threadId)) {
2273
- this.routesRequiringRecovery.delete(threadId);
2274
- this.routeRecoveryAttempts.delete(threadId);
2275
- return;
2276
- }
2277
- }
2278
- this.routeRecoveryAttempts.set(threadId, (this.routeRecoveryAttempts.get(threadId) ?? 0) + 1);
2279
- }
2280
- }
2281
- cancelRouteRecovery(threadId) {
2282
- this.routesRequiringRecovery.delete(threadId);
2283
- this.routeRecoveryAttempts.delete(threadId);
2284
- const timer = this.routeRecoveryTimers.get(threadId);
2285
- if (timer !== undefined)
2286
- clearTimeout(timer);
2287
- this.routeRecoveryTimers.delete(threadId);
2288
- }
2289
- isEndpointGenerationChanged(error) {
2290
- return (error instanceof LocalCodexTransportError &&
2291
- error.code === "ENDPOINT_GENERATION_CHANGED");
2292
- }
2293
- pendingEndpointActivationClaims(routeHandle) {
2294
- const event = this.pendingEndpointRefreshEvent;
2295
- return (event !== undefined &&
2296
- event.current.endpointGeneration === this.factory.endpointGeneration &&
2297
- event.routes.some((route) => route.routeHandle === routeHandle));
2298
- }
2299
- retainedEndpointRefreshResult() {
2300
- const event = this.pendingEndpointRefreshEvent;
2301
- if (event === undefined ||
2302
- this.pendingEndpointRefreshSelectorClaimed ||
2303
- event.current.endpointGeneration !== this.factory.endpointGeneration) {
2304
- return undefined;
2305
- }
2306
- return {
2307
- event,
2308
- delivery: "selector",
2309
- selectorClaimed: false,
2310
- published: true,
2311
- };
2312
- }
2313
- async refreshEndpoint(delivery) {
2314
- if (this.refreshFactory === undefined || this.closing || this.closed) {
2315
- return undefined;
2316
- }
2317
- const existing = this.endpointRefresh;
2318
- if (existing !== undefined) {
2319
- // A selector already running inside the controller lock takes ownership
2320
- // of the transition result. This avoids waiting on a callback that needs
2321
- // that same lock while still ensuring exactly one activation path.
2322
- if (delivery === "selector")
2323
- this.endpointRefreshDelivery = "selector";
2324
- return await existing;
2325
- }
2326
- this.endpointRefreshDelivery = delivery;
2327
- const refresh = this.performEndpointRefresh();
2328
- this.endpointRefresh = refresh;
912
+ this.observationTimer = timer;
913
+ }
914
+ async runObservation(route) {
915
+ const controller = new AbortController();
916
+ this.observationController = controller;
917
+ let observation;
918
+ const timeout = this.timers.setTimeout(() => controller.abort(), this.observationTimeoutMs);
919
+ timeout.unref();
2329
920
  try {
2330
- return await refresh;
2331
- }
2332
- finally {
2333
- if (this.endpointRefresh === refresh) {
2334
- this.endpointRefresh = undefined;
2335
- this.endpointRefreshDelivery = undefined;
2336
- }
2337
- }
2338
- }
2339
- async performEndpointRefresh() {
2340
- const refreshFactory = this.refreshFactory;
2341
- if (refreshFactory === undefined)
2342
- return undefined;
2343
- const previousFactory = this.factory;
2344
- const previous = this.identity;
2345
- let candidate;
2346
- try {
2347
- const resolved = await this.resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous);
2348
- candidate = resolved.factory;
2349
- await this.retireEndpointGeneration(previousFactory, previous.endpointGeneration);
2350
- if (this.closing || this.closed) {
2351
- await candidate.close();
2352
- candidate = undefined;
2353
- return undefined;
2354
- }
2355
- const replacementFactory = candidate;
2356
- const refreshedRoutes = [];
2357
- const stagedRoutes = [];
2358
- for (const routeHandle of [...this.trackedRoutes]) {
2359
- if (this.closing || this.closed)
2360
- break;
2361
- try {
2362
- const route = await this.createRoute(routeHandle, replacementFactory, false);
2363
- if (this.closing || this.closed) {
2364
- await this.closeRoute(route);
2365
- break;
2366
- }
2367
- const state = codexRouteState(route.connector.observation());
2368
- if (state === "stale") {
2369
- await this.closeRoute(route);
2370
- throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task was stale on the replacement endpoint.");
2371
- }
2372
- stagedRoutes.push({ routeHandle, route, state });
2373
- }
2374
- catch (error) {
2375
- this.routesRequiringRecovery.add(routeHandle);
2376
- if (isCodexTrustFailure(error))
2377
- throw error;
2378
- }
2379
- }
2380
- if (this.closing || this.closed) {
2381
- await replacementFactory.close();
2382
- candidate = undefined;
2383
- return undefined;
2384
- }
2385
- this.factory = replacementFactory;
2386
- candidate = undefined;
2387
- this.retiredEndpointGeneration = undefined;
2388
- this.endpointUnavailable = true;
2389
- for (const staged of stagedRoutes) {
2390
- this.routes.set(staged.routeHandle, staged.route);
2391
- this.routesRequiringRecovery.delete(staged.routeHandle);
2392
- this.routeRecoveryAttempts.delete(staged.routeHandle);
2393
- refreshedRoutes.push({
2394
- routeHandle: staged.routeHandle,
2395
- state: staged.state,
2396
- });
2397
- }
2398
- const result = {
2399
- event: {
2400
- previous,
2401
- current: this.identity,
2402
- routes: refreshedRoutes,
2403
- },
2404
- delivery: this.endpointRefreshDelivery ?? "callback",
2405
- selectorClaimed: false,
2406
- published: false,
2407
- };
2408
- this.pendingEndpointRefreshEvent = result.event;
2409
- this.pendingEndpointRefreshSelectorClaimed = false;
2410
- if (result.delivery === "callback") {
2411
- result.selectorClaimed = true;
2412
- result.published = true;
2413
- this.publishEndpointRefresh(result.event);
2414
- }
2415
- for (const routeHandle of this.routesRequiringRecovery) {
2416
- if (this.trackedRoutes.has(routeHandle)) {
2417
- this.scheduleRouteRecovery(routeHandle);
2418
- }
2419
- }
2420
- return result;
2421
- }
2422
- catch (error) {
2423
- if (candidate !== undefined && candidate !== previousFactory) {
2424
- await candidate.close();
2425
- }
2426
- throw error;
921
+ observation = await this.operation.observe({
922
+ alias: route.alias,
923
+ hostId: this.hostId,
924
+ registrationId: route.registrationId,
925
+ threadId: route.routeHandle,
926
+ }, controller.signal);
2427
927
  }
2428
928
  finally {
2429
- if (this.callbackDrainFrozen) {
2430
- this.callbackDrainFrozen = false;
2431
- this.drainCallbackQueue();
2432
- }
2433
- }
2434
- }
2435
- async resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous) {
2436
- for (let attempt = 0; attempt < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES; attempt += 1) {
2437
- let candidate;
2438
- try {
2439
- candidate = await refreshFactory();
2440
- validateCodexFactory(candidate);
2441
- if (candidate.hostId !== previous.hostId ||
2442
- candidate.protocol !== previousFactory.protocol) {
2443
- throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The replacement Codex factory moved outside its pinned boundary.");
2444
- }
2445
- if (candidate === previousFactory ||
2446
- candidate.endpointGeneration === previous.endpointGeneration) {
2447
- if (candidate !== previousFactory)
2448
- await candidate.close();
2449
- candidate = undefined;
2450
- if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
2451
- continue;
2452
- }
2453
- throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2454
- }
2455
- return { factory: candidate };
2456
- }
2457
- catch (error) {
2458
- if (candidate !== undefined && candidate !== previousFactory) {
2459
- await candidate.close();
2460
- }
2461
- if (this.isEndpointGenerationChanged(error)) {
2462
- if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
2463
- continue;
2464
- }
2465
- throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2466
- }
2467
- throw error;
2468
- }
2469
- }
2470
- throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
2471
- }
2472
- async retireEndpointGeneration(factory, endpointGeneration) {
2473
- this.callbackDrainFrozen = true;
2474
- this.drainCallbackQueue(true);
2475
- // Fence every operation admitted on the previous immutable factory.
2476
- // Ordinary route creation is blocked by endpointRefresh; joining these
2477
- // exact promises ensures none can be inserted after the close sweep.
2478
- await Promise.allSettled([...this.routeCreations.values()]);
2479
- await Promise.allSettled([...this.routeReleases.values()]);
2480
- this.drainCallbackQueue(true);
2481
- for (const [routeHandle, route] of [...this.routes]) {
2482
- if (route.endpointGeneration !== endpointGeneration)
2483
- continue;
2484
- await this.closeRoute(route);
2485
- if (this.routes.get(routeHandle) === route) {
2486
- this.routes.delete(routeHandle);
2487
- }
2488
- }
2489
- this.drainCallbackQueue(true);
2490
- await factory.close();
2491
- this.drainCallbackQueue(true);
2492
- this.retiredEndpointGeneration = endpointGeneration;
2493
- }
2494
- reserveSelectorRefresh(result) {
2495
- if (result.delivery !== "selector" || result.selectorClaimed) {
2496
- return false;
2497
- }
2498
- result.selectorClaimed = true;
2499
- if (this.pendingEndpointRefreshEvent === result.event) {
2500
- this.pendingEndpointRefreshSelectorClaimed = true;
2501
- }
2502
- return true;
2503
- }
2504
- emitSelectorRefreshFallback(result, staleRouteHandle) {
2505
- if (result.delivery !== "selector" || !result.selectorClaimed)
2506
- return;
2507
- if (result.published)
2508
- return;
2509
- let event = result.event;
2510
- if (staleRouteHandle !== undefined &&
2511
- event.routes.some((route) => route.routeHandle === staleRouteHandle)) {
2512
- event = {
2513
- ...event,
2514
- routes: event.routes.filter((route) => route.routeHandle !== staleRouteHandle),
2515
- };
2516
- result.event = event;
2517
- this.pendingEndpointRefreshEvent = event;
2518
- }
2519
- result.published = true;
2520
- this.publishEndpointRefresh(event);
2521
- }
2522
- publishEndpointRefresh(event) {
2523
- this.emitEndpointRefresh(event);
2524
- this.armEndpointActivationRetry(event);
2525
- }
2526
- armEndpointActivationRetry(event) {
2527
- this.clearEndpointActivationRetry();
2528
- this.endpointActivationRetry = { event, attempt: 0 };
2529
- this.scheduleEndpointActivationRetry();
2530
- }
2531
- scheduleEndpointActivationRetry() {
2532
- const pending = this.endpointActivationRetry;
2533
- if (pending === undefined ||
2534
- this.closing ||
2535
- this.closed ||
2536
- pending.attempt >= 3 ||
2537
- this.pendingEndpointRefreshEvent !== pending.event) {
2538
- this.clearEndpointActivationRetry();
2539
- return;
2540
- }
2541
- const delay = Math.max(DEFAULT_CODEX_RECOVERY_INITIAL_MS, Math.min(this.recoveryMaxMs, this.recoveryInitialMs * 2 ** pending.attempt));
2542
- const timer = setTimeout(() => {
2543
- if (this.endpointActivationRetryTimer !== timer)
2544
- return;
2545
- this.endpointActivationRetryTimer = undefined;
2546
- const current = this.endpointActivationRetry;
2547
- if (current === undefined || current.event !== pending.event)
2548
- return;
2549
- if (this.pendingEndpointRefreshEvent !== current.event) {
2550
- this.clearEndpointActivationRetry();
2551
- return;
2552
- }
2553
- this.emitEndpointRefresh(current.event);
2554
- if (this.endpointActivationRetry === undefined ||
2555
- this.endpointActivationRetry.event !== current.event) {
2556
- return;
2557
- }
2558
- this.endpointActivationRetry = {
2559
- event: current.event,
2560
- attempt: current.attempt + 1,
2561
- };
2562
- this.scheduleEndpointActivationRetry();
2563
- }, delay);
2564
- timer.unref();
2565
- this.endpointActivationRetryTimer = timer;
2566
- }
2567
- clearEndpointActivationRetry() {
2568
- if (this.endpointActivationRetryTimer !== undefined) {
2569
- clearTimeout(this.endpointActivationRetryTimer);
2570
- }
2571
- this.endpointActivationRetryTimer = undefined;
2572
- this.endpointActivationRetry = undefined;
2573
- }
2574
- emitEndpointRefresh(event) {
2575
- const callbacks = this.callbacks;
2576
- if (callbacks?.onEndpointRefresh === undefined)
2577
- return;
2578
- invokeCallback(() => callbacks.onEndpointRefresh?.(event));
2579
- }
2580
- onTurnResult(endpointGeneration, result) {
2581
- const wantsReply = this.expectsReply.get(result.messageId) === true;
2582
- this.expectsReply.delete(result.messageId);
2583
- let replyText;
2584
- if (wantsReply && result.text !== null) {
2585
- const bytes = Buffer.from(result.text, "utf8");
2586
- if (bytes.length <= this.maxReplyBytes) {
2587
- replyText = Buffer.from(bytes).toString("utf8");
2588
- }
2589
- }
2590
- const state = result.outcome === "completed"
2591
- ? "completed"
2592
- : result.outcome === "interrupted"
2593
- ? "cancelled"
2594
- : result.outcome === "expired"
2595
- ? "expired"
2596
- : result.outcome === "failed"
2597
- ? "failed"
2598
- : "ambiguous";
2599
- const safeErrorCode = result.outcome === "completed"
2600
- ? result.replyCode === "REPLY_TOO_LARGE"
2601
- ? "CODEX_REPLY_TOO_LARGE"
2602
- : undefined
2603
- : result.outcome === "expired"
2604
- ? "MESSAGE_EXPIRED"
2605
- : result.outcome === "interrupted"
2606
- ? "CODEX_TURN_INTERRUPTED"
2607
- : result.outcome === "failed"
2608
- ? "CODEX_TURN_FAILED"
2609
- : "CODEX_DELIVERY_AMBIGUOUS";
2610
- this.enqueueCodexCallback({
2611
- type: "delivery",
2612
- endpointGeneration,
2613
- value: {
2614
- messageId: result.messageId,
2615
- state,
2616
- ...(safeErrorCode === undefined ? {} : { safeErrorCode }),
2617
- ...(replyText === undefined ? {} : { replyText }),
929
+ this.timers.clearTimeout(timeout);
930
+ if (this.observationController === controller) {
931
+ this.observationController = undefined;
932
+ }
933
+ }
934
+ const current = this.observedRoutes.get(route.registrationId);
935
+ if (current !== route || this.closing || this.closed)
936
+ return;
937
+ const failed = observation.state === "unobserved";
938
+ current.attempt = failed ? Math.min(current.attempt + 1, 16) : 0;
939
+ const delay = failed
940
+ ? Math.min(this.observationBackoffMaxMs, this.observationPollMs * 2 ** Math.max(0, current.attempt - 1))
941
+ : this.observationPollMs;
942
+ current.nextAt = this.now().getTime() + delay;
943
+ this.emitObservation(current, observation);
944
+ }
945
+ emitObservation(route, observation) {
946
+ const current = this.observedRoutes.get(route.registrationId);
947
+ if (this.callbacks === undefined ||
948
+ current === undefined ||
949
+ current.alias !== route.alias ||
950
+ current.routeHandle !== route.routeHandle) {
951
+ return;
952
+ }
953
+ invokeCallback(() => this.callbacks.onRouteState({
954
+ route: {
955
+ provider: "codex",
956
+ hostId: this.hostId,
957
+ routeHandle: route.routeHandle,
958
+ registrationId: route.registrationId,
2618
959
  },
2619
- });
2620
- }
2621
- queueRouteObservation(route, observation) {
2622
- this.enqueueCodexCallback({
2623
- type: "route",
2624
- endpointGeneration: route.endpointGeneration,
2625
- generation: route.generation,
2626
- routeHandle: route.threadId,
2627
- state: codexRouteState(observation),
2628
- ...(codexRouteSafeCode(observation) === undefined
960
+ state: observation.state,
961
+ observedAt: this.now().toISOString(),
962
+ ...(observation.safeErrorCode === undefined
2629
963
  ? {}
2630
- : { safeErrorCode: codexRouteSafeCode(observation) }),
2631
- });
2632
- }
2633
- enqueueCodexCallback(event) {
2634
- if (this.closed)
2635
- return;
2636
- if (this.callbackQueue.length >= this.maxCallbacks) {
2637
- const routeIndex = this.callbackQueue.findIndex((candidate) => candidate.type === "route");
2638
- if (routeIndex >= 0) {
2639
- this.callbackQueue.splice(routeIndex, 1);
2640
- }
2641
- else {
2642
- // The callback consumer is synchronous and itself queues safely. Drain
2643
- // one exact terminal handoff before accepting another; never drop one.
2644
- this.drainOneCallback();
2645
- }
2646
- }
2647
- this.callbackQueue.push(event);
2648
- if (this.callbackDrainFrozen)
2649
- return;
2650
- if (this.callbackScheduled)
2651
- return;
2652
- this.callbackScheduled = true;
2653
- queueMicrotask(() => {
2654
- this.callbackScheduled = false;
2655
- this.drainCallbackQueue();
2656
- });
2657
- }
2658
- drainOneCallback() {
2659
- const event = this.callbackQueue.shift();
2660
- const callbacks = this.callbacks;
2661
- if (event === undefined || callbacks === undefined)
2662
- return;
2663
- if (event.endpointGeneration !== this.factory.endpointGeneration ||
2664
- this.retiredEndpointGeneration === event.endpointGeneration) {
2665
- return;
2666
- }
2667
- if (event.type === "delivery") {
2668
- invokeCallback(() => callbacks.onDelivery(event.value));
2669
- }
2670
- else {
2671
- if (this.routes.get(event.routeHandle)?.generation !== event.generation) {
2672
- return;
2673
- }
2674
- invokeCallback(() => callbacks.onRouteState({
2675
- endpoint: callbackEndpoint({
2676
- provider: "codex",
2677
- hostId: this.identity.hostId,
2678
- endpointGeneration: event.endpointGeneration,
2679
- }, event.routeHandle),
2680
- state: event.state,
2681
- ...(event.safeErrorCode === undefined
2682
- ? {}
2683
- : { safeErrorCode: event.safeErrorCode }),
2684
- }));
2685
- }
2686
- }
2687
- drainCallbackQueue(force = false) {
2688
- if (this.callbackDrainFrozen && !force)
2689
- return;
2690
- while (this.callbackQueue.length > 0)
2691
- this.drainOneCallback();
2692
- }
2693
- async closeRoute(route) {
2694
- let observation = route.connector.observation();
2695
- if (observation.connection === "ready") {
2696
- try {
2697
- route.connector.cancelQueuedMessages(route.connector.guard());
2698
- }
2699
- catch {
2700
- throw new BridgeError("CODEX_PROVIDER_CLEANUP_AMBIGUOUS", "The exact Codex queue could not be safely cancelled.");
2701
- }
2702
- let guard = route.connector.guard();
2703
- let externalTurnObserved = false;
2704
- if (guard.activeTurnId !== null &&
2705
- (guard.status === "active" || guard.status === "waiting_approval")) {
2706
- try {
2707
- await route.connector.interruptOwnedTurn(guard);
2708
- }
2709
- catch (error) {
2710
- if (error instanceof CodexConnectorError &&
2711
- error.code === "TURN_NOT_OWNED") {
2712
- // An approval request or turn notification can expose another
2713
- // Desktop client's turn ID. The connector verifies ownership
2714
- // before issuing RPC, so release must detach without waiting for
2715
- // or attempting to control that external work.
2716
- externalTurnObserved = true;
2717
- }
2718
- else {
2719
- throw new BridgeError("CODEX_PROVIDER_CLEANUP_AMBIGUOUS", "The exact bridge-owned Codex turn did not confirm interruption.");
2720
- }
2721
- }
2722
- }
2723
- if (!externalTurnObserved) {
2724
- guard = await this.waitForOwnedTurnToSettle(route.connector);
2725
- observation = route.connector.observation();
2726
- if (guard.activeTurnId !== null ||
2727
- observation.hasActiveTurn ||
2728
- observation.queueDepth !== 0) {
2729
- throw new BridgeError("CODEX_PROVIDER_CLEANUP_AMBIGUOUS", "The exact bridge-owned Codex work did not reach a terminal state.");
2730
- }
2731
- if (observation.routeStatus !== "active" &&
2732
- observation.routeStatus !== "waiting_approval" &&
2733
- observation.routeStatus !== "starting" &&
2734
- observation.routeStatus !== "interrupting") {
2735
- await route.connector
2736
- .unsubscribeThread(route.connector.guard())
2737
- .catch(() => undefined);
2738
- }
2739
- }
2740
- }
2741
- await route.connector.close();
2742
- if (!route.transport.cleanupConfirmed) {
2743
- await route.transport.close();
2744
- }
2745
- if (!route.transport.cleanupConfirmed) {
2746
- throw new BridgeError("CODEX_PROVIDER_CLEANUP_FAILED", "The exact owned Codex proxy did not confirm termination.");
2747
- }
2748
- }
2749
- async waitForOwnedTurnToSettle(connector) {
2750
- const deadline = Date.now() + this.cleanupTimeoutMs;
2751
- while (true) {
2752
- const guard = connector.guard();
2753
- if (guard.activeTurnId === null)
2754
- return guard;
2755
- if (connector.observation().connection !== "ready" ||
2756
- Date.now() >= deadline) {
2757
- throw new BridgeError("CODEX_PROVIDER_CLEANUP_TIMEOUT", "The exact bridge-owned Codex turn did not confirm termination before the cleanup deadline.");
2758
- }
2759
- await new Promise((resolve) => {
2760
- setTimeout(resolve, this.cleanupPollMs);
2761
- });
2762
- }
964
+ : { safeErrorCode: observation.safeErrorCode }),
965
+ }));
2763
966
  }
2764
967
  }
2765
968
  export function createLocalCodexGatewayProvider(options) {