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