agent-embassy 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/CONTRIBUTING.md +25 -7
- package/README.md +66 -13
- package/README.zh-CN.md +41 -13
- package/SECURITY.md +80 -42
- package/assets/live-dashboard/app.css +14 -5
- package/dist/src/gateway/claude-helper-protocol.d.ts +3 -0
- package/dist/src/gateway/claude-helper-protocol.js +15 -1
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -0
- package/dist/src/gateway/claude-helper-supervisor.js +24 -1
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +0 -2
- package/dist/src/gateway/claude-runtime.js +6 -17
- 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 +4 -1
- package/dist/src/gateway/cli-copy.en.js +6 -3
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +2 -0
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +4 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +6 -3
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +2 -5
- package/dist/src/gateway/cli.js +49 -73
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +1 -7
- package/dist/src/gateway/codex-app-server.js +10 -53
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +10 -3
- package/dist/src/gateway/codex-local-transport.js +25 -9
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +0 -27
- package/dist/src/gateway/compatibility.js +1 -101
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/config.d.ts +0 -3
- package/dist/src/gateway/config.js +4 -12
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +11 -20
- package/dist/src/gateway/control.js +50 -47
- 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 +12 -7
- package/dist/src/gateway/dashboard-copy.en.js +26 -21
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +12 -7
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +12 -7
- package/dist/src/gateway/dashboard-copy.zh-CN.js +26 -21
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +7 -9
- package/dist/src/gateway/dashboard-model.js +47 -16
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.js +10 -10
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +65 -55
- package/dist/src/gateway/live-dashboard-command.d.ts +5 -1
- package/dist/src/gateway/live-dashboard-command.js +18 -6
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +3 -4
- package/dist/src/gateway/live-dashboard-http.js +32 -93
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/live-dashboard-protocol.d.ts +1 -5
- package/dist/src/gateway/live-dashboard-protocol.js +0 -42
- package/dist/src/gateway/live-dashboard-protocol.js.map +1 -1
- package/dist/src/gateway/live-dashboard-server.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-server.js +18 -4
- package/dist/src/gateway/live-dashboard-server.js.map +1 -1
- package/dist/src/gateway/live-dashboard-stream.js +26 -25
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
- package/dist/src/gateway/live-dashboard.d.ts +3 -6
- package/dist/src/gateway/live-dashboard.js +6 -42
- package/dist/src/gateway/live-dashboard.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +16 -0
- package/dist/src/gateway/provenance-envelope.js +80 -0
- package/dist/src/gateway/provenance-envelope.js.map +1 -0
- package/dist/src/gateway/providers.d.ts +40 -33
- package/dist/src/gateway/providers.js +683 -302
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +2 -0
- package/dist/src/gateway/server.js +8 -5
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +103 -23
- package/dist/src/gateway/service.js +1346 -272
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +39 -3
- package/dist/src/gateway/store.js +604 -44
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +90 -9
- package/dist/src/gateway/types.js +6 -0
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +155 -56
- package/package.json +3 -2
- package/skills/embassy-peer/SKILL.md +36 -7
- package/dist/src/gateway/claude-compatibility-scratch.d.ts +0 -18
- package/dist/src/gateway/claude-compatibility-scratch.js +0 -158
- package/dist/src/gateway/claude-compatibility-scratch.js.map +0 -1
- package/dist/src/gateway/live-dashboard-bootstrap.d.ts +0 -36
- package/dist/src/gateway/live-dashboard-bootstrap.js +0 -176
- package/dist/src/gateway/live-dashboard-bootstrap.js.map +0 -1
- package/docs/DESIGN.md +0 -134
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { createHash
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { BridgeError } from "../errors.js";
|
|
4
4
|
import { CLAUDE_PEER_COMPATIBILITY, ClaudePeerAdapter, } from "./claude-peer.js";
|
|
5
|
-
import {
|
|
6
|
-
import { compatibilityProbeNames, sharesCompatibilityMajor, } from "./compatibility.js";
|
|
5
|
+
import { certifiedCompatibilityVersions, compatibilityProbeNames, evaluateCompatibilityAttestation, sharesCompatibilityMajor, } from "./compatibility.js";
|
|
7
6
|
import { CODEX_APP_SERVER_WRITABLE_VERSIONS, CodexAppServerConnector, CodexConnectorError, } from "./codex-app-server.js";
|
|
8
7
|
import { LocalCodexTransportError } from "./codex-local-transport.js";
|
|
9
8
|
import { ClaudeNativeHelperSupervisor, } from "./claude-helper-supervisor.js";
|
|
10
9
|
import { isDashboardLocale } from "./locale.js";
|
|
10
|
+
import { composeProvenanceEnvelope } from "./provenance-envelope.js";
|
|
11
11
|
const LOCAL_HOST = "this-mac";
|
|
12
12
|
const NATIVE_CLAUDE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
13
13
|
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
@@ -22,10 +22,42 @@ const DEFAULT_CODEX_CLEANUP_TIMEOUT_MS = 5_000;
|
|
|
22
22
|
const DEFAULT_CODEX_CLEANUP_POLL_MS = 25;
|
|
23
23
|
const DEFAULT_CODEX_RECOVERY_INITIAL_MS = 250;
|
|
24
24
|
const DEFAULT_CODEX_RECOVERY_MAX_MS = 5_000;
|
|
25
|
+
const MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES = 3;
|
|
25
26
|
const COMPATIBILITY_PROBE_THREAD_ID = "00000000-0000-7000-8000-000000000000";
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const CLAUDE_CLEAN_PREWRITE_RETRY_CODES = new Set([
|
|
28
|
+
"CLAUDE_PEER_TARGET_UNKNOWN",
|
|
29
|
+
"CLAUDE_PEER_TARGET_STALE",
|
|
30
|
+
"CLAUDE_PEER_TARGET_CHANGED",
|
|
31
|
+
"CLAUDE_PEER_WORKSPACE_UNATTESTED",
|
|
32
|
+
]);
|
|
33
|
+
function claudeCleanPrewriteResult(error) {
|
|
34
|
+
if (error instanceof BridgeError &&
|
|
35
|
+
error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
|
|
36
|
+
return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
|
|
37
|
+
}
|
|
38
|
+
if (error instanceof BridgeError &&
|
|
39
|
+
CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
|
|
40
|
+
return { state: "deferred", safeErrorCode: error.code };
|
|
41
|
+
}
|
|
42
|
+
const systemCode = typeof error === "object" &&
|
|
43
|
+
error !== null &&
|
|
44
|
+
"code" in error &&
|
|
45
|
+
typeof error.code === "string"
|
|
46
|
+
? error.code
|
|
47
|
+
: undefined;
|
|
48
|
+
return {
|
|
49
|
+
state: "failed",
|
|
50
|
+
safeErrorCode: error instanceof BridgeError
|
|
51
|
+
? error.code
|
|
52
|
+
: systemCode === "ENOENT"
|
|
53
|
+
? "CLAUDE_DISPATCH_PREWRITE_PATH_MISSING"
|
|
54
|
+
: systemCode === "EACCES" || systemCode === "EPERM"
|
|
55
|
+
? "CLAUDE_DISPATCH_PREWRITE_ACCESS_DENIED"
|
|
56
|
+
: systemCode === "ETIMEDOUT"
|
|
57
|
+
? "CLAUDE_DISPATCH_PREWRITE_TIMEOUT"
|
|
58
|
+
: "CLAUDE_DISPATCH_PREWRITE_FAILED",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
29
61
|
function passedProbe(name) {
|
|
30
62
|
return { name, outcome: "pass" };
|
|
31
63
|
}
|
|
@@ -73,7 +105,8 @@ function invokeCallback(operation) {
|
|
|
73
105
|
}
|
|
74
106
|
}
|
|
75
107
|
function validateAttestedClaudeRuntime(runtime) {
|
|
76
|
-
if (
|
|
108
|
+
if (runtime.claudeCodeVersion !==
|
|
109
|
+
CLAUDE_PEER_COMPATIBILITY.claudeCodeVersion ||
|
|
77
110
|
[
|
|
78
111
|
runtime.claudeExecutable,
|
|
79
112
|
runtime.sessionsDir,
|
|
@@ -110,7 +143,6 @@ export class LocalClaudeGatewayProvider {
|
|
|
110
143
|
now;
|
|
111
144
|
selectedStateRoots = new Map();
|
|
112
145
|
nativeHelpers;
|
|
113
|
-
compatibilityScratchFactory;
|
|
114
146
|
discovered = new Map();
|
|
115
147
|
selected = new Map();
|
|
116
148
|
/** Exact live same-UID sessions observed through native inbound frames. */
|
|
@@ -143,7 +175,6 @@ export class LocalClaudeGatewayProvider {
|
|
|
143
175
|
retiredNativeCodexListener;
|
|
144
176
|
/** Fences discovery callbacks and monitor restarts across listener rotation. */
|
|
145
177
|
nativeCodexSuccessionFreeze;
|
|
146
|
-
compatibilityCertificationInFlight = false;
|
|
147
178
|
initialized = false;
|
|
148
179
|
closed = false;
|
|
149
180
|
constructor(options) {
|
|
@@ -169,9 +200,6 @@ export class LocalClaudeGatewayProvider {
|
|
|
169
200
|
locale,
|
|
170
201
|
deliveryNotices,
|
|
171
202
|
})))(options.runtime, options.locale ?? "en", options.deliveryNotices ?? "merged");
|
|
172
|
-
this.compatibilityScratchFactory =
|
|
173
|
-
options.compatibilityScratchFactory ??
|
|
174
|
-
(() => startClaudeCompatibilityScratch(options.runtime));
|
|
175
203
|
this.nativeHelpers =
|
|
176
204
|
options.nativeHelpers === undefined
|
|
177
205
|
? undefined
|
|
@@ -222,90 +250,6 @@ export class LocalClaudeGatewayProvider {
|
|
|
222
250
|
];
|
|
223
251
|
}
|
|
224
252
|
}
|
|
225
|
-
async runCompatibilityCertification(input) {
|
|
226
|
-
void input.routeHandle;
|
|
227
|
-
void input.withTurn;
|
|
228
|
-
const depth = "wire";
|
|
229
|
-
let scratch;
|
|
230
|
-
let listener;
|
|
231
|
-
if (this.compatibilityCertificationInFlight) {
|
|
232
|
-
return {
|
|
233
|
-
depth,
|
|
234
|
-
outcome: "fail",
|
|
235
|
-
certifiedAt: new Date(this.now()).toISOString(),
|
|
236
|
-
safeErrorCode: "CLAUDE_CERTIFICATION_BUSY",
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
this.compatibilityCertificationInFlight = true;
|
|
240
|
-
try {
|
|
241
|
-
this.assertReady();
|
|
242
|
-
scratch = await this.compatibilityScratchFactory();
|
|
243
|
-
const deadlineAt = this.now() + CLAUDE_CERTIFICATION_TIMEOUT_MS;
|
|
244
|
-
let targetId;
|
|
245
|
-
while (this.now() < deadlineAt) {
|
|
246
|
-
scratch.assertRunning();
|
|
247
|
-
const discovery = await this.peer.discover();
|
|
248
|
-
if (discovery.truncated) {
|
|
249
|
-
throw new BridgeError("CLAUDE_CERTIFICATION_DISCOVERY_INCOMPLETE", "The scratch session could not be proven from a complete registry scan.", true);
|
|
250
|
-
}
|
|
251
|
-
const matches = discovery.peers.filter((peer) => peer.alias === scratch?.name && peer.targetId === scratch?.sessionId);
|
|
252
|
-
if (matches.length > 1) {
|
|
253
|
-
throw new BridgeError("CLAUDE_CERTIFICATION_TARGET_COLLISION", "The scratch session identity was not unique.");
|
|
254
|
-
}
|
|
255
|
-
if (matches.length === 1) {
|
|
256
|
-
targetId = matches[0]?.targetId;
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
await new Promise((resolve) => {
|
|
260
|
-
const timer = setTimeout(resolve, CLAUDE_CERTIFICATION_DISCOVERY_POLL_MS);
|
|
261
|
-
timer.unref();
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
if (targetId === undefined) {
|
|
265
|
-
throw new BridgeError("CLAUDE_CERTIFICATION_TARGET_UNAVAILABLE", "The bounded scratch session did not publish a discoverable peer record.", true);
|
|
266
|
-
}
|
|
267
|
-
await this.peer.assertTargetWorkspaceDisjoint(targetId, input.controllerStateRoot);
|
|
268
|
-
listener = await this.peer.listen({
|
|
269
|
-
onMessage: () => undefined,
|
|
270
|
-
onReceipt: () => undefined,
|
|
271
|
-
});
|
|
272
|
-
const sent = await this.peer.send(targetId, "[embassy compat-certify] ignore", {
|
|
273
|
-
listener,
|
|
274
|
-
receiptDeadlineAt: deadlineAt,
|
|
275
|
-
});
|
|
276
|
-
if (sent.transportStatus !== "transport_written" ||
|
|
277
|
-
sent.receiptStatus !== "pending") {
|
|
278
|
-
throw new BridgeError("CLAUDE_CERTIFICATION_WRITE_UNCONFIRMED", "The scratch diagnostic frame was not accepted for receipt tracking.", true);
|
|
279
|
-
}
|
|
280
|
-
// A direct `crossSessionInbound: accept` write emits no native
|
|
281
|
-
// peer_message_status frame. That status family describes the optional
|
|
282
|
-
// approval lifecycle, not universal delivery acknowledgement. For this
|
|
283
|
-
// wire-depth certification, the strongest portable evidence is the
|
|
284
|
-
// confirmed write to the exact, uniquely discovered scratch session.
|
|
285
|
-
return {
|
|
286
|
-
depth,
|
|
287
|
-
outcome: "pass",
|
|
288
|
-
certifiedAt: new Date(this.now()).toISOString(),
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
const observedCode = error instanceof BridgeError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code)
|
|
293
|
-
? error.code
|
|
294
|
-
: "CLAUDE_CERTIFICATION_FAILED";
|
|
295
|
-
return {
|
|
296
|
-
depth,
|
|
297
|
-
outcome: "fail",
|
|
298
|
-
certifiedAt: new Date(this.now()).toISOString(),
|
|
299
|
-
safeErrorCode: observedCode,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
finally {
|
|
303
|
-
await listener?.close().catch(() => undefined);
|
|
304
|
-
await scratch?.close().catch(() => undefined);
|
|
305
|
-
await this.peer.discover().catch(() => undefined);
|
|
306
|
-
this.compatibilityCertificationInFlight = false;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
253
|
async initialize(callbacks) {
|
|
310
254
|
if (this.closed) {
|
|
311
255
|
throw new BridgeError("CLAUDE_PROVIDER_CLOSED", "The local Claude provider is closed.");
|
|
@@ -881,22 +825,22 @@ export class LocalClaudeGatewayProvider {
|
|
|
881
825
|
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
882
826
|
}
|
|
883
827
|
if (input.authorization === "native_reply") {
|
|
884
|
-
try {
|
|
885
|
-
await this.refreshClaudeDiscovery();
|
|
886
|
-
}
|
|
887
|
-
catch {
|
|
888
|
-
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
889
|
-
}
|
|
890
828
|
const observedRoute = this.nativeInbound.get(input.binding.routeHandle);
|
|
891
|
-
const current = this.discovered.get(input.binding.routeHandle);
|
|
892
829
|
if (observedRoute === undefined ||
|
|
893
|
-
observedRoute.listenerGeneration !== activeListener.generation
|
|
894
|
-
current?.alias !== observedRoute.alias) {
|
|
830
|
+
observedRoute.listenerGeneration !== activeListener.generation) {
|
|
895
831
|
this.nativeInbound.delete(input.binding.routeHandle);
|
|
896
832
|
return { state: "failed", safeErrorCode: "CLAUDE_NATIVE_REPLY_STALE" };
|
|
897
833
|
}
|
|
898
834
|
}
|
|
899
|
-
else
|
|
835
|
+
else {
|
|
836
|
+
const selectedAlias = this.selected.get(input.binding.routeHandle);
|
|
837
|
+
const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
|
|
838
|
+
if (selectedAlias === undefined ||
|
|
839
|
+
stateRoot === undefined) {
|
|
840
|
+
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (activeListener.registration?.alias !== input.sourceAlias) {
|
|
900
844
|
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
901
845
|
}
|
|
902
846
|
if (this.activeNativeCodexListener !== activeListener ||
|
|
@@ -912,15 +856,44 @@ export class LocalClaudeGatewayProvider {
|
|
|
912
856
|
this.providerIdByGatewayId.has(input.messageId)) {
|
|
913
857
|
return { state: "failed", safeErrorCode: "CLAUDE_RECEIPT_CAPACITY" };
|
|
914
858
|
}
|
|
915
|
-
|
|
916
|
-
|
|
859
|
+
let content;
|
|
860
|
+
try {
|
|
861
|
+
content = composeProvenanceEnvelope({
|
|
862
|
+
direction: "claude",
|
|
863
|
+
sourceAlias: input.sourceAlias,
|
|
864
|
+
targetAlias: input.targetAlias,
|
|
865
|
+
conversationId: input.conversationId,
|
|
866
|
+
body: input.text,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
if (error instanceof BridgeError &&
|
|
871
|
+
(error.code === "PROVENANCE_ENVELOPE_INVALID" ||
|
|
872
|
+
error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")) {
|
|
873
|
+
return { state: "failed", safeErrorCode: error.code };
|
|
874
|
+
}
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
917
877
|
if (input.authorization === "selected_route") {
|
|
878
|
+
const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
|
|
879
|
+
if (stateRoot === undefined) {
|
|
880
|
+
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
881
|
+
}
|
|
918
882
|
this.selectedDispatchEpoch.set(input.binding.routeHandle, (this.selectedDispatchEpoch.get(input.binding.routeHandle) ?? 0) + 1);
|
|
919
|
-
//
|
|
920
|
-
//
|
|
883
|
+
// The exact workspace attestation is refreshed before every send. A
|
|
884
|
+
// clean prewrite failure remains retryable within the message deadline;
|
|
885
|
+
// retry timing does not depend on the model turn's observed state.
|
|
921
886
|
this.selectedObservationDirty.add(input.binding.routeHandle);
|
|
922
887
|
this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
|
|
888
|
+
try {
|
|
889
|
+
await this.peer.assertTargetWorkspaceDisjoint(input.binding.routeHandle, stateRoot);
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
return claudeCleanPrewriteResult(error);
|
|
893
|
+
}
|
|
923
894
|
}
|
|
895
|
+
this.providerIdByGatewayId.set(input.messageId, "");
|
|
896
|
+
this.pendingTargetByGatewayId.set(input.messageId, input.binding.routeHandle);
|
|
924
897
|
let providerId;
|
|
925
898
|
let trackingFailed = false;
|
|
926
899
|
let terminalDuringSend = false;
|
|
@@ -962,7 +935,7 @@ export class LocalClaudeGatewayProvider {
|
|
|
962
935
|
// listener's bounded timeout is authoritative and may settle it later.
|
|
963
936
|
};
|
|
964
937
|
try {
|
|
965
|
-
const result = await this.peer.send(input.binding.routeHandle,
|
|
938
|
+
const result = await this.peer.send(input.binding.routeHandle, content, {
|
|
966
939
|
listener: activeListener.listener,
|
|
967
940
|
receiptDeadlineAt: deadline,
|
|
968
941
|
onTransportStatus,
|
|
@@ -1017,11 +990,7 @@ export class LocalClaudeGatewayProvider {
|
|
|
1017
990
|
}
|
|
1018
991
|
this.providerIdByGatewayId.delete(input.messageId);
|
|
1019
992
|
this.pendingTargetByGatewayId.delete(input.messageId);
|
|
1020
|
-
|
|
1021
|
-
error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
|
|
1022
|
-
return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
|
|
1023
|
-
}
|
|
1024
|
-
return { state: "failed", safeErrorCode: "CLAUDE_DISPATCH_REJECTED" };
|
|
993
|
+
return claudeCleanPrewriteResult(error);
|
|
1025
994
|
}
|
|
1026
995
|
}
|
|
1027
996
|
async releaseRoute(routeHandle) {
|
|
@@ -1483,29 +1452,16 @@ export class LocalClaudeGatewayProvider {
|
|
|
1483
1452
|
this.discovered.set(peer.targetId, row);
|
|
1484
1453
|
rows.push(row);
|
|
1485
1454
|
}
|
|
1486
|
-
for (const [routeHandle, route] of this.nativeInbound) {
|
|
1487
|
-
const current = this.discovered.get(routeHandle);
|
|
1488
|
-
if (current !== undefined) {
|
|
1489
|
-
// The stable session UUID owns native reply authority. A rename only
|
|
1490
|
-
// updates its current public coordinate.
|
|
1491
|
-
this.nativeInbound.set(routeHandle, {
|
|
1492
|
-
alias: current.alias,
|
|
1493
|
-
listenerGeneration: route.listenerGeneration,
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
else if (!discovery.truncated) {
|
|
1497
|
-
this.nativeInbound.delete(routeHandle);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
1455
|
for (const [routeHandle, selectedAlias] of this.selected) {
|
|
1501
1456
|
const row = this.discovered.get(routeHandle);
|
|
1502
1457
|
if (row === undefined) {
|
|
1503
1458
|
this.emitClaudeRouteObservation(routeHandle, "busy", "CLAUDE_PEER_NOT_OBSERVED", true);
|
|
1504
1459
|
}
|
|
1505
1460
|
else {
|
|
1506
|
-
if (row.alias !== selectedAlias) {
|
|
1461
|
+
if (!discovery.truncated && row.alias !== selectedAlias) {
|
|
1507
1462
|
// A Claude rename changes only the live name index. The native
|
|
1508
|
-
// session UUID remains the selected logical route.
|
|
1463
|
+
// session UUID remains the selected logical route. An incomplete
|
|
1464
|
+
// scan is display evidence only and cannot rename dispatch authority.
|
|
1509
1465
|
this.selected.set(routeHandle, row.alias);
|
|
1510
1466
|
}
|
|
1511
1467
|
if (row.state === "idle" &&
|
|
@@ -1585,13 +1541,13 @@ function validateCodexFactory(factory) {
|
|
|
1585
1541
|
const schema = factory.schemaCompatibility;
|
|
1586
1542
|
const write = factory.writeCompatibility;
|
|
1587
1543
|
const certified = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(factory.appServerVersion);
|
|
1588
|
-
const
|
|
1589
|
-
schema.observedSchemaCandidate === true &&
|
|
1544
|
+
const schemaCandidate = schema.observedSchemaCandidate === true &&
|
|
1590
1545
|
CODEX_APP_SERVER_WRITABLE_VERSIONS.some((version) => sharesCompatibilityMajor(version, factory.appServerVersion));
|
|
1591
1546
|
if (factory.hostId !== LOCAL_HOST ||
|
|
1592
1547
|
factory.protocol !== "codex-app-server" ||
|
|
1593
1548
|
factory.protocolVersion !== factory.appServerVersion ||
|
|
1594
|
-
(!certified &&
|
|
1549
|
+
(!certified &&
|
|
1550
|
+
(!schemaCandidate || write !== null || factory.writableReady)) ||
|
|
1595
1551
|
schema.appServerVersion !== factory.appServerVersion ||
|
|
1596
1552
|
schema.endpointGeneration !== factory.endpointGeneration ||
|
|
1597
1553
|
schema.protocol !== "app-server-v2-stable" ||
|
|
@@ -1638,10 +1594,8 @@ function codexRouteSafeCode(observation) {
|
|
|
1638
1594
|
return undefined;
|
|
1639
1595
|
}
|
|
1640
1596
|
export class LocalCodexGatewayProvider {
|
|
1641
|
-
identity;
|
|
1642
|
-
protocol;
|
|
1643
|
-
protocolVersion;
|
|
1644
1597
|
factory;
|
|
1598
|
+
refreshFactory;
|
|
1645
1599
|
maxCallbacks;
|
|
1646
1600
|
maxRoutes;
|
|
1647
1601
|
maxReplyBytes;
|
|
@@ -1651,32 +1605,37 @@ export class LocalCodexGatewayProvider {
|
|
|
1651
1605
|
recoveryMaxMs;
|
|
1652
1606
|
now;
|
|
1653
1607
|
routes = new Map();
|
|
1608
|
+
/** Exact public alias authority retained across internal route recovery. */
|
|
1609
|
+
routeAliases = new Map();
|
|
1654
1610
|
routeCreations = new Map();
|
|
1655
1611
|
routeReleases = new Map();
|
|
1656
1612
|
routeRecoveryAttempts = new Map();
|
|
1657
1613
|
routeRecoveryTimers = new Map();
|
|
1658
1614
|
routeRecoveries = new Map();
|
|
1659
1615
|
routesRequiringRecovery = new Set();
|
|
1616
|
+
trackedRoutes = new Set();
|
|
1617
|
+
retiredEndpointGeneration;
|
|
1660
1618
|
expectsReply = new Map();
|
|
1661
|
-
compatibilityCertifyingRoutes = new Set();
|
|
1662
|
-
compatibilityTurns = new Map();
|
|
1663
1619
|
callbackQueue = [];
|
|
1664
1620
|
callbackScheduled = false;
|
|
1665
1621
|
callbacks;
|
|
1622
|
+
endpointRefresh;
|
|
1623
|
+
endpointRefreshDelivery;
|
|
1624
|
+
endpointActivationRetry;
|
|
1625
|
+
endpointActivationRetryTimer;
|
|
1626
|
+
callbackDrainFrozen = false;
|
|
1666
1627
|
compatibilityAttested;
|
|
1628
|
+
endpointUnavailable = false;
|
|
1629
|
+
pendingEndpointAttestation;
|
|
1630
|
+
pendingEndpointRefreshEvent;
|
|
1667
1631
|
initialized = false;
|
|
1668
1632
|
closing = false;
|
|
1669
1633
|
closed = false;
|
|
1670
1634
|
constructor(options) {
|
|
1671
1635
|
validateCodexFactory(options.factory);
|
|
1672
1636
|
this.factory = options.factory;
|
|
1673
|
-
this.
|
|
1674
|
-
|
|
1675
|
-
hostId: exactLocalHost(options.factory.hostId),
|
|
1676
|
-
endpointGeneration: options.factory.endpointGeneration,
|
|
1677
|
-
};
|
|
1678
|
-
this.protocol = options.factory.protocol;
|
|
1679
|
-
this.protocolVersion = options.factory.protocolVersion;
|
|
1637
|
+
this.refreshFactory = options.refreshFactory;
|
|
1638
|
+
exactLocalHost(options.factory.hostId);
|
|
1680
1639
|
this.compatibilityAttested = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(options.factory.appServerVersion);
|
|
1681
1640
|
this.maxCallbacks = positiveBounded(options.maxCallbackEvents, MAX_CODEX_CALLBACKS, 4_096);
|
|
1682
1641
|
this.maxRoutes = positiveBounded(options.maxRoutes, MAX_CODEX_ROUTES, 256);
|
|
@@ -1693,30 +1652,61 @@ export class LocalCodexGatewayProvider {
|
|
|
1693
1652
|
}
|
|
1694
1653
|
this.now = options.now ?? (() => new Date());
|
|
1695
1654
|
}
|
|
1655
|
+
get identity() {
|
|
1656
|
+
return {
|
|
1657
|
+
provider: "codex",
|
|
1658
|
+
hostId: exactLocalHost(this.factory.hostId),
|
|
1659
|
+
endpointGeneration: this.factory.endpointGeneration,
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
get protocol() {
|
|
1663
|
+
return this.factory.protocol;
|
|
1664
|
+
}
|
|
1665
|
+
get protocolVersion() {
|
|
1666
|
+
return this.factory.protocolVersion;
|
|
1667
|
+
}
|
|
1696
1668
|
compatibilitySurface() {
|
|
1697
1669
|
return { surface: "codex", version: this.factory.appServerVersion };
|
|
1698
1670
|
}
|
|
1699
1671
|
acceptCompatibilityAttestation(attestation) {
|
|
1672
|
+
const pending = this.pendingEndpointAttestation;
|
|
1700
1673
|
if (attestation.surface !== "codex" ||
|
|
1701
1674
|
attestation.version !== this.factory.appServerVersion ||
|
|
1702
|
-
attestation.tier
|
|
1675
|
+
attestation.tier !== "certified" ||
|
|
1676
|
+
!CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(attestation.version) ||
|
|
1677
|
+
(this.endpointUnavailable && pending === undefined) ||
|
|
1678
|
+
(pending !== undefined &&
|
|
1679
|
+
(attestation.checkedAt !== pending.checkedAt ||
|
|
1680
|
+
attestation.tier !== pending.tier ||
|
|
1681
|
+
attestation.safeErrorCode !== pending.safeErrorCode ||
|
|
1682
|
+
attestation.probes.length !== pending.probes.length ||
|
|
1683
|
+
attestation.probes.some((probe, index) => probe.name !== pending.probes[index]?.name ||
|
|
1684
|
+
probe.outcome !== pending.probes[index]?.outcome ||
|
|
1685
|
+
probe.safeErrorCode !== pending.probes[index]?.safeErrorCode)))) {
|
|
1703
1686
|
throw new BridgeError("CODEX_COMPATIBILITY_ATTESTATION_MISMATCH", "The Codex compatibility attestation does not match this endpoint.");
|
|
1704
1687
|
}
|
|
1688
|
+
this.pendingEndpointAttestation = undefined;
|
|
1689
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
1690
|
+
this.clearEndpointActivationRetry();
|
|
1705
1691
|
this.compatibilityAttested = true;
|
|
1692
|
+
this.endpointUnavailable = false;
|
|
1706
1693
|
}
|
|
1707
1694
|
async runCompatibilityProbes() {
|
|
1695
|
+
return await this.runCompatibilityProbesFor(this.factory);
|
|
1696
|
+
}
|
|
1697
|
+
async runCompatibilityProbesFor(factory) {
|
|
1708
1698
|
const [installation, control, initialize, threadList] = compatibilityProbeNames.codex;
|
|
1709
1699
|
let transport;
|
|
1710
1700
|
let connector;
|
|
1711
1701
|
let stage = "transport";
|
|
1712
1702
|
try {
|
|
1713
|
-
transport = await
|
|
1703
|
+
transport = await factory.connectTransport();
|
|
1714
1704
|
stage = "initialize";
|
|
1715
1705
|
connector = await CodexAppServerConnector.connect({
|
|
1716
|
-
compatibility:
|
|
1706
|
+
compatibility: factory.schemaCompatibility,
|
|
1717
1707
|
writesEnabled: false,
|
|
1718
1708
|
route: {
|
|
1719
|
-
endpointGeneration:
|
|
1709
|
+
endpointGeneration: factory.endpointGeneration,
|
|
1720
1710
|
threadId: COMPATIBILITY_PROBE_THREAD_ID,
|
|
1721
1711
|
},
|
|
1722
1712
|
transport,
|
|
@@ -1727,7 +1717,6 @@ export class LocalCodexGatewayProvider {
|
|
|
1727
1717
|
});
|
|
1728
1718
|
stage = "thread_list";
|
|
1729
1719
|
await connector.observeLoadedThread(connector.guard());
|
|
1730
|
-
this.compatibilityAttested = true;
|
|
1731
1720
|
return [
|
|
1732
1721
|
passedProbe(installation),
|
|
1733
1722
|
passedProbe(control),
|
|
@@ -1736,6 +1725,8 @@ export class LocalCodexGatewayProvider {
|
|
|
1736
1725
|
];
|
|
1737
1726
|
}
|
|
1738
1727
|
catch (error) {
|
|
1728
|
+
if (this.isEndpointGenerationChanged(error))
|
|
1729
|
+
throw error;
|
|
1739
1730
|
if (stage === "transport") {
|
|
1740
1731
|
const controlFailure = error instanceof LocalCodexTransportError &&
|
|
1741
1732
|
[
|
|
@@ -1772,107 +1763,6 @@ export class LocalCodexGatewayProvider {
|
|
|
1772
1763
|
}
|
|
1773
1764
|
}
|
|
1774
1765
|
}
|
|
1775
|
-
async runCompatibilityCertification(input) {
|
|
1776
|
-
void input.controllerStateRoot;
|
|
1777
|
-
const certifiedAt = this.now().toISOString();
|
|
1778
|
-
const depth = input.withTurn ? "turn" : "thread_ops";
|
|
1779
|
-
const routeHandle = input.routeHandle;
|
|
1780
|
-
if (routeHandle === undefined || this.compatibilityCertifyingRoutes.has(routeHandle)) {
|
|
1781
|
-
return {
|
|
1782
|
-
depth,
|
|
1783
|
-
outcome: "fail",
|
|
1784
|
-
certifiedAt,
|
|
1785
|
-
safeErrorCode: "CODEX_CERTIFICATION_ROUTE_REQUIRED",
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
this.assertReady();
|
|
1789
|
-
const route = this.routes.get(routeHandle);
|
|
1790
|
-
if (route === undefined || !this.observeRouteSuccessionBarrier(routeHandle).clean) {
|
|
1791
|
-
return {
|
|
1792
|
-
depth,
|
|
1793
|
-
outcome: "fail",
|
|
1794
|
-
certifiedAt,
|
|
1795
|
-
safeErrorCode: "CODEX_CERTIFICATION_ROUTE_BUSY",
|
|
1796
|
-
};
|
|
1797
|
-
}
|
|
1798
|
-
this.compatibilityCertifyingRoutes.add(routeHandle);
|
|
1799
|
-
try {
|
|
1800
|
-
const loaded = await route.connector.observeLoadedThread(route.connector.guard());
|
|
1801
|
-
if (!loaded.selectedThreadLoaded) {
|
|
1802
|
-
return {
|
|
1803
|
-
depth,
|
|
1804
|
-
outcome: "fail",
|
|
1805
|
-
certifiedAt: this.now().toISOString(),
|
|
1806
|
-
safeErrorCode: "CODEX_CERTIFICATION_THREAD_UNAVAILABLE",
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
await route.connector.certifyThreadOperations(route.connector.guard());
|
|
1810
|
-
if (!input.withTurn) {
|
|
1811
|
-
return {
|
|
1812
|
-
depth,
|
|
1813
|
-
outcome: "pass",
|
|
1814
|
-
certifiedAt: this.now().toISOString(),
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
const messageId = `compat_${randomBytes(12).toString("hex")}`;
|
|
1818
|
-
const result = new Promise((resolve) => {
|
|
1819
|
-
const timer = setTimeout(() => {
|
|
1820
|
-
this.compatibilityTurns.delete(messageId);
|
|
1821
|
-
resolve({
|
|
1822
|
-
messageId,
|
|
1823
|
-
outcome: "expired",
|
|
1824
|
-
replyCode: "REPLY_UNAVAILABLE",
|
|
1825
|
-
text: null,
|
|
1826
|
-
});
|
|
1827
|
-
}, COMPATIBILITY_TURN_TIMEOUT_MS);
|
|
1828
|
-
timer.unref();
|
|
1829
|
-
this.compatibilityTurns.set(messageId, { resolve, timer });
|
|
1830
|
-
});
|
|
1831
|
-
try {
|
|
1832
|
-
const disposition = await route.connector.submitMessage(route.connector.guard(), {
|
|
1833
|
-
deadlineAt: new Date(this.now().getTime() + COMPATIBILITY_TURN_TIMEOUT_MS).toISOString(),
|
|
1834
|
-
messageId,
|
|
1835
|
-
text: "reply OK",
|
|
1836
|
-
});
|
|
1837
|
-
if (disposition.disposition !== "started") {
|
|
1838
|
-
throw new BridgeError("CODEX_CERTIFICATION_TURN_NOT_STARTED", "The bounded certification turn did not start on the exact idle route.");
|
|
1839
|
-
}
|
|
1840
|
-
const settled = await result;
|
|
1841
|
-
if (settled.outcome !== "completed" ||
|
|
1842
|
-
settled.text?.trim() !== "OK") {
|
|
1843
|
-
return {
|
|
1844
|
-
depth,
|
|
1845
|
-
outcome: "fail",
|
|
1846
|
-
certifiedAt: this.now().toISOString(),
|
|
1847
|
-
safeErrorCode: "CODEX_CERTIFICATION_TURN_FAILED",
|
|
1848
|
-
};
|
|
1849
|
-
}
|
|
1850
|
-
return {
|
|
1851
|
-
depth,
|
|
1852
|
-
outcome: "pass",
|
|
1853
|
-
certifiedAt: this.now().toISOString(),
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
finally {
|
|
1857
|
-
const pending = this.compatibilityTurns.get(messageId);
|
|
1858
|
-
if (pending !== undefined) {
|
|
1859
|
-
clearTimeout(pending.timer);
|
|
1860
|
-
this.compatibilityTurns.delete(messageId);
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
catch {
|
|
1865
|
-
return {
|
|
1866
|
-
depth,
|
|
1867
|
-
outcome: "fail",
|
|
1868
|
-
certifiedAt: this.now().toISOString(),
|
|
1869
|
-
safeErrorCode: "CODEX_CERTIFICATION_FAILED",
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1872
|
-
finally {
|
|
1873
|
-
this.compatibilityCertifyingRoutes.delete(routeHandle);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
1766
|
async initialize(callbacks) {
|
|
1877
1767
|
if (this.closed || this.initialized) {
|
|
1878
1768
|
throw new BridgeError("CODEX_PROVIDER_INITIALIZATION_REJECTED", "The local Codex provider cannot be initialized in this state.");
|
|
@@ -1888,7 +1778,9 @@ export class LocalCodexGatewayProvider {
|
|
|
1888
1778
|
: { health: "healthy", compatibility: "compatible" };
|
|
1889
1779
|
}
|
|
1890
1780
|
async selectRoute(input) {
|
|
1891
|
-
this.
|
|
1781
|
+
if (!this.initialized || this.closing || this.closed) {
|
|
1782
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
1783
|
+
}
|
|
1892
1784
|
if (!PUBLIC_ALIAS.test(input.alias) ||
|
|
1893
1785
|
!input.alias.endsWith(`@${this.identity.hostId}`)) {
|
|
1894
1786
|
throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex alias is outside the exact local provider boundary.");
|
|
@@ -1896,25 +1788,116 @@ export class LocalCodexGatewayProvider {
|
|
|
1896
1788
|
if (!OPAQUE_ROUTE.test(input.routeHandle)) {
|
|
1897
1789
|
throw new BridgeError("CODEX_ROUTE_INVALID", "The Codex task identifier is outside the exact provider boundary.");
|
|
1898
1790
|
}
|
|
1791
|
+
const retainedAlias = this.routeAliases.get(input.routeHandle);
|
|
1792
|
+
if (retainedAlias !== undefined && retainedAlias !== input.alias) {
|
|
1793
|
+
throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex route is already bound to another exact public alias.");
|
|
1794
|
+
}
|
|
1899
1795
|
this.cancelRouteRecovery(input.routeHandle);
|
|
1900
|
-
|
|
1796
|
+
let refreshResult;
|
|
1797
|
+
let selectorRefreshReserved = false;
|
|
1798
|
+
let route;
|
|
1799
|
+
const stageRefreshedRoute = async (result) => {
|
|
1800
|
+
if (result === undefined ||
|
|
1801
|
+
result.event.outcome !== "compatible") {
|
|
1802
|
+
throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex endpoint replacement was not compatibility-attested.");
|
|
1803
|
+
}
|
|
1804
|
+
refreshResult = result;
|
|
1805
|
+
selectorRefreshReserved = this.reserveSelectorRefresh(result);
|
|
1806
|
+
if (!selectorRefreshReserved) {
|
|
1807
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
|
|
1808
|
+
}
|
|
1809
|
+
try {
|
|
1810
|
+
return await this.ensureRoute(input.routeHandle, false, true);
|
|
1811
|
+
}
|
|
1812
|
+
catch (selectionError) {
|
|
1813
|
+
this.emitSelectorRefreshFallback(result, input.routeHandle);
|
|
1814
|
+
throw selectionError;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
if (this.endpointRefresh !== undefined) {
|
|
1818
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
1819
|
+
}
|
|
1820
|
+
else if (this.endpointUnavailable &&
|
|
1821
|
+
this.pendingEndpointAttestation === undefined) {
|
|
1822
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
this.assertReady();
|
|
1826
|
+
try {
|
|
1827
|
+
route = await this.ensureRoute(input.routeHandle);
|
|
1828
|
+
}
|
|
1829
|
+
catch (error) {
|
|
1830
|
+
const refreshPending = error instanceof BridgeError &&
|
|
1831
|
+
error.code === "CODEX_ENDPOINT_REFRESH_PENDING";
|
|
1832
|
+
if (!this.isEndpointGenerationChanged(error) && !refreshPending) {
|
|
1833
|
+
throw error;
|
|
1834
|
+
}
|
|
1835
|
+
if (this.isEndpointGenerationChanged(error)) {
|
|
1836
|
+
this.endpointUnavailable = true;
|
|
1837
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
1838
|
+
}
|
|
1839
|
+
else if (this.endpointRefresh !== undefined) {
|
|
1840
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (refreshResult === undefined &&
|
|
1847
|
+
(route.endpointGeneration !== this.factory.endpointGeneration ||
|
|
1848
|
+
this.endpointUnavailable ||
|
|
1849
|
+
this.endpointRefresh !== undefined)) {
|
|
1850
|
+
if (this.endpointRefresh === undefined) {
|
|
1851
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
|
|
1852
|
+
}
|
|
1853
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1901
1856
|
const observation = route.connector.observation();
|
|
1902
1857
|
const state = codexRouteState(observation);
|
|
1903
1858
|
if (state === "stale") {
|
|
1904
|
-
|
|
1859
|
+
if (refreshResult !== undefined && selectorRefreshReserved) {
|
|
1860
|
+
// Publish the transition before any asynchronous connector cleanup so
|
|
1861
|
+
// the controller can accept or durably reject the exact replacement
|
|
1862
|
+
// generation. A route which closed after refresh staging is not valid
|
|
1863
|
+
// re-anchoring evidence and must be removed from both the callback and
|
|
1864
|
+
// its retained retry authority.
|
|
1865
|
+
this.emitSelectorRefreshFallback(refreshResult, input.routeHandle);
|
|
1866
|
+
}
|
|
1867
|
+
this.routesRequiringRecovery.add(input.routeHandle);
|
|
1868
|
+
await this.releaseRouteInternal(input.routeHandle);
|
|
1869
|
+
if (this.trackedRoutes.has(input.routeHandle)) {
|
|
1870
|
+
this.scheduleRouteRecovery(input.routeHandle);
|
|
1871
|
+
}
|
|
1905
1872
|
throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task connection closed during route selection.");
|
|
1906
1873
|
}
|
|
1907
|
-
this.
|
|
1874
|
+
this.trackedRoutes.add(input.routeHandle);
|
|
1875
|
+
this.routeAliases.set(input.routeHandle, input.alias);
|
|
1876
|
+
if (refreshResult === undefined) {
|
|
1877
|
+
this.queueRouteObservation(route, observation);
|
|
1878
|
+
}
|
|
1879
|
+
const endpointRefresh = refreshResult === undefined || !selectorRefreshReserved
|
|
1880
|
+
? undefined
|
|
1881
|
+
: refreshResult.event;
|
|
1882
|
+
if (endpointRefresh !== undefined) {
|
|
1883
|
+
this.armEndpointActivationRetry(endpointRefresh);
|
|
1884
|
+
}
|
|
1908
1885
|
return {
|
|
1909
1886
|
routeHandle: input.routeHandle,
|
|
1910
1887
|
state,
|
|
1888
|
+
...(endpointRefresh === undefined ? {} : { endpointRefresh }),
|
|
1911
1889
|
};
|
|
1912
1890
|
}
|
|
1913
1891
|
observeRouteSuccessionBarrier(routeHandle) {
|
|
1914
|
-
this.
|
|
1892
|
+
if (!this.initialized || this.closing || this.closed) {
|
|
1893
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
1894
|
+
}
|
|
1915
1895
|
const route = this.routes.get(routeHandle);
|
|
1916
1896
|
const observation = route?.connector.observation();
|
|
1917
|
-
const routeCreationInFlight = this.routeCreations.has(routeHandle)
|
|
1897
|
+
const routeCreationInFlight = this.routeCreations.has(routeHandle) ||
|
|
1898
|
+
this.routeRecoveries.has(routeHandle) ||
|
|
1899
|
+
(this.endpointRefresh !== undefined && this.trackedRoutes.has(routeHandle)) ||
|
|
1900
|
+
this.pendingEndpointActivationClaims(routeHandle);
|
|
1918
1901
|
const routeReleaseInFlight = this.routeReleases.has(routeHandle);
|
|
1919
1902
|
const pendingReplyCorrelations = this.expectsReply.size;
|
|
1920
1903
|
const pendingCallbacks = Math.max(this.callbackQueue.length, this.callbackScheduled ? 1 : 0);
|
|
@@ -1951,6 +1934,8 @@ export class LocalCodexGatewayProvider {
|
|
|
1951
1934
|
if (!this.initialized ||
|
|
1952
1935
|
this.closing ||
|
|
1953
1936
|
this.closed ||
|
|
1937
|
+
this.endpointUnavailable ||
|
|
1938
|
+
this.endpointRefresh !== undefined ||
|
|
1954
1939
|
!this.compatibilityAttested ||
|
|
1955
1940
|
input.authorization !== "selected_route" ||
|
|
1956
1941
|
!sameEndpoint(input.binding, this.identity)) {
|
|
@@ -1958,12 +1943,12 @@ export class LocalCodexGatewayProvider {
|
|
|
1958
1943
|
}
|
|
1959
1944
|
const route = this.routes.get(input.binding.routeHandle);
|
|
1960
1945
|
if (route === undefined ||
|
|
1961
|
-
this.
|
|
1946
|
+
this.routeAliases.get(input.binding.routeHandle) !== input.targetAlias ||
|
|
1947
|
+
this.routeReleases.has(input.binding.routeHandle) ||
|
|
1948
|
+
route.endpointGeneration !== input.binding.endpointGeneration ||
|
|
1949
|
+
route.endpointGeneration !== this.factory.endpointGeneration) {
|
|
1962
1950
|
return { state: "failed", safeErrorCode: "CODEX_ROUTE_UNAVAILABLE" };
|
|
1963
1951
|
}
|
|
1964
|
-
if (this.compatibilityCertifyingRoutes.has(input.binding.routeHandle)) {
|
|
1965
|
-
return { state: "deferred", safeErrorCode: "CODEX_ROUTE_HELD" };
|
|
1966
|
-
}
|
|
1967
1952
|
if (!this.factory.writableReady ||
|
|
1968
1953
|
this.factory.writeCompatibility === null) {
|
|
1969
1954
|
return { state: "failed", safeErrorCode: "CODEX_WRITES_DISABLED" };
|
|
@@ -1978,13 +1963,31 @@ export class LocalCodexGatewayProvider {
|
|
|
1978
1963
|
if (this.expectsReply.has(input.messageId)) {
|
|
1979
1964
|
return { state: "failed", safeErrorCode: "CODEX_MESSAGE_DUPLICATE" };
|
|
1980
1965
|
}
|
|
1966
|
+
let content;
|
|
1967
|
+
try {
|
|
1968
|
+
content = composeProvenanceEnvelope({
|
|
1969
|
+
direction: "codex",
|
|
1970
|
+
sourceAlias: input.sourceAlias,
|
|
1971
|
+
targetAlias: input.targetAlias,
|
|
1972
|
+
conversationId: input.conversationId,
|
|
1973
|
+
body: input.text,
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
catch (error) {
|
|
1977
|
+
const safeErrorCode = error instanceof BridgeError &&
|
|
1978
|
+
(error.code === "PROVENANCE_ENVELOPE_INVALID" ||
|
|
1979
|
+
error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")
|
|
1980
|
+
? error.code
|
|
1981
|
+
: "PROVENANCE_ENVELOPE_INVALID";
|
|
1982
|
+
return { state: "failed", safeErrorCode };
|
|
1983
|
+
}
|
|
1981
1984
|
this.expectsReply.set(input.messageId, input.expectsReply);
|
|
1982
1985
|
try {
|
|
1983
1986
|
const disposition = await route.connector.submitMessage(guard, {
|
|
1984
1987
|
deadlineAt: input.deadlineAt,
|
|
1985
1988
|
messageId: input.messageId,
|
|
1986
1989
|
...(input.steer === true ? { steer: true } : {}),
|
|
1987
|
-
text:
|
|
1990
|
+
text: content,
|
|
1988
1991
|
});
|
|
1989
1992
|
if (disposition.disposition === "deferred") {
|
|
1990
1993
|
this.expectsReply.delete(input.messageId);
|
|
@@ -2019,11 +2022,22 @@ export class LocalCodexGatewayProvider {
|
|
|
2019
2022
|
}
|
|
2020
2023
|
}
|
|
2021
2024
|
async releaseRoute(routeHandle) {
|
|
2025
|
+
if (this.pendingEndpointActivationClaims(routeHandle)) {
|
|
2026
|
+
throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route still belongs to an unaccepted endpoint transition.", true);
|
|
2027
|
+
}
|
|
2028
|
+
this.trackedRoutes.delete(routeHandle);
|
|
2022
2029
|
this.cancelRouteRecovery(routeHandle);
|
|
2023
2030
|
const recovery = this.routeRecoveries.get(routeHandle);
|
|
2024
2031
|
if (recovery !== undefined)
|
|
2025
2032
|
await recovery;
|
|
2033
|
+
const endpointRefresh = this.endpointRefresh;
|
|
2034
|
+
if (endpointRefresh !== undefined)
|
|
2035
|
+
await endpointRefresh;
|
|
2036
|
+
if (this.pendingEndpointActivationClaims(routeHandle)) {
|
|
2037
|
+
throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route acquired endpoint-transition authority during release.", true);
|
|
2038
|
+
}
|
|
2026
2039
|
await this.releaseRouteInternal(routeHandle);
|
|
2040
|
+
this.routeAliases.delete(routeHandle);
|
|
2027
2041
|
}
|
|
2028
2042
|
async releaseRouteInternal(routeHandle) {
|
|
2029
2043
|
const pending = this.routeReleases.get(routeHandle);
|
|
@@ -2053,6 +2067,12 @@ export class LocalCodexGatewayProvider {
|
|
|
2053
2067
|
clearTimeout(timer);
|
|
2054
2068
|
this.routeRecoveryTimers.clear();
|
|
2055
2069
|
this.routesRequiringRecovery.clear();
|
|
2070
|
+
this.clearEndpointActivationRetry();
|
|
2071
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
2072
|
+
this.pendingEndpointAttestation = undefined;
|
|
2073
|
+
const refreshResult = this.endpointRefresh === undefined
|
|
2074
|
+
? []
|
|
2075
|
+
: await Promise.allSettled([this.endpointRefresh]);
|
|
2056
2076
|
const entries = [...this.routes.entries()];
|
|
2057
2077
|
const routeResults = await Promise.allSettled(entries.map(async ([routeHandle]) => this.releaseRoute(routeHandle)));
|
|
2058
2078
|
await Promise.allSettled([
|
|
@@ -2060,7 +2080,8 @@ export class LocalCodexGatewayProvider {
|
|
|
2060
2080
|
...this.routeCreations.values(),
|
|
2061
2081
|
]);
|
|
2062
2082
|
this.drainCallbackQueue();
|
|
2063
|
-
if (
|
|
2083
|
+
if (refreshResult.some((result) => result.status === "rejected") ||
|
|
2084
|
+
routeResults.some((result) => result.status === "rejected")) {
|
|
2064
2085
|
this.closing = false;
|
|
2065
2086
|
throw new BridgeError("CODEX_PROVIDER_CLEANUP_FAILED", "An exact owned Codex turn did not confirm termination.");
|
|
2066
2087
|
}
|
|
@@ -2074,17 +2095,10 @@ export class LocalCodexGatewayProvider {
|
|
|
2074
2095
|
this.closed = true;
|
|
2075
2096
|
this.closing = false;
|
|
2076
2097
|
this.callbackQueue.length = 0;
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
outcome: "abandoned",
|
|
2082
|
-
replyCode: "REPLY_UNAVAILABLE",
|
|
2083
|
-
text: null,
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2086
|
-
this.compatibilityTurns.clear();
|
|
2087
|
-
this.compatibilityCertifyingRoutes.clear();
|
|
2098
|
+
this.pendingEndpointAttestation = undefined;
|
|
2099
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
2100
|
+
this.trackedRoutes.clear();
|
|
2101
|
+
this.routeAliases.clear();
|
|
2088
2102
|
this.expectsReply.clear();
|
|
2089
2103
|
this.callbacks = undefined;
|
|
2090
2104
|
}
|
|
@@ -2092,16 +2106,25 @@ export class LocalCodexGatewayProvider {
|
|
|
2092
2106
|
if (!this.initialized ||
|
|
2093
2107
|
this.closing ||
|
|
2094
2108
|
this.closed ||
|
|
2109
|
+
this.endpointUnavailable ||
|
|
2110
|
+
this.endpointRefresh !== undefined ||
|
|
2095
2111
|
!this.compatibilityAttested) {
|
|
2096
2112
|
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
2097
2113
|
}
|
|
2098
2114
|
}
|
|
2099
|
-
async ensureRoute(threadId) {
|
|
2100
|
-
|
|
2115
|
+
async ensureRoute(threadId, queueInitialObservation = true, allowPendingEndpointAttestation = false) {
|
|
2116
|
+
if (allowPendingEndpointAttestation) {
|
|
2117
|
+
if (!this.initialized || this.closing || this.closed) {
|
|
2118
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
else {
|
|
2122
|
+
this.assertReady();
|
|
2123
|
+
}
|
|
2101
2124
|
const pendingRelease = this.routeReleases.get(threadId);
|
|
2102
2125
|
if (pendingRelease !== undefined) {
|
|
2103
2126
|
await pendingRelease;
|
|
2104
|
-
return await this.ensureRoute(threadId);
|
|
2127
|
+
return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
|
|
2105
2128
|
}
|
|
2106
2129
|
const existing = this.routes.get(threadId);
|
|
2107
2130
|
if (existing !== undefined) {
|
|
@@ -2113,7 +2136,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2113
2136
|
// a fresh one. The identity-checked release lets automatic recovery and
|
|
2114
2137
|
// an explicit selector join the same cleanup safely.
|
|
2115
2138
|
await this.releaseRouteInternal(threadId);
|
|
2116
|
-
return await this.ensureRoute(threadId);
|
|
2139
|
+
return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
|
|
2117
2140
|
}
|
|
2118
2141
|
const pending = this.routeCreations.get(threadId);
|
|
2119
2142
|
if (pending !== undefined)
|
|
@@ -2121,42 +2144,51 @@ export class LocalCodexGatewayProvider {
|
|
|
2121
2144
|
if (this.routes.size + this.routeCreations.size >= this.maxRoutes) {
|
|
2122
2145
|
throw new BridgeError("CODEX_ROUTE_CAPACITY", "The bounded local Codex route table is full.", true);
|
|
2123
2146
|
}
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
const route = await creation;
|
|
2147
|
+
const admittedFactory = this.factory;
|
|
2148
|
+
const creation = (async () => {
|
|
2149
|
+
const route = await this.createRoute(threadId, admittedFactory, queueInitialObservation);
|
|
2128
2150
|
if (this.closing || this.closed) {
|
|
2129
2151
|
await this.closeRoute(route);
|
|
2130
2152
|
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider closed during route creation.");
|
|
2131
2153
|
}
|
|
2154
|
+
if (!allowPendingEndpointAttestation &&
|
|
2155
|
+
(this.endpointUnavailable ||
|
|
2156
|
+
this.endpointRefresh !== undefined ||
|
|
2157
|
+
admittedFactory !== this.factory ||
|
|
2158
|
+
route.endpointGeneration !== this.factory.endpointGeneration)) {
|
|
2159
|
+
await this.closeRoute(route);
|
|
2160
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The Codex endpoint moved during exact route creation.", true);
|
|
2161
|
+
}
|
|
2132
2162
|
this.routes.set(threadId, route);
|
|
2133
2163
|
return route;
|
|
2164
|
+
})();
|
|
2165
|
+
this.routeCreations.set(threadId, creation);
|
|
2166
|
+
try {
|
|
2167
|
+
return await creation;
|
|
2134
2168
|
}
|
|
2135
2169
|
finally {
|
|
2136
2170
|
this.routeCreations.delete(threadId);
|
|
2137
2171
|
}
|
|
2138
2172
|
}
|
|
2139
|
-
async createRoute(threadId) {
|
|
2173
|
+
async createRoute(threadId, factory, queueInitialObservation) {
|
|
2140
2174
|
let transport;
|
|
2141
2175
|
let connector;
|
|
2142
2176
|
const generation = Symbol(threadId);
|
|
2143
2177
|
try {
|
|
2144
|
-
transport = await
|
|
2145
|
-
const writesEnabled =
|
|
2146
|
-
this.factory.writeCompatibility !== null;
|
|
2178
|
+
transport = await factory.connectTransport();
|
|
2179
|
+
const writesEnabled = factory.writableReady && factory.writeCompatibility !== null;
|
|
2147
2180
|
connector = await CodexAppServerConnector.connect({
|
|
2148
|
-
compatibility:
|
|
2149
|
-
this.factory.schemaCompatibility,
|
|
2181
|
+
compatibility: factory.writeCompatibility ?? factory.schemaCompatibility,
|
|
2150
2182
|
writesEnabled,
|
|
2151
2183
|
route: {
|
|
2152
|
-
endpointGeneration:
|
|
2184
|
+
endpointGeneration: factory.endpointGeneration,
|
|
2153
2185
|
threadId,
|
|
2154
2186
|
},
|
|
2155
2187
|
transport,
|
|
2156
2188
|
maxReplyBytes: this.maxReplyBytes,
|
|
2157
2189
|
now: this.now,
|
|
2158
2190
|
onEvent: (event) => this.onConnectorEvent(threadId, generation, event),
|
|
2159
|
-
onTurnResult: (result) => this.onTurnResult(result),
|
|
2191
|
+
onTurnResult: (result) => this.onTurnResult(factory.endpointGeneration, result),
|
|
2160
2192
|
});
|
|
2161
2193
|
const loaded = await connector.observeLoadedThread(connector.guard());
|
|
2162
2194
|
if (!loaded.selectedThreadLoaded) {
|
|
@@ -2165,11 +2197,14 @@ export class LocalCodexGatewayProvider {
|
|
|
2165
2197
|
await connector.resumeThread(connector.guard());
|
|
2166
2198
|
const route = {
|
|
2167
2199
|
connector,
|
|
2200
|
+
endpointGeneration: factory.endpointGeneration,
|
|
2168
2201
|
generation,
|
|
2169
2202
|
threadId,
|
|
2170
2203
|
transport,
|
|
2171
2204
|
};
|
|
2172
|
-
|
|
2205
|
+
if (queueInitialObservation) {
|
|
2206
|
+
this.queueRouteObservation(route, connector.observation());
|
|
2207
|
+
}
|
|
2173
2208
|
return route;
|
|
2174
2209
|
}
|
|
2175
2210
|
catch (error) {
|
|
@@ -2179,6 +2214,8 @@ export class LocalCodexGatewayProvider {
|
|
|
2179
2214
|
else if (transport !== undefined) {
|
|
2180
2215
|
await transport.close().catch(() => undefined);
|
|
2181
2216
|
}
|
|
2217
|
+
if (this.isEndpointGenerationChanged(error))
|
|
2218
|
+
throw error;
|
|
2182
2219
|
if (error instanceof BridgeError)
|
|
2183
2220
|
throw error;
|
|
2184
2221
|
if (error instanceof CodexConnectorError) {
|
|
@@ -2236,6 +2273,19 @@ export class LocalCodexGatewayProvider {
|
|
|
2236
2273
|
return;
|
|
2237
2274
|
}
|
|
2238
2275
|
try {
|
|
2276
|
+
if (this.endpointUnavailable) {
|
|
2277
|
+
if (this.pendingEndpointAttestation !== undefined) {
|
|
2278
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is awaiting controller activation.", true);
|
|
2279
|
+
}
|
|
2280
|
+
const refreshed = await this.refreshEndpoint("callback");
|
|
2281
|
+
if (refreshed?.event.outcome === "compatible" &&
|
|
2282
|
+
refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
|
|
2283
|
+
this.routesRequiringRecovery.delete(threadId);
|
|
2284
|
+
this.routeRecoveryAttempts.delete(threadId);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is not ready.", true);
|
|
2288
|
+
}
|
|
2239
2289
|
const current = this.routes.get(threadId);
|
|
2240
2290
|
if (current !== undefined &&
|
|
2241
2291
|
codexRouteState(current.connector.observation()) === "stale") {
|
|
@@ -2255,7 +2305,17 @@ export class LocalCodexGatewayProvider {
|
|
|
2255
2305
|
this.routeRecoveryAttempts.delete(threadId);
|
|
2256
2306
|
this.queueRouteObservation(replacement, replacement.connector.observation());
|
|
2257
2307
|
}
|
|
2258
|
-
catch {
|
|
2308
|
+
catch (error) {
|
|
2309
|
+
if (this.isEndpointGenerationChanged(error)) {
|
|
2310
|
+
this.endpointUnavailable = true;
|
|
2311
|
+
const refreshed = await this.refreshEndpoint("callback").catch(() => undefined);
|
|
2312
|
+
if (refreshed?.event.outcome === "compatible" &&
|
|
2313
|
+
refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
|
|
2314
|
+
this.routesRequiringRecovery.delete(threadId);
|
|
2315
|
+
this.routeRecoveryAttempts.delete(threadId);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2259
2319
|
this.routeRecoveryAttempts.set(threadId, (this.routeRecoveryAttempts.get(threadId) ?? 0) + 1);
|
|
2260
2320
|
}
|
|
2261
2321
|
}
|
|
@@ -2267,14 +2327,321 @@ export class LocalCodexGatewayProvider {
|
|
|
2267
2327
|
clearTimeout(timer);
|
|
2268
2328
|
this.routeRecoveryTimers.delete(threadId);
|
|
2269
2329
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2330
|
+
isEndpointGenerationChanged(error) {
|
|
2331
|
+
return (error instanceof LocalCodexTransportError &&
|
|
2332
|
+
error.code === "ENDPOINT_GENERATION_CHANGED");
|
|
2333
|
+
}
|
|
2334
|
+
pendingEndpointActivationClaims(routeHandle) {
|
|
2335
|
+
const event = this.pendingEndpointRefreshEvent;
|
|
2336
|
+
return (event !== undefined &&
|
|
2337
|
+
event.current.endpointGeneration === this.factory.endpointGeneration &&
|
|
2338
|
+
event.routes.some((route) => route.routeHandle === routeHandle));
|
|
2339
|
+
}
|
|
2340
|
+
async refreshEndpoint(delivery) {
|
|
2341
|
+
if (this.refreshFactory === undefined || this.closing || this.closed) {
|
|
2342
|
+
return undefined;
|
|
2343
|
+
}
|
|
2344
|
+
const existing = this.endpointRefresh;
|
|
2345
|
+
if (existing !== undefined) {
|
|
2346
|
+
// A selector already running inside the controller lock takes ownership
|
|
2347
|
+
// of the transition result. This avoids waiting on a callback that needs
|
|
2348
|
+
// that same lock while still ensuring exactly one activation path.
|
|
2349
|
+
if (delivery === "selector")
|
|
2350
|
+
this.endpointRefreshDelivery = "selector";
|
|
2351
|
+
return await existing;
|
|
2352
|
+
}
|
|
2353
|
+
this.endpointRefreshDelivery = delivery;
|
|
2354
|
+
const refresh = this.performEndpointRefresh();
|
|
2355
|
+
this.endpointRefresh = refresh;
|
|
2356
|
+
try {
|
|
2357
|
+
return await refresh;
|
|
2358
|
+
}
|
|
2359
|
+
finally {
|
|
2360
|
+
if (this.endpointRefresh === refresh) {
|
|
2361
|
+
this.endpointRefresh = undefined;
|
|
2362
|
+
this.endpointRefreshDelivery = undefined;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
async performEndpointRefresh() {
|
|
2367
|
+
const refreshFactory = this.refreshFactory;
|
|
2368
|
+
if (refreshFactory === undefined)
|
|
2369
|
+
return undefined;
|
|
2370
|
+
const previousFactory = this.factory;
|
|
2371
|
+
const previous = this.identity;
|
|
2372
|
+
let candidate;
|
|
2373
|
+
try {
|
|
2374
|
+
const resolved = await this.resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous);
|
|
2375
|
+
candidate = resolved.factory;
|
|
2376
|
+
const { attestation, identity: candidateIdentity } = resolved;
|
|
2377
|
+
await this.retireEndpointGeneration(previousFactory, previous.endpointGeneration);
|
|
2378
|
+
if (attestation.tier === "incompatible") {
|
|
2379
|
+
await candidate.close();
|
|
2380
|
+
candidate = undefined;
|
|
2381
|
+
this.compatibilityAttested = false;
|
|
2382
|
+
this.pendingEndpointAttestation = undefined;
|
|
2383
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
2384
|
+
for (const routeHandle of this.trackedRoutes) {
|
|
2385
|
+
this.routesRequiringRecovery.add(routeHandle);
|
|
2386
|
+
}
|
|
2387
|
+
if (this.closing || this.closed)
|
|
2388
|
+
return undefined;
|
|
2389
|
+
const result = {
|
|
2390
|
+
event: {
|
|
2391
|
+
outcome: "incompatible",
|
|
2392
|
+
previous,
|
|
2393
|
+
candidate: candidateIdentity,
|
|
2394
|
+
attestation,
|
|
2395
|
+
},
|
|
2396
|
+
delivery: "callback",
|
|
2397
|
+
selectorClaimed: true,
|
|
2398
|
+
};
|
|
2399
|
+
this.emitEndpointRefresh(result.event);
|
|
2400
|
+
for (const routeHandle of this.routesRequiringRecovery) {
|
|
2401
|
+
if (this.trackedRoutes.has(routeHandle)) {
|
|
2402
|
+
this.scheduleRouteRecovery(routeHandle);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return result;
|
|
2406
|
+
}
|
|
2407
|
+
if (this.closing || this.closed) {
|
|
2408
|
+
await candidate.close();
|
|
2409
|
+
candidate = undefined;
|
|
2410
|
+
return undefined;
|
|
2411
|
+
}
|
|
2412
|
+
this.factory = candidate;
|
|
2413
|
+
candidate = undefined;
|
|
2414
|
+
this.retiredEndpointGeneration = undefined;
|
|
2415
|
+
this.compatibilityAttested = false;
|
|
2416
|
+
this.endpointUnavailable = true;
|
|
2417
|
+
this.pendingEndpointAttestation = attestation;
|
|
2418
|
+
const refreshedRoutes = [];
|
|
2419
|
+
for (const routeHandle of [...this.trackedRoutes]) {
|
|
2420
|
+
if (this.closing || this.closed)
|
|
2421
|
+
break;
|
|
2422
|
+
try {
|
|
2423
|
+
const route = await this.createRoute(routeHandle, this.factory, false);
|
|
2424
|
+
if (this.closing || this.closed) {
|
|
2425
|
+
await this.closeRoute(route);
|
|
2426
|
+
break;
|
|
2427
|
+
}
|
|
2428
|
+
const state = codexRouteState(route.connector.observation());
|
|
2429
|
+
if (state === "stale") {
|
|
2430
|
+
await this.closeRoute(route);
|
|
2431
|
+
throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task was stale on the replacement endpoint.");
|
|
2432
|
+
}
|
|
2433
|
+
this.routes.set(routeHandle, route);
|
|
2434
|
+
this.routesRequiringRecovery.delete(routeHandle);
|
|
2435
|
+
this.routeRecoveryAttempts.delete(routeHandle);
|
|
2436
|
+
refreshedRoutes.push({ routeHandle, state });
|
|
2437
|
+
}
|
|
2438
|
+
catch {
|
|
2439
|
+
this.routesRequiringRecovery.add(routeHandle);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
if (this.closing || this.closed)
|
|
2443
|
+
return undefined;
|
|
2444
|
+
const result = {
|
|
2445
|
+
event: {
|
|
2446
|
+
outcome: "compatible",
|
|
2447
|
+
previous,
|
|
2448
|
+
current: this.identity,
|
|
2449
|
+
attestation,
|
|
2450
|
+
routes: refreshedRoutes,
|
|
2451
|
+
},
|
|
2452
|
+
delivery: this.endpointRefreshDelivery ?? "callback",
|
|
2453
|
+
selectorClaimed: false,
|
|
2454
|
+
};
|
|
2455
|
+
if (result.event.outcome === "compatible") {
|
|
2456
|
+
this.pendingEndpointRefreshEvent = result.event;
|
|
2457
|
+
}
|
|
2458
|
+
if (result.delivery === "callback") {
|
|
2459
|
+
result.selectorClaimed = true;
|
|
2460
|
+
this.publishEndpointRefresh(result.event);
|
|
2461
|
+
}
|
|
2462
|
+
for (const routeHandle of this.routesRequiringRecovery) {
|
|
2463
|
+
if (this.trackedRoutes.has(routeHandle)) {
|
|
2464
|
+
this.scheduleRouteRecovery(routeHandle);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return result;
|
|
2468
|
+
}
|
|
2469
|
+
catch (error) {
|
|
2470
|
+
if (candidate !== undefined && candidate !== previousFactory) {
|
|
2471
|
+
await candidate.close().catch(() => undefined);
|
|
2472
|
+
}
|
|
2473
|
+
throw error;
|
|
2474
|
+
}
|
|
2475
|
+
finally {
|
|
2476
|
+
if (this.callbackDrainFrozen) {
|
|
2477
|
+
this.callbackDrainFrozen = false;
|
|
2478
|
+
this.drainCallbackQueue();
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
async resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous) {
|
|
2483
|
+
for (let attempt = 0; attempt < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES; attempt += 1) {
|
|
2484
|
+
let candidate;
|
|
2485
|
+
try {
|
|
2486
|
+
candidate = await refreshFactory();
|
|
2487
|
+
validateCodexFactory(candidate);
|
|
2488
|
+
if (candidate.hostId !== previous.hostId ||
|
|
2489
|
+
candidate.protocol !== previousFactory.protocol) {
|
|
2490
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The replacement Codex factory moved outside its pinned boundary.");
|
|
2491
|
+
}
|
|
2492
|
+
if (candidate === previousFactory ||
|
|
2493
|
+
candidate.endpointGeneration === previous.endpointGeneration) {
|
|
2494
|
+
if (candidate !== previousFactory)
|
|
2495
|
+
await candidate.close();
|
|
2496
|
+
candidate = undefined;
|
|
2497
|
+
if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
|
|
2501
|
+
}
|
|
2502
|
+
// Endpoint generations never inherit cached compatibility. Every
|
|
2503
|
+
// candidate independently proves all four read-only wire probes.
|
|
2504
|
+
const probes = await this.runCompatibilityProbesFor(candidate);
|
|
2505
|
+
const attestation = evaluateCompatibilityAttestation({
|
|
2506
|
+
surface: "codex",
|
|
2507
|
+
version: candidate.appServerVersion,
|
|
2508
|
+
checkedAt: this.now().toISOString(),
|
|
2509
|
+
certifiedVersions: certifiedCompatibilityVersions.codex,
|
|
2510
|
+
probes,
|
|
2511
|
+
});
|
|
2512
|
+
return {
|
|
2513
|
+
factory: candidate,
|
|
2514
|
+
identity: {
|
|
2515
|
+
provider: "codex",
|
|
2516
|
+
hostId: previous.hostId,
|
|
2517
|
+
endpointGeneration: candidate.endpointGeneration,
|
|
2518
|
+
},
|
|
2519
|
+
attestation,
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
catch (error) {
|
|
2523
|
+
if (candidate !== undefined && candidate !== previousFactory) {
|
|
2524
|
+
await candidate.close();
|
|
2525
|
+
}
|
|
2526
|
+
if (this.isEndpointGenerationChanged(error)) {
|
|
2527
|
+
if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
|
|
2531
|
+
}
|
|
2532
|
+
throw error;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
|
|
2536
|
+
}
|
|
2537
|
+
async retireEndpointGeneration(factory, endpointGeneration) {
|
|
2538
|
+
this.callbackDrainFrozen = true;
|
|
2539
|
+
this.drainCallbackQueue(true);
|
|
2540
|
+
// Fence every operation admitted on the previous immutable factory.
|
|
2541
|
+
// Ordinary route creation is blocked by endpointRefresh; joining these
|
|
2542
|
+
// exact promises ensures none can be inserted after the close sweep.
|
|
2543
|
+
await Promise.allSettled([...this.routeCreations.values()]);
|
|
2544
|
+
await Promise.allSettled([...this.routeReleases.values()]);
|
|
2545
|
+
this.drainCallbackQueue(true);
|
|
2546
|
+
for (const [routeHandle, route] of [...this.routes]) {
|
|
2547
|
+
if (route.endpointGeneration !== endpointGeneration)
|
|
2548
|
+
continue;
|
|
2549
|
+
await this.closeRoute(route);
|
|
2550
|
+
if (this.routes.get(routeHandle) === route) {
|
|
2551
|
+
this.routes.delete(routeHandle);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
this.drainCallbackQueue(true);
|
|
2555
|
+
await factory.close();
|
|
2556
|
+
this.drainCallbackQueue(true);
|
|
2557
|
+
this.retiredEndpointGeneration = endpointGeneration;
|
|
2558
|
+
}
|
|
2559
|
+
reserveSelectorRefresh(result) {
|
|
2560
|
+
if (result.delivery !== "selector" || result.selectorClaimed) {
|
|
2561
|
+
return false;
|
|
2562
|
+
}
|
|
2563
|
+
result.selectorClaimed = true;
|
|
2564
|
+
return true;
|
|
2565
|
+
}
|
|
2566
|
+
emitSelectorRefreshFallback(result, staleRouteHandle) {
|
|
2567
|
+
if (result.delivery !== "selector" || !result.selectorClaimed)
|
|
2276
2568
|
return;
|
|
2569
|
+
let event = result.event;
|
|
2570
|
+
if (staleRouteHandle !== undefined &&
|
|
2571
|
+
event.outcome === "compatible" &&
|
|
2572
|
+
event.routes.some((route) => route.routeHandle === staleRouteHandle)) {
|
|
2573
|
+
event = {
|
|
2574
|
+
...event,
|
|
2575
|
+
routes: event.routes.filter((route) => route.routeHandle !== staleRouteHandle),
|
|
2576
|
+
};
|
|
2577
|
+
result.event = event;
|
|
2578
|
+
this.pendingEndpointRefreshEvent = event;
|
|
2277
2579
|
}
|
|
2580
|
+
this.publishEndpointRefresh(event);
|
|
2581
|
+
}
|
|
2582
|
+
publishEndpointRefresh(event) {
|
|
2583
|
+
this.emitEndpointRefresh(event);
|
|
2584
|
+
if (event.outcome === "compatible") {
|
|
2585
|
+
this.armEndpointActivationRetry(event);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
armEndpointActivationRetry(event) {
|
|
2589
|
+
this.clearEndpointActivationRetry();
|
|
2590
|
+
this.endpointActivationRetry = { event, attempt: 0 };
|
|
2591
|
+
this.scheduleEndpointActivationRetry();
|
|
2592
|
+
}
|
|
2593
|
+
scheduleEndpointActivationRetry() {
|
|
2594
|
+
const pending = this.endpointActivationRetry;
|
|
2595
|
+
if (pending === undefined ||
|
|
2596
|
+
this.closing ||
|
|
2597
|
+
this.closed ||
|
|
2598
|
+
pending.attempt >= 3 ||
|
|
2599
|
+
(pending.event.outcome === "compatible" &&
|
|
2600
|
+
this.pendingEndpointAttestation === undefined)) {
|
|
2601
|
+
this.clearEndpointActivationRetry();
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
const delay = Math.max(DEFAULT_CODEX_RECOVERY_INITIAL_MS, Math.min(this.recoveryMaxMs, this.recoveryInitialMs * 2 ** pending.attempt));
|
|
2605
|
+
const timer = setTimeout(() => {
|
|
2606
|
+
if (this.endpointActivationRetryTimer !== timer)
|
|
2607
|
+
return;
|
|
2608
|
+
this.endpointActivationRetryTimer = undefined;
|
|
2609
|
+
const current = this.endpointActivationRetry;
|
|
2610
|
+
if (current === undefined || current.event !== pending.event)
|
|
2611
|
+
return;
|
|
2612
|
+
if (current.event.outcome === "compatible" &&
|
|
2613
|
+
this.pendingEndpointAttestation === undefined) {
|
|
2614
|
+
this.clearEndpointActivationRetry();
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
this.emitEndpointRefresh(current.event);
|
|
2618
|
+
if (this.endpointActivationRetry === undefined ||
|
|
2619
|
+
this.endpointActivationRetry.event !== current.event) {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
this.endpointActivationRetry = {
|
|
2623
|
+
event: current.event,
|
|
2624
|
+
attempt: current.attempt + 1,
|
|
2625
|
+
};
|
|
2626
|
+
this.scheduleEndpointActivationRetry();
|
|
2627
|
+
}, delay);
|
|
2628
|
+
timer.unref();
|
|
2629
|
+
this.endpointActivationRetryTimer = timer;
|
|
2630
|
+
}
|
|
2631
|
+
clearEndpointActivationRetry() {
|
|
2632
|
+
if (this.endpointActivationRetryTimer !== undefined) {
|
|
2633
|
+
clearTimeout(this.endpointActivationRetryTimer);
|
|
2634
|
+
}
|
|
2635
|
+
this.endpointActivationRetryTimer = undefined;
|
|
2636
|
+
this.endpointActivationRetry = undefined;
|
|
2637
|
+
}
|
|
2638
|
+
emitEndpointRefresh(event) {
|
|
2639
|
+
const callbacks = this.callbacks;
|
|
2640
|
+
if (callbacks?.onEndpointRefresh === undefined)
|
|
2641
|
+
return;
|
|
2642
|
+
invokeCallback(() => callbacks.onEndpointRefresh?.(event));
|
|
2643
|
+
}
|
|
2644
|
+
onTurnResult(endpointGeneration, result) {
|
|
2278
2645
|
const wantsReply = this.expectsReply.get(result.messageId) === true;
|
|
2279
2646
|
this.expectsReply.delete(result.messageId);
|
|
2280
2647
|
let replyText;
|
|
@@ -2306,6 +2673,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2306
2673
|
: "CODEX_DELIVERY_AMBIGUOUS";
|
|
2307
2674
|
this.enqueueCodexCallback({
|
|
2308
2675
|
type: "delivery",
|
|
2676
|
+
endpointGeneration,
|
|
2309
2677
|
value: {
|
|
2310
2678
|
messageId: result.messageId,
|
|
2311
2679
|
state,
|
|
@@ -2317,6 +2685,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2317
2685
|
queueRouteObservation(route, observation) {
|
|
2318
2686
|
this.enqueueCodexCallback({
|
|
2319
2687
|
type: "route",
|
|
2688
|
+
endpointGeneration: route.endpointGeneration,
|
|
2320
2689
|
generation: route.generation,
|
|
2321
2690
|
routeHandle: route.threadId,
|
|
2322
2691
|
state: codexRouteState(observation),
|
|
@@ -2340,6 +2709,8 @@ export class LocalCodexGatewayProvider {
|
|
|
2340
2709
|
}
|
|
2341
2710
|
}
|
|
2342
2711
|
this.callbackQueue.push(event);
|
|
2712
|
+
if (this.callbackDrainFrozen)
|
|
2713
|
+
return;
|
|
2343
2714
|
if (this.callbackScheduled)
|
|
2344
2715
|
return;
|
|
2345
2716
|
this.callbackScheduled = true;
|
|
@@ -2353,6 +2724,10 @@ export class LocalCodexGatewayProvider {
|
|
|
2353
2724
|
const callbacks = this.callbacks;
|
|
2354
2725
|
if (event === undefined || callbacks === undefined)
|
|
2355
2726
|
return;
|
|
2727
|
+
if (event.endpointGeneration !== this.factory.endpointGeneration ||
|
|
2728
|
+
this.retiredEndpointGeneration === event.endpointGeneration) {
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2356
2731
|
if (event.type === "delivery") {
|
|
2357
2732
|
invokeCallback(() => callbacks.onDelivery(event.value));
|
|
2358
2733
|
}
|
|
@@ -2361,7 +2736,11 @@ export class LocalCodexGatewayProvider {
|
|
|
2361
2736
|
return;
|
|
2362
2737
|
}
|
|
2363
2738
|
invokeCallback(() => callbacks.onRouteState({
|
|
2364
|
-
endpoint: callbackEndpoint(
|
|
2739
|
+
endpoint: callbackEndpoint({
|
|
2740
|
+
provider: "codex",
|
|
2741
|
+
hostId: this.identity.hostId,
|
|
2742
|
+
endpointGeneration: event.endpointGeneration,
|
|
2743
|
+
}, event.routeHandle),
|
|
2365
2744
|
state: event.state,
|
|
2366
2745
|
...(event.safeErrorCode === undefined
|
|
2367
2746
|
? {}
|
|
@@ -2369,7 +2748,9 @@ export class LocalCodexGatewayProvider {
|
|
|
2369
2748
|
}));
|
|
2370
2749
|
}
|
|
2371
2750
|
}
|
|
2372
|
-
drainCallbackQueue() {
|
|
2751
|
+
drainCallbackQueue(force = false) {
|
|
2752
|
+
if (this.callbackDrainFrozen && !force)
|
|
2753
|
+
return;
|
|
2373
2754
|
while (this.callbackQueue.length > 0)
|
|
2374
2755
|
this.drainOneCallback();
|
|
2375
2756
|
}
|