agent-embassy 1.7.1 → 1.8.1

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