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