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,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { BridgeError } from "../errors.js";
|
|
3
3
|
import { KeyedMutex } from "../mutex.js";
|
|
4
|
-
import { certifiedCompatibilityVersions, compatibilitySurfaces,
|
|
4
|
+
import { certifiedCompatibilityVersions, compatibilitySurfaces, evaluateCompatibilityAttestation, isCompatibilityAttestation, } from "./compatibility.js";
|
|
5
5
|
import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, } from "./control.js";
|
|
6
6
|
import { publishGatewayDashboard } from "./dashboard.js";
|
|
7
7
|
import { createDeliveryMachine, isTerminalDeliveryMachine, projectDelivery, projectDeliveryWakeups, transitionDelivery, } from "./delivery-machine.js";
|
|
@@ -49,6 +49,41 @@ function bindingKey(binding) {
|
|
|
49
49
|
binding.ownerLease,
|
|
50
50
|
].join("\0");
|
|
51
51
|
}
|
|
52
|
+
function endpointRefreshCallbackKey(event) {
|
|
53
|
+
const attestation = {
|
|
54
|
+
schemaVersion: event.attestation.schemaVersion,
|
|
55
|
+
surface: event.attestation.surface,
|
|
56
|
+
version: event.attestation.version,
|
|
57
|
+
tier: event.attestation.tier,
|
|
58
|
+
checkedAt: event.attestation.checkedAt,
|
|
59
|
+
probes: event.attestation.probes.map((probe) => ({
|
|
60
|
+
name: probe.name,
|
|
61
|
+
outcome: probe.outcome,
|
|
62
|
+
...(probe.safeErrorCode === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: { safeErrorCode: probe.safeErrorCode }),
|
|
65
|
+
})),
|
|
66
|
+
...(event.attestation.safeErrorCode === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { safeErrorCode: event.attestation.safeErrorCode }),
|
|
69
|
+
};
|
|
70
|
+
return event.outcome === "compatible"
|
|
71
|
+
? JSON.stringify({
|
|
72
|
+
outcome: event.outcome,
|
|
73
|
+
previous: event.previous,
|
|
74
|
+
current: event.current,
|
|
75
|
+
attestation,
|
|
76
|
+
routes: [...event.routes]
|
|
77
|
+
.map((route) => ({ ...route }))
|
|
78
|
+
.sort((left, right) => left.routeHandle.localeCompare(right.routeHandle)),
|
|
79
|
+
})
|
|
80
|
+
: JSON.stringify({
|
|
81
|
+
outcome: event.outcome,
|
|
82
|
+
previous: event.previous,
|
|
83
|
+
candidate: event.candidate,
|
|
84
|
+
attestation,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
52
87
|
function stableLease(provider, value) {
|
|
53
88
|
return `lease_${createHash("sha256").update(provider).update("\0").update(value).digest("base64url")}`;
|
|
54
89
|
}
|
|
@@ -107,10 +142,34 @@ export class GatewayService {
|
|
|
107
142
|
deliveryTokens = new Map();
|
|
108
143
|
runtimeAlerts = [];
|
|
109
144
|
activityEvents = [];
|
|
145
|
+
activeCodexIncompatibility;
|
|
110
146
|
activitySequence = 0;
|
|
111
147
|
detachedReceiptWrites = new Set();
|
|
112
148
|
nativeIngressByConversation = new Map();
|
|
113
149
|
callbackQueue = [];
|
|
150
|
+
/**
|
|
151
|
+
* Endpoint replacement is authority-bearing lifecycle evidence, not an
|
|
152
|
+
* ordinary refreshable observation. Keep one coalesced slot outside the
|
|
153
|
+
* bounded callback queue so delivery evidence can never displace it (or be
|
|
154
|
+
* displaced by it).
|
|
155
|
+
*/
|
|
156
|
+
endpointRefreshCallback;
|
|
157
|
+
activeEndpointRefreshCallback;
|
|
158
|
+
endpointRefreshRetryRequested;
|
|
159
|
+
endpointRefreshConflictBoundary;
|
|
160
|
+
activeEndpointRefreshCallbackKey;
|
|
161
|
+
endpointRefreshCallbackPoisoned = false;
|
|
162
|
+
endpointRefreshConflictPending = false;
|
|
163
|
+
/** Last successfully activated transition per allowed Codex host. */
|
|
164
|
+
activatedEndpointRefreshes = new Map();
|
|
165
|
+
/** Durable reanchors awaiting their exact native-listener activation. */
|
|
166
|
+
durablyAppliedEndpointRefreshes = new Map();
|
|
167
|
+
/** Selector transitions staged until their exact registration owns a helper. */
|
|
168
|
+
pendingSelectorEndpointActivations = new Map();
|
|
169
|
+
/** Last incompatible transition already projected per allowed Codex host. */
|
|
170
|
+
handledIncompatibleEndpointRefreshes = new Map();
|
|
171
|
+
/** Process-local dedupe for automatic public activity rows across retries. */
|
|
172
|
+
endpointRefreshActivityKeys = new Set();
|
|
114
173
|
callbackWorker;
|
|
115
174
|
callbackScheduled = false;
|
|
116
175
|
callbackCapacity;
|
|
@@ -136,6 +195,12 @@ export class GatewayService {
|
|
|
136
195
|
/** Route-scoped fences; unrelated Codex registrations remain dispatchable. */
|
|
137
196
|
codexSuccessionDispatchFrozen = new Set();
|
|
138
197
|
codexSuccessionPoisoned = new Set();
|
|
198
|
+
/**
|
|
199
|
+
* Exact stale authority whose process-owned native advertisement was already
|
|
200
|
+
* removed, but whose durable orphan deletion did not commit. Keep dispatch
|
|
201
|
+
* frozen and permit only the same exact cleanup transaction to retry.
|
|
202
|
+
*/
|
|
203
|
+
codexOrphanCleanupPending = new Map();
|
|
139
204
|
codexSuccessionState;
|
|
140
205
|
pendingCodexSuccessionRecovery;
|
|
141
206
|
/**
|
|
@@ -171,6 +236,7 @@ export class GatewayService {
|
|
|
171
236
|
async start(signal) {
|
|
172
237
|
if (this.running || this.closing)
|
|
173
238
|
return;
|
|
239
|
+
let bootReactivatedIdleAliases = [];
|
|
174
240
|
const assertStartActive = () => {
|
|
175
241
|
if (signal?.aborted === true || this.closing) {
|
|
176
242
|
throw new BridgeError("GATEWAY_START_CANCELLED", "Gateway startup was cancelled before it became ready.", true);
|
|
@@ -191,7 +257,7 @@ export class GatewayService {
|
|
|
191
257
|
throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
|
|
192
258
|
}
|
|
193
259
|
seen.add(key);
|
|
194
|
-
const observation = await adapter.initialize(this.callbacksFor(adapter
|
|
260
|
+
const observation = await adapter.initialize(this.callbacksFor(adapter));
|
|
195
261
|
assertStartActive();
|
|
196
262
|
await this.store.observeConnector({
|
|
197
263
|
identity: adapter.identity,
|
|
@@ -208,13 +274,15 @@ export class GatewayService {
|
|
|
208
274
|
if (this.adapters.length > 0 &&
|
|
209
275
|
this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
|
|
210
276
|
adapter.runCompatibilityProbes !== undefined)) {
|
|
211
|
-
const
|
|
212
|
-
const incompatible =
|
|
277
|
+
const attestations = await this.runAutomaticCompatibilityProbesLocked();
|
|
278
|
+
const incompatible = attestations.find((surface) => surface.tier === "incompatible");
|
|
213
279
|
if (incompatible !== undefined) {
|
|
214
280
|
throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
|
|
215
281
|
}
|
|
216
282
|
}
|
|
217
283
|
await this.recoverCodexSuccessionAfterRestartLocked();
|
|
284
|
+
bootReactivatedIdleAliases =
|
|
285
|
+
await this.reactivateRetainedCodexRoutesAfterRestartLocked();
|
|
218
286
|
assertStartActive();
|
|
219
287
|
const control = await startGatewayControlServer({
|
|
220
288
|
stateDir: this.store.rootDir,
|
|
@@ -235,6 +303,9 @@ export class GatewayService {
|
|
|
235
303
|
this.nextDashboardRefreshAt = undefined;
|
|
236
304
|
await this.refreshNextProgressWatchWakeLocked();
|
|
237
305
|
this.scheduleLifecycleWakeLocked();
|
|
306
|
+
for (const alias of bootReactivatedIdleAliases) {
|
|
307
|
+
this.scheduleDispatch(alias);
|
|
308
|
+
}
|
|
238
309
|
await this.publish();
|
|
239
310
|
assertStartActive();
|
|
240
311
|
}
|
|
@@ -284,7 +355,10 @@ export class GatewayService {
|
|
|
284
355
|
for (const messageId of [...this.providerTurnContinuations.keys()]) {
|
|
285
356
|
await this.finishProviderTurnContinuationLocked(messageId, "cancelled");
|
|
286
357
|
}
|
|
358
|
+
const retainedQueuedMessageIds = new Set(await this.store.inspectQueuedMessageIds());
|
|
287
359
|
for (const messageId of [...this.messageContexts.keys()]) {
|
|
360
|
+
if (retainedQueuedMessageIds.has(messageId))
|
|
361
|
+
continue;
|
|
288
362
|
const queued = await this.store.settleQueuedMessage({
|
|
289
363
|
messageId,
|
|
290
364
|
state: "cancelled",
|
|
@@ -300,6 +374,8 @@ export class GatewayService {
|
|
|
300
374
|
});
|
|
301
375
|
}
|
|
302
376
|
for (const tracker of this.deliveryTrackers.values()) {
|
|
377
|
+
if (retainedQueuedMessageIds.has(tracker.messageId))
|
|
378
|
+
continue;
|
|
303
379
|
if (!isTerminalDeliveryMachine(tracker.machine)) {
|
|
304
380
|
await this.advanceDeliveryLocked(tracker.messageId, {
|
|
305
381
|
type: "shutdown",
|
|
@@ -351,6 +427,19 @@ export class GatewayService {
|
|
|
351
427
|
this.detachedReceiptWrites.clear();
|
|
352
428
|
this.nativeIngressByConversation.clear();
|
|
353
429
|
this.callbackQueue.length = 0;
|
|
430
|
+
this.endpointRefreshCallback = undefined;
|
|
431
|
+
this.activeEndpointRefreshCallback = undefined;
|
|
432
|
+
this.endpointRefreshRetryRequested = undefined;
|
|
433
|
+
this.endpointRefreshConflictBoundary = undefined;
|
|
434
|
+
this.activeEndpointRefreshCallbackKey = undefined;
|
|
435
|
+
this.endpointRefreshConflictPending = false;
|
|
436
|
+
this.endpointRefreshCallbackPoisoned = true;
|
|
437
|
+
this.activatedEndpointRefreshes.clear();
|
|
438
|
+
this.durablyAppliedEndpointRefreshes.clear();
|
|
439
|
+
this.pendingSelectorEndpointActivations.clear();
|
|
440
|
+
this.handledIncompatibleEndpointRefreshes.clear();
|
|
441
|
+
this.endpointRefreshActivityKeys.clear();
|
|
442
|
+
this.codexOrphanCleanupPending.clear();
|
|
354
443
|
this.candidates.clear();
|
|
355
444
|
this.routeBindings.clear();
|
|
356
445
|
this.routeStates.clear();
|
|
@@ -390,6 +479,11 @@ export class GatewayService {
|
|
|
390
479
|
action: "codex_unregistered",
|
|
391
480
|
aliases: [params.alias],
|
|
392
481
|
}),
|
|
482
|
+
removeStaleCodexRegistration: async (params) => await this.exclusiveDecision(async () => this.removeStaleCodexRegistration(params.alias), {
|
|
483
|
+
kind: "recovery",
|
|
484
|
+
action: "codex_orphan_removed",
|
|
485
|
+
aliases: [params.alias],
|
|
486
|
+
}),
|
|
393
487
|
selectClaude: async (params) => await this.exclusiveDecision(async () => this.selectClaude(params), {
|
|
394
488
|
kind: "selection",
|
|
395
489
|
action: "claude_selected",
|
|
@@ -412,8 +506,6 @@ export class GatewayService {
|
|
|
412
506
|
}),
|
|
413
507
|
listSnapshot: async () => (await this.observeSnapshot()).snapshot,
|
|
414
508
|
observeSnapshot: async () => await this.observeSnapshot(),
|
|
415
|
-
compatibilityCheck: async () => await this.compatibilityCheck(),
|
|
416
|
-
compatibilityCertify: async (params) => await this.compatibilityCertify(params),
|
|
417
509
|
deliveryStatus: async (params) => await this.deliveryStatus(params.token),
|
|
418
510
|
untrack: async (params) => await this.exclusiveDecision(async () => {
|
|
419
511
|
const settled = await this.store.settleProgressWatch({
|
|
@@ -444,133 +536,7 @@ export class GatewayService {
|
|
|
444
536
|
async snapshot() {
|
|
445
537
|
return (await this.observeSnapshot()).snapshot;
|
|
446
538
|
}
|
|
447
|
-
async
|
|
448
|
-
return await this.mutex.run("service", async () => await this.compatibilityCheckLocked());
|
|
449
|
-
}
|
|
450
|
-
async compatibilityCertify(params) {
|
|
451
|
-
const plan = await this.mutex.run("service", async () => {
|
|
452
|
-
if (!this.running || this.closing) {
|
|
453
|
-
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Compatibility certification requires one running broker.");
|
|
454
|
-
}
|
|
455
|
-
const report = await this.compatibilityCheckLocked();
|
|
456
|
-
const codexRoutes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.enabled);
|
|
457
|
-
const codexRoute = params.codexAlias === undefined
|
|
458
|
-
? codexRoutes.length === 1
|
|
459
|
-
? codexRoutes[0]
|
|
460
|
-
: undefined
|
|
461
|
-
: codexRoutes.find((route) => route.alias === params.codexAlias);
|
|
462
|
-
if (codexRoute === undefined) {
|
|
463
|
-
throw new BridgeError("COMPAT_CERTIFY_CODEX_ROUTE_REQUIRED", "Choose one exact registered Codex route for live certification.");
|
|
464
|
-
}
|
|
465
|
-
const adapters = new Map(this.adapters.map((adapter) => [adapter.identity.provider, adapter]));
|
|
466
|
-
return { adapters, codexRoute, report };
|
|
467
|
-
});
|
|
468
|
-
const certifiedAt = this.now().toISOString();
|
|
469
|
-
const updated = [];
|
|
470
|
-
for (const attestation of plan.report.surfaces) {
|
|
471
|
-
const adapter = plan.adapters.get(attestation.surface);
|
|
472
|
-
let certification;
|
|
473
|
-
if (attestation.tier === "incompatible" ||
|
|
474
|
-
adapter?.runCompatibilityCertification === undefined) {
|
|
475
|
-
certification = {
|
|
476
|
-
depth: attestation.surface === "claude"
|
|
477
|
-
? "wire"
|
|
478
|
-
: params.withTurn
|
|
479
|
-
? "turn"
|
|
480
|
-
: "thread_ops",
|
|
481
|
-
outcome: "fail",
|
|
482
|
-
certifiedAt,
|
|
483
|
-
safeErrorCode: attestation.safeErrorCode ??
|
|
484
|
-
`${attestation.surface.toUpperCase()}_CERTIFICATION_UNAVAILABLE`,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
try {
|
|
489
|
-
certification = await adapter.runCompatibilityCertification({
|
|
490
|
-
controllerStateRoot: this.store.rootDir,
|
|
491
|
-
withTurn: params.withTurn,
|
|
492
|
-
...(attestation.surface === "codex"
|
|
493
|
-
? { routeHandle: plan.codexRoute.binding.routeHandle }
|
|
494
|
-
: {}),
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
catch {
|
|
498
|
-
certification = {
|
|
499
|
-
depth: attestation.surface === "claude"
|
|
500
|
-
? "wire"
|
|
501
|
-
: params.withTurn
|
|
502
|
-
? "turn"
|
|
503
|
-
: "thread_ops",
|
|
504
|
-
outcome: "fail",
|
|
505
|
-
certifiedAt: this.now().toISOString(),
|
|
506
|
-
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_FAILED`,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
const expectedDepth = attestation.surface === "claude"
|
|
511
|
-
? "wire"
|
|
512
|
-
: params.withTurn
|
|
513
|
-
? "turn"
|
|
514
|
-
: "thread_ops";
|
|
515
|
-
if (!isCompatibilityCertification(certification) ||
|
|
516
|
-
certification.depth !== expectedDepth) {
|
|
517
|
-
certification = {
|
|
518
|
-
depth: expectedDepth,
|
|
519
|
-
outcome: "fail",
|
|
520
|
-
certifiedAt: this.now().toISOString(),
|
|
521
|
-
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_EVIDENCE_INVALID`,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
updated.push(attachCompatibilityCertification(attestation, certification));
|
|
525
|
-
}
|
|
526
|
-
return await this.mutex.run("service", async () => {
|
|
527
|
-
const currentCodexRoute = (await this.store.inspectPrivateCodexRoutes()).find((route) => route.enabled &&
|
|
528
|
-
route.alias === plan.codexRoute.alias &&
|
|
529
|
-
bindingKey(route.binding) === bindingKey(plan.codexRoute.binding));
|
|
530
|
-
const reconciled = updated.map((attestation) => {
|
|
531
|
-
if (attestation.surface === "codex" && currentCodexRoute === undefined) {
|
|
532
|
-
return attachCompatibilityCertification(attestation, {
|
|
533
|
-
depth: params.withTurn ? "turn" : "thread_ops",
|
|
534
|
-
outcome: "fail",
|
|
535
|
-
certifiedAt: this.now().toISOString(),
|
|
536
|
-
safeErrorCode: "CODEX_CERTIFICATION_ROUTE_CHANGED",
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
const observation = plan.adapters
|
|
540
|
-
.get(attestation.surface)
|
|
541
|
-
?.compatibilitySurface?.();
|
|
542
|
-
if (observation?.surface === attestation.surface &&
|
|
543
|
-
observation.version === attestation.version) {
|
|
544
|
-
return attestation;
|
|
545
|
-
}
|
|
546
|
-
return attachCompatibilityCertification(attestation, {
|
|
547
|
-
depth: attestation.surface === "claude"
|
|
548
|
-
? "wire"
|
|
549
|
-
: params.withTurn
|
|
550
|
-
? "turn"
|
|
551
|
-
: "thread_ops",
|
|
552
|
-
outcome: "fail",
|
|
553
|
-
certifiedAt: this.now().toISOString(),
|
|
554
|
-
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_VERSION_CHANGED`,
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
const result = {
|
|
558
|
-
policy: plan.report.policy,
|
|
559
|
-
certified: reconciled.every((attestation) => attestation.certification?.outcome === "pass"),
|
|
560
|
-
withTurn: params.withTurn,
|
|
561
|
-
surfaces: reconciled,
|
|
562
|
-
};
|
|
563
|
-
if (!isCompatibilityCertificationReport(result)) {
|
|
564
|
-
throw new BridgeError("COMPAT_CERTIFICATION_REPORT_INVALID", "Compatibility certification evidence failed its closed report schema.");
|
|
565
|
-
}
|
|
566
|
-
for (const attestation of reconciled) {
|
|
567
|
-
await this.store.recordCompatibilityAttestation(attestation);
|
|
568
|
-
}
|
|
569
|
-
await this.changed();
|
|
570
|
-
return result;
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
async compatibilityCheckLocked() {
|
|
539
|
+
async runAutomaticCompatibilityProbesLocked() {
|
|
574
540
|
const bySurface = new Map(this.adapters.map((adapter) => {
|
|
575
541
|
if (adapter.compatibilitySurface === undefined ||
|
|
576
542
|
adapter.runCompatibilityProbes === undefined) {
|
|
@@ -589,39 +555,21 @@ export class GatewayService {
|
|
|
589
555
|
if (entry === undefined) {
|
|
590
556
|
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
|
|
591
557
|
}
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
const checkedAt = cached?.checkedAt ?? this.now().toISOString();
|
|
595
|
-
let attestation = evaluateCompatibilityAttestation({
|
|
558
|
+
const probes = await entry.adapter.runCompatibilityProbes();
|
|
559
|
+
const attestation = evaluateCompatibilityAttestation({
|
|
596
560
|
surface,
|
|
597
561
|
version: entry.observation.version,
|
|
598
|
-
checkedAt,
|
|
599
|
-
policy: this.config.compatibilityPolicy ?? "observed",
|
|
562
|
+
checkedAt: this.now().toISOString(),
|
|
600
563
|
certifiedVersions: certifiedCompatibilityVersions[surface],
|
|
601
564
|
probes,
|
|
602
565
|
});
|
|
603
|
-
|
|
604
|
-
attestation.tier !== "incompatible") {
|
|
605
|
-
attestation = attachCompatibilityCertification(attestation, cached.certification);
|
|
606
|
-
}
|
|
607
|
-
if (cached === undefined ||
|
|
608
|
-
JSON.stringify(cached) !== JSON.stringify(attestation)) {
|
|
609
|
-
await this.store.recordCompatibilityAttestation(attestation);
|
|
610
|
-
}
|
|
566
|
+
await this.store.recordCompatibilityAttestation(attestation);
|
|
611
567
|
if (attestation.tier !== "incompatible") {
|
|
612
568
|
entry.adapter.acceptCompatibilityAttestation?.(attestation);
|
|
613
569
|
}
|
|
614
570
|
attestations.push(attestation);
|
|
615
571
|
}
|
|
616
|
-
|
|
617
|
-
policy: this.config.compatibilityPolicy ?? "observed",
|
|
618
|
-
compatible: attestations.every((attestation) => attestation.tier !== "incompatible"),
|
|
619
|
-
surfaces: attestations,
|
|
620
|
-
};
|
|
621
|
-
if (!isCompatibilityCheckReport(report)) {
|
|
622
|
-
throw new BridgeError("COMPAT_REPORT_INVALID", "Compatibility evidence failed its closed report schema.");
|
|
623
|
-
}
|
|
624
|
-
return report;
|
|
572
|
+
return attestations;
|
|
625
573
|
}
|
|
626
574
|
async observeSnapshot() {
|
|
627
575
|
return await this.mutex.run("service", async () => {
|
|
@@ -663,9 +611,10 @@ export class GatewayService {
|
|
|
663
611
|
}
|
|
664
612
|
return createHash("sha256").update(encoded, "utf8").digest("base64url");
|
|
665
613
|
}
|
|
666
|
-
callbacksFor(
|
|
614
|
+
callbacksFor(adapter) {
|
|
667
615
|
return {
|
|
668
616
|
onDelivery: (event) => {
|
|
617
|
+
const source = adapter.identity;
|
|
669
618
|
if (!this.running ||
|
|
670
619
|
!MESSAGE_ID.test(event.messageId) ||
|
|
671
620
|
this.deliveryCorrelation(event.messageId) === undefined ||
|
|
@@ -694,6 +643,7 @@ export class GatewayService {
|
|
|
694
643
|
});
|
|
695
644
|
},
|
|
696
645
|
onRouteState: (event) => {
|
|
646
|
+
const source = adapter.identity;
|
|
697
647
|
if (!this.running ||
|
|
698
648
|
event.endpoint.provider !== source.provider ||
|
|
699
649
|
event.endpoint.hostId !== source.hostId ||
|
|
@@ -716,6 +666,7 @@ export class GatewayService {
|
|
|
716
666
|
});
|
|
717
667
|
},
|
|
718
668
|
onClaudeReply: (event) => {
|
|
669
|
+
const source = adapter.identity;
|
|
719
670
|
if (!this.running ||
|
|
720
671
|
event.endpoint.provider !== source.provider ||
|
|
721
672
|
event.endpoint.hostId !== source.hostId ||
|
|
@@ -742,6 +693,7 @@ export class GatewayService {
|
|
|
742
693
|
});
|
|
743
694
|
},
|
|
744
695
|
onClaudeMessage: (event) => {
|
|
696
|
+
const source = adapter.identity;
|
|
745
697
|
const ingressFailureCode = this.closing
|
|
746
698
|
? "GATEWAY_SHUTDOWN"
|
|
747
699
|
: "NATIVE_INGRESS_INVALID";
|
|
@@ -779,6 +731,7 @@ export class GatewayService {
|
|
|
779
731
|
}
|
|
780
732
|
},
|
|
781
733
|
onProtocolNotice: (event) => {
|
|
734
|
+
const source = adapter.identity;
|
|
782
735
|
if (!this.running || !SAFE_CODE.test(event.code))
|
|
783
736
|
return;
|
|
784
737
|
this.enqueueCallback({
|
|
@@ -787,11 +740,61 @@ export class GatewayService {
|
|
|
787
740
|
value: { code: event.code },
|
|
788
741
|
});
|
|
789
742
|
},
|
|
743
|
+
onEndpointRefresh: (event) => {
|
|
744
|
+
if (!this.running ||
|
|
745
|
+
adapter.identity.provider !== "codex" ||
|
|
746
|
+
event.previous.provider !== "codex" ||
|
|
747
|
+
event.previous.hostId !== adapter.identity.hostId ||
|
|
748
|
+
!isCompatibilityAttestation(event.attestation) ||
|
|
749
|
+
event.attestation.surface !== "codex") {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (event.outcome === "compatible") {
|
|
753
|
+
const routeHandles = event.routes.map((route) => route.routeHandle);
|
|
754
|
+
if (event.current.provider !== "codex" ||
|
|
755
|
+
event.current.hostId !== event.previous.hostId ||
|
|
756
|
+
event.current.endpointGeneration !==
|
|
757
|
+
adapter.identity.endpointGeneration ||
|
|
758
|
+
event.attestation.version !== adapter.protocolVersion ||
|
|
759
|
+
event.attestation.tier === "incompatible" ||
|
|
760
|
+
routeHandles.length > this.config.limits.maxRoutes ||
|
|
761
|
+
new Set(routeHandles).size !== routeHandles.length ||
|
|
762
|
+
routeHandles.some((routeHandle) => !PRIVATE_HANDLE.test(routeHandle))) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else if (event.candidate.provider !== "codex" ||
|
|
767
|
+
event.candidate.hostId !== event.previous.hostId ||
|
|
768
|
+
event.previous.endpointGeneration !==
|
|
769
|
+
adapter.identity.endpointGeneration ||
|
|
770
|
+
event.attestation.tier !== "incompatible") {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
this.enqueueCallback({
|
|
774
|
+
type: "endpoint_refresh",
|
|
775
|
+
adapter,
|
|
776
|
+
value: event.outcome === "compatible"
|
|
777
|
+
? {
|
|
778
|
+
...event,
|
|
779
|
+
previous: { ...event.previous },
|
|
780
|
+
current: { ...event.current },
|
|
781
|
+
routes: event.routes.map((route) => ({ ...route })),
|
|
782
|
+
}
|
|
783
|
+
: {
|
|
784
|
+
...event,
|
|
785
|
+
previous: { ...event.previous },
|
|
786
|
+
candidate: { ...event.candidate },
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
},
|
|
790
790
|
};
|
|
791
791
|
}
|
|
792
792
|
enqueueCallback(event) {
|
|
793
793
|
if (!this.acceptingCallbacks)
|
|
794
794
|
return false;
|
|
795
|
+
if (event.type === "endpoint_refresh") {
|
|
796
|
+
return this.enqueueEndpointRefreshCallback(event);
|
|
797
|
+
}
|
|
795
798
|
if (event.type === "delivery") {
|
|
796
799
|
const duplicate = this.callbackQueue.findIndex((candidate) => candidate.type === "delivery" &&
|
|
797
800
|
candidate.value.messageId === event.value.messageId &&
|
|
@@ -834,7 +837,7 @@ export class GatewayService {
|
|
|
834
837
|
// Delivery write/approval observations are authoritative evidence for
|
|
835
838
|
// no-replay and deadline arbitration. Never evict them. A stale route
|
|
836
839
|
// observation is safe to replace because discovery re-observes it.
|
|
837
|
-
const replaceable = this.callbackQueue.findIndex((candidate) => candidate.type === "route");
|
|
840
|
+
const replaceable = this.callbackQueue.findIndex((candidate) => candidate.type === "route" || candidate.type === "protocol_notice");
|
|
838
841
|
if (replaceable < 0) {
|
|
839
842
|
this.dashboardHealthy = false;
|
|
840
843
|
return false;
|
|
@@ -842,29 +845,49 @@ export class GatewayService {
|
|
|
842
845
|
this.callbackQueue.splice(replaceable, 1);
|
|
843
846
|
}
|
|
844
847
|
this.callbackQueue.push(event);
|
|
845
|
-
|
|
846
|
-
return true;
|
|
847
|
-
this.callbackScheduled = true;
|
|
848
|
-
setImmediate(() => {
|
|
849
|
-
this.callbackScheduled = false;
|
|
850
|
-
const worker = this.mutex.run("service", async () => {
|
|
851
|
-
await this.drainCallbackQueueLocked();
|
|
852
|
-
});
|
|
853
|
-
this.callbackWorker = worker;
|
|
854
|
-
worker
|
|
855
|
-
.catch(() => {
|
|
856
|
-
this.dashboardHealthy = false;
|
|
857
|
-
})
|
|
858
|
-
.finally(() => {
|
|
859
|
-
if (this.callbackWorker === worker)
|
|
860
|
-
this.callbackWorker = undefined;
|
|
861
|
-
if (this.callbackQueue.length > 0 && !this.closing) {
|
|
862
|
-
this.enqueueCallbackWorkerOnly();
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
});
|
|
848
|
+
this.enqueueCallbackWorkerOnly();
|
|
866
849
|
return true;
|
|
867
850
|
}
|
|
851
|
+
enqueueEndpointRefreshCallback(event) {
|
|
852
|
+
if (this.endpointRefreshCallbackPoisoned)
|
|
853
|
+
return false;
|
|
854
|
+
const key = endpointRefreshCallbackKey(event.value);
|
|
855
|
+
if (this.activeEndpointRefreshCallbackKey !== undefined) {
|
|
856
|
+
if (this.activeEndpointRefreshCallbackKey === key) {
|
|
857
|
+
this.endpointRefreshRetryRequested = event;
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
this.poisonEndpointRefreshCallbacks(event);
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (this.endpointRefreshCallback !== undefined) {
|
|
864
|
+
if (endpointRefreshCallbackKey(this.endpointRefreshCallback.value) === key) {
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
this.poisonEndpointRefreshCallbacks(event);
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
this.endpointRefreshCallback = event;
|
|
871
|
+
this.enqueueCallbackWorkerOnly();
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
poisonEndpointRefreshCallbacks(conflicting) {
|
|
875
|
+
this.endpointRefreshConflictBoundary =
|
|
876
|
+
this.activeEndpointRefreshCallback ??
|
|
877
|
+
this.endpointRefreshCallback ??
|
|
878
|
+
conflicting;
|
|
879
|
+
this.endpointRefreshCallback = undefined;
|
|
880
|
+
this.endpointRefreshRetryRequested = undefined;
|
|
881
|
+
this.endpointRefreshCallbackPoisoned = true;
|
|
882
|
+
this.endpointRefreshConflictPending = true;
|
|
883
|
+
this.dashboardHealthy = false;
|
|
884
|
+
this.enqueueCallbackWorkerOnly();
|
|
885
|
+
}
|
|
886
|
+
hasPendingCallbackWork() {
|
|
887
|
+
return (this.callbackQueue.length > 0 ||
|
|
888
|
+
this.endpointRefreshCallback !== undefined ||
|
|
889
|
+
this.endpointRefreshConflictPending);
|
|
890
|
+
}
|
|
868
891
|
enqueueCallbackWorkerOnly() {
|
|
869
892
|
if (this.callbackScheduled)
|
|
870
893
|
return;
|
|
@@ -882,7 +905,7 @@ export class GatewayService {
|
|
|
882
905
|
.finally(() => {
|
|
883
906
|
if (this.callbackWorker === worker)
|
|
884
907
|
this.callbackWorker = undefined;
|
|
885
|
-
if (this.
|
|
908
|
+
if (this.hasPendingCallbackWork() && !this.closing) {
|
|
886
909
|
this.enqueueCallbackWorkerOnly();
|
|
887
910
|
}
|
|
888
911
|
});
|
|
@@ -895,10 +918,95 @@ export class GatewayService {
|
|
|
895
918
|
await this.publish();
|
|
896
919
|
}
|
|
897
920
|
this.pruneTransient();
|
|
898
|
-
while (this.
|
|
899
|
-
|
|
900
|
-
|
|
921
|
+
while (this.hasPendingCallbackWork()) {
|
|
922
|
+
// Preserve the complete ordinary queue order. In particular, every
|
|
923
|
+
// delivery observation already retained before endpoint replacement is
|
|
924
|
+
// reduced before the lifecycle transition can settle its generation.
|
|
925
|
+
const ordinaryBatch = this.callbackQueue.splice(0, this.callbackQueue.length);
|
|
926
|
+
for (const event of ordinaryBatch) {
|
|
927
|
+
try {
|
|
928
|
+
if (event.type === "delivery") {
|
|
929
|
+
await this.onDelivery(event.source, event.value, event.receivedAt);
|
|
930
|
+
}
|
|
931
|
+
else if (event.type === "route") {
|
|
932
|
+
await this.onRouteState(event.source, event.value);
|
|
933
|
+
}
|
|
934
|
+
else if (event.type === "claude_reply") {
|
|
935
|
+
await this.onClaudeReply(event.value);
|
|
936
|
+
}
|
|
937
|
+
else if (event.type === "claude_message") {
|
|
938
|
+
await this.onClaudeMessage(event.value);
|
|
939
|
+
}
|
|
940
|
+
else if (event.type === "protocol_notice") {
|
|
941
|
+
this.addRuntimeAlert(event.value.code, "warning", {
|
|
942
|
+
provider: event.source.provider,
|
|
943
|
+
host: event.source.hostId,
|
|
944
|
+
});
|
|
945
|
+
await this.changed();
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
this.dashboardHealthy = false;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
953
|
+
if (this.endpointRefreshConflictPending) {
|
|
954
|
+
this.endpointRefreshConflictPending = false;
|
|
955
|
+
const boundary = this.endpointRefreshConflictBoundary;
|
|
956
|
+
if (boundary !== undefined) {
|
|
957
|
+
await this.failClosedEndpointRefreshConflict(boundary);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
this.addRuntimeAlert("CODEX_ENDPOINT_REFRESH_CONFLICT", "error", { provider: "codex" });
|
|
961
|
+
await this.changed();
|
|
962
|
+
}
|
|
901
963
|
continue;
|
|
964
|
+
}
|
|
965
|
+
const endpointRefresh = this.endpointRefreshCallback;
|
|
966
|
+
if (endpointRefresh === undefined)
|
|
967
|
+
continue;
|
|
968
|
+
const key = endpointRefreshCallbackKey(endpointRefresh.value);
|
|
969
|
+
this.endpointRefreshCallback = undefined;
|
|
970
|
+
this.activeEndpointRefreshCallback = endpointRefresh;
|
|
971
|
+
this.activeEndpointRefreshCallbackKey = key;
|
|
972
|
+
let succeeded = false;
|
|
973
|
+
try {
|
|
974
|
+
await this.onCodexEndpointRefresh(endpointRefresh.adapter, endpointRefresh.value);
|
|
975
|
+
succeeded = true;
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
this.dashboardHealthy = false;
|
|
979
|
+
}
|
|
980
|
+
finally {
|
|
981
|
+
this.activeEndpointRefreshCallback = undefined;
|
|
982
|
+
if (this.activeEndpointRefreshCallbackKey === key) {
|
|
983
|
+
this.activeEndpointRefreshCallbackKey = undefined;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const requestedRetry = this.endpointRefreshRetryRequested;
|
|
987
|
+
this.endpointRefreshRetryRequested = undefined;
|
|
988
|
+
if (!succeeded &&
|
|
989
|
+
!this.endpointRefreshCallbackPoisoned &&
|
|
990
|
+
requestedRetry !== undefined) {
|
|
991
|
+
this.endpointRefreshCallback = requestedRetry;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* A selector can synchronously admit callbacks while its promise is still
|
|
997
|
+
* running under the service mutex. Drain exactly the ordinary work that was
|
|
998
|
+
* retained before the selector returned before applying a returned endpoint
|
|
999
|
+
* transition. Delivery evidence wins within that fixed snapshot; callbacks
|
|
1000
|
+
* admitted while it is reduced remain queued for the ordinary worker, and
|
|
1001
|
+
* the dedicated endpoint-transition slot is never consumed recursively.
|
|
1002
|
+
*/
|
|
1003
|
+
async drainSelectorCallbackSnapshotLocked() {
|
|
1004
|
+
const snapshot = this.callbackQueue.splice(0, this.callbackQueue.length);
|
|
1005
|
+
const ordered = [
|
|
1006
|
+
...snapshot.filter((event) => event.type === "delivery"),
|
|
1007
|
+
...snapshot.filter((event) => event.type !== "delivery"),
|
|
1008
|
+
];
|
|
1009
|
+
for (const event of ordered) {
|
|
902
1010
|
try {
|
|
903
1011
|
if (event.type === "delivery") {
|
|
904
1012
|
await this.onDelivery(event.source, event.value, event.receivedAt);
|
|
@@ -912,7 +1020,7 @@ export class GatewayService {
|
|
|
912
1020
|
else if (event.type === "claude_message") {
|
|
913
1021
|
await this.onClaudeMessage(event.value);
|
|
914
1022
|
}
|
|
915
|
-
else {
|
|
1023
|
+
else if (event.type === "protocol_notice") {
|
|
916
1024
|
this.addRuntimeAlert(event.value.code, "warning", {
|
|
917
1025
|
provider: event.source.provider,
|
|
918
1026
|
host: event.source.hostId,
|
|
@@ -924,6 +1032,59 @@ export class GatewayService {
|
|
|
924
1032
|
this.dashboardHealthy = false;
|
|
925
1033
|
}
|
|
926
1034
|
}
|
|
1035
|
+
// Synchronous dispatch may have admitted authoritative delivery evidence,
|
|
1036
|
+
// and rejected native ingress may have started a detached terminal write.
|
|
1037
|
+
// Join both before the endpoint boundary can move to its next generation.
|
|
1038
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
1039
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
1040
|
+
}
|
|
1041
|
+
async failClosedEndpointRefreshConflict(boundary) {
|
|
1042
|
+
const identities = [
|
|
1043
|
+
boundary.value.previous,
|
|
1044
|
+
...(boundary.value.outcome === "compatible"
|
|
1045
|
+
? [boundary.value.current]
|
|
1046
|
+
: []),
|
|
1047
|
+
];
|
|
1048
|
+
for (const identity of identities) {
|
|
1049
|
+
const routes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.binding.hostId === identity.hostId &&
|
|
1050
|
+
route.binding.endpointGeneration === identity.endpointGeneration);
|
|
1051
|
+
const aliases = routes.map((route) => route.alias);
|
|
1052
|
+
const inFlightSettlements = aliases.length === 0
|
|
1053
|
+
? []
|
|
1054
|
+
: await this.planRoutesInFlightSettlementsLocked(aliases, {
|
|
1055
|
+
unwrittenOutcome: "failed",
|
|
1056
|
+
safeErrorCode: "CODEX_ENDPOINT_REFRESH_CONFLICT",
|
|
1057
|
+
});
|
|
1058
|
+
try {
|
|
1059
|
+
const settlements = await this.store.markConnectorOffline(identity, "CODEX_ENDPOINT_REFRESH_CONFLICT", inFlightSettlements, { preserveQueued: true });
|
|
1060
|
+
for (const settlement of settlements) {
|
|
1061
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
1062
|
+
}
|
|
1063
|
+
for (const route of routes) {
|
|
1064
|
+
this.routeStates.set(route.alias, "stale");
|
|
1065
|
+
}
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
if (error instanceof BridgeError &&
|
|
1070
|
+
error.code === "CONNECTOR_NOT_FOUND") {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
throw error;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
this.addRuntimeAlert("CODEX_ENDPOINT_REFRESH_CONFLICT", "error", { provider: "codex", host: boundary.value.previous.hostId });
|
|
1077
|
+
await this.changed();
|
|
1078
|
+
}
|
|
1079
|
+
async requireEndpointRefreshUnconflicted() {
|
|
1080
|
+
if (!this.endpointRefreshCallbackPoisoned)
|
|
1081
|
+
return;
|
|
1082
|
+
this.endpointRefreshConflictPending = false;
|
|
1083
|
+
const boundary = this.endpointRefreshConflictBoundary;
|
|
1084
|
+
if (boundary !== undefined) {
|
|
1085
|
+
await this.failClosedEndpointRefreshConflict(boundary);
|
|
1086
|
+
}
|
|
1087
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "Conflicting Codex endpoint transitions require a clean broker restart.");
|
|
927
1088
|
}
|
|
928
1089
|
isTerminalAdapterDelivery(event) {
|
|
929
1090
|
return (event.state !== "transport_uncertain" &&
|
|
@@ -1030,6 +1191,144 @@ export class GatewayService {
|
|
|
1030
1191
|
generation: authority.journal.new.generation,
|
|
1031
1192
|
}));
|
|
1032
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Rebuild process-owned route and listener authority for retained Codex
|
|
1196
|
+
* registrations during staged startup. The provider's selectRoute performs
|
|
1197
|
+
* the exact loaded/list + history-free resume proof; durable state advances
|
|
1198
|
+
* only after the new native advertisement and status are live.
|
|
1199
|
+
*/
|
|
1200
|
+
async reactivateRetainedCodexRoutesAfterRestartLocked() {
|
|
1201
|
+
const idleAliases = [];
|
|
1202
|
+
const retained = await this.store.inspectPrivateCodexRoutes();
|
|
1203
|
+
for (const route of retained) {
|
|
1204
|
+
if (!route.enabled ||
|
|
1205
|
+
route.state !== "stale" ||
|
|
1206
|
+
route.compatibility !== "expired" ||
|
|
1207
|
+
this.pendingCodexSuccessionRecovery?.journal.new.alias === route.alias ||
|
|
1208
|
+
this.codexSuccessionPoisoned.has(route.alias)) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const codexAdapter = this.adapters.find((adapter) => adapter.identity.provider === "codex" &&
|
|
1212
|
+
adapter.identity.hostId === route.binding.hostId);
|
|
1213
|
+
const claudeAdapter = this.adapters.find((adapter) => adapter.identity.provider === "claude" &&
|
|
1214
|
+
adapter.identity.hostId === route.binding.hostId);
|
|
1215
|
+
if (codexAdapter === undefined || claudeAdapter === undefined) {
|
|
1216
|
+
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
let routeSelected = false;
|
|
1220
|
+
let advertiseAttempted = false;
|
|
1221
|
+
let storeReactivated = false;
|
|
1222
|
+
let deferredEndpointRefresh = false;
|
|
1223
|
+
try {
|
|
1224
|
+
const selected = await codexAdapter.selectRoute({
|
|
1225
|
+
alias: route.alias,
|
|
1226
|
+
routeHandle: route.binding.routeHandle,
|
|
1227
|
+
});
|
|
1228
|
+
routeSelected = true;
|
|
1229
|
+
await this.drainSelectorCallbackSnapshotLocked();
|
|
1230
|
+
if (selected.routeHandle !== route.binding.routeHandle) {
|
|
1231
|
+
throw new BridgeError("CODEX_BOOT_REACTIVATION_MISMATCH", "The retained Codex task changed during broker startup reactivation.");
|
|
1232
|
+
}
|
|
1233
|
+
if (selected.endpointRefresh !== undefined) {
|
|
1234
|
+
deferredEndpointRefresh = await this.onCodexEndpointRefresh(codexAdapter, selected.endpointRefresh, {
|
|
1235
|
+
deferActivation: true,
|
|
1236
|
+
deferredRoute: {
|
|
1237
|
+
alias: route.alias,
|
|
1238
|
+
threadId: route.binding.routeHandle,
|
|
1239
|
+
state: selected.state,
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
if (!deferredEndpointRefresh) {
|
|
1243
|
+
throw new BridgeError("CODEX_BOOT_REACTIVATION_MISMATCH", "The retained Codex task could not stage a compatible endpoint refresh during startup.");
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const binding = {
|
|
1247
|
+
...codexAdapter.identity,
|
|
1248
|
+
routeHandle: route.binding.routeHandle,
|
|
1249
|
+
ownerLease: route.binding.ownerLease,
|
|
1250
|
+
};
|
|
1251
|
+
if (deferredEndpointRefresh) {
|
|
1252
|
+
const afterStaging = await this.store.inspectPrivateRoute(route.alias);
|
|
1253
|
+
storeReactivated =
|
|
1254
|
+
afterStaging !== undefined &&
|
|
1255
|
+
afterStaging.enabled &&
|
|
1256
|
+
afterStaging.compatibility === "compatible" &&
|
|
1257
|
+
bindingKey(afterStaging.binding) === bindingKey(binding);
|
|
1258
|
+
}
|
|
1259
|
+
advertiseAttempted = true;
|
|
1260
|
+
const advertise = claudeAdapter.advertiseNativeCodexPeer;
|
|
1261
|
+
if (advertise === undefined) {
|
|
1262
|
+
throw new BridgeError("CODEX_BOOT_REACTIVATION_UNAVAILABLE", "The retained Codex task cannot rebuild its native advertisement.");
|
|
1263
|
+
}
|
|
1264
|
+
await advertise.call(claudeAdapter, {
|
|
1265
|
+
alias: route.alias,
|
|
1266
|
+
cwd: this.nativePeerCwd,
|
|
1267
|
+
});
|
|
1268
|
+
await claudeAdapter.updateNativeCodexPeerStatus?.(route.alias, selected.state === "idle"
|
|
1269
|
+
? "idle"
|
|
1270
|
+
: selected.state === "awaiting_approval"
|
|
1271
|
+
? "waiting"
|
|
1272
|
+
: "busy");
|
|
1273
|
+
const listenerGeneration = this.requireCurrentNativeCodexGeneration(claudeAdapter, route.alias);
|
|
1274
|
+
if (!storeReactivated) {
|
|
1275
|
+
await this.store.rebindStaleRoute({
|
|
1276
|
+
alias: route.alias,
|
|
1277
|
+
currentOwnerLease: route.binding.ownerLease,
|
|
1278
|
+
newBinding: binding,
|
|
1279
|
+
reason: "endpoint_reobserved",
|
|
1280
|
+
journalReason: "boot_reactivation",
|
|
1281
|
+
state: selected.state,
|
|
1282
|
+
});
|
|
1283
|
+
storeReactivated = true;
|
|
1284
|
+
}
|
|
1285
|
+
this.rememberBinding(route.alias, binding, selected.state);
|
|
1286
|
+
this.lockCodexRegistration({
|
|
1287
|
+
alias: route.alias,
|
|
1288
|
+
threadId: route.binding.routeHandle,
|
|
1289
|
+
hostId: route.binding.hostId,
|
|
1290
|
+
busyPolicy: "queue",
|
|
1291
|
+
}, listenerGeneration);
|
|
1292
|
+
if (deferredEndpointRefresh &&
|
|
1293
|
+
selected.endpointRefresh !== undefined) {
|
|
1294
|
+
await this.onCodexEndpointRefresh(codexAdapter, selected.endpointRefresh, {
|
|
1295
|
+
requiredRoute: {
|
|
1296
|
+
alias: route.alias,
|
|
1297
|
+
threadId: route.binding.routeHandle,
|
|
1298
|
+
state: selected.state,
|
|
1299
|
+
},
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
if (this.requireCurrentNativeCodexGeneration(claudeAdapter, route.alias) !== listenerGeneration) {
|
|
1303
|
+
throw new BridgeError("CODEX_BOOT_REACTIVATION_NATIVE_LISTENER_MISSING", "The retained Codex task's native listener changed during boot reactivation.");
|
|
1304
|
+
}
|
|
1305
|
+
if (selected.state === "idle")
|
|
1306
|
+
idleAliases.push(route.alias);
|
|
1307
|
+
}
|
|
1308
|
+
catch {
|
|
1309
|
+
let cleanupFailed = false;
|
|
1310
|
+
if (advertiseAttempted) {
|
|
1311
|
+
await claudeAdapter
|
|
1312
|
+
.unadvertiseNativeCodexPeer?.(route.alias)
|
|
1313
|
+
.catch(() => {
|
|
1314
|
+
cleanupFailed = true;
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
if (routeSelected) {
|
|
1318
|
+
await codexAdapter
|
|
1319
|
+
.releaseRoute?.(route.binding.routeHandle)
|
|
1320
|
+
.catch(() => {
|
|
1321
|
+
cleanupFailed = true;
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
if (cleanupFailed || storeReactivated || deferredEndpointRefresh) {
|
|
1325
|
+
throw new BridgeError("CODEX_BOOT_REACTIVATION_CLEANUP_FAILED", "A Codex boot reactivation crossed its durable boundary without exact live authority.");
|
|
1326
|
+
}
|
|
1327
|
+
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return idleAliases;
|
|
1331
|
+
}
|
|
1033
1332
|
async completePendingCodexSuccessionRecoveryLocked(params) {
|
|
1034
1333
|
const recovery = this.pendingCodexSuccessionRecovery;
|
|
1035
1334
|
if (recovery === undefined)
|
|
@@ -1744,7 +2043,7 @@ export class GatewayService {
|
|
|
1744
2043
|
claude.rejectedInboundSettlements === 0 &&
|
|
1745
2044
|
codexQuiet &&
|
|
1746
2045
|
this.successionServiceStateClean(aliases) &&
|
|
1747
|
-
this.
|
|
2046
|
+
this.endpointCallbacksQuiet() &&
|
|
1748
2047
|
this.detachedReceiptWrites.size === 0);
|
|
1749
2048
|
}
|
|
1750
2049
|
async codexSuccessionBarrierCleanLocked(oldRegistration) {
|
|
@@ -1771,7 +2070,7 @@ export class GatewayService {
|
|
|
1771
2070
|
claude.monitorFrozen &&
|
|
1772
2071
|
codex.clean &&
|
|
1773
2072
|
this.successionServiceStateClean(aliases) &&
|
|
1774
|
-
this.
|
|
2073
|
+
this.endpointCallbacksQuiet() &&
|
|
1775
2074
|
this.detachedReceiptWrites.size === 0);
|
|
1776
2075
|
}
|
|
1777
2076
|
rescheduleIdleRoutesLocked() {
|
|
@@ -1780,13 +2079,48 @@ export class GatewayService {
|
|
|
1780
2079
|
this.scheduleDispatch(alias);
|
|
1781
2080
|
}
|
|
1782
2081
|
}
|
|
2082
|
+
codexSuccessionIsStable() {
|
|
2083
|
+
const state = this.codexSuccessionState;
|
|
2084
|
+
return (state === undefined ||
|
|
2085
|
+
state.phase === "active_old" ||
|
|
2086
|
+
(state.phase === "active_new" && state.retired === null));
|
|
2087
|
+
}
|
|
2088
|
+
async requireUnchangedStaleCodexOrphan(alias, expected) {
|
|
2089
|
+
let current;
|
|
2090
|
+
try {
|
|
2091
|
+
current = await this.store.inspectStaleCodexOrphan(alias);
|
|
2092
|
+
}
|
|
2093
|
+
catch {
|
|
2094
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed during recovery.");
|
|
2095
|
+
}
|
|
2096
|
+
if (bindingKey(current) !== bindingKey(expected)) {
|
|
2097
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed during recovery.");
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
nativeCodexOrphanBarrierClean(observation, generation) {
|
|
2101
|
+
return (observation.clean &&
|
|
2102
|
+
observation.generation === generation &&
|
|
2103
|
+
observation.activeGenerationMatched &&
|
|
2104
|
+
observation.ingressQuiesced &&
|
|
2105
|
+
observation.monitorFrozen &&
|
|
2106
|
+
!observation.discoveryInFlight &&
|
|
2107
|
+
observation.pendingOutboundReceipts === 0 &&
|
|
2108
|
+
observation.pendingInboundReceipts === 0 &&
|
|
2109
|
+
observation.rejectedInboundSettlements === 0);
|
|
2110
|
+
}
|
|
2111
|
+
endpointCallbacksQuiet() {
|
|
2112
|
+
return (!this.hasPendingCallbackWork() &&
|
|
2113
|
+
this.activeEndpointRefreshCallbackKey === undefined);
|
|
2114
|
+
}
|
|
1783
2115
|
async registerCodex(params) {
|
|
1784
2116
|
if (!params.alias.startsWith("codex-")) {
|
|
1785
2117
|
throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
|
|
1786
2118
|
}
|
|
1787
2119
|
if (this.codexSuccessionPoisoned.has(params.alias) ||
|
|
2120
|
+
this.codexOrphanCleanupPending.has(params.alias) ||
|
|
1788
2121
|
(params.succeedsAlias !== undefined &&
|
|
1789
|
-
this.codexSuccessionPoisoned.has(params.succeedsAlias)
|
|
2122
|
+
(this.codexSuccessionPoisoned.has(params.succeedsAlias) ||
|
|
2123
|
+
this.codexOrphanCleanupPending.has(params.succeedsAlias)))) {
|
|
1790
2124
|
throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "Codex registration is offline until the incomplete succession is manually recovered.");
|
|
1791
2125
|
}
|
|
1792
2126
|
if (params.succeedsAlias !== undefined) {
|
|
@@ -1822,16 +2156,41 @@ export class GatewayService {
|
|
|
1822
2156
|
alias: params.alias,
|
|
1823
2157
|
routeHandle: params.threadId,
|
|
1824
2158
|
});
|
|
2159
|
+
await this.drainSelectorCallbackSnapshotLocked();
|
|
1825
2160
|
if (registered.routeHandle !== params.threadId) {
|
|
1826
2161
|
throw new BridgeError("ROUTE_MISMATCH", "The connector registered a different task.");
|
|
1827
2162
|
}
|
|
2163
|
+
let deferredEndpointRefresh = false;
|
|
2164
|
+
if (registered.endpointRefresh !== undefined) {
|
|
2165
|
+
try {
|
|
2166
|
+
deferredEndpointRefresh = await this.onCodexEndpointRefresh(adapter, registered.endpointRefresh, {
|
|
2167
|
+
deferActivation: true,
|
|
2168
|
+
deferredRoute: {
|
|
2169
|
+
alias: params.alias,
|
|
2170
|
+
threadId: params.threadId,
|
|
2171
|
+
state: registered.state,
|
|
2172
|
+
},
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
catch (error) {
|
|
2176
|
+
await adapter.releaseRoute?.(params.threadId).catch(() => undefined);
|
|
2177
|
+
throw error;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
1828
2180
|
const binding = {
|
|
1829
2181
|
...adapter.identity,
|
|
1830
2182
|
routeHandle: params.threadId,
|
|
1831
2183
|
ownerLease: stableLease("codex", `${params.hostId}\0${params.threadId}`),
|
|
1832
2184
|
};
|
|
1833
2185
|
try {
|
|
1834
|
-
await this.
|
|
2186
|
+
const afterRefresh = await this.store.inspectPrivateRoute(params.alias);
|
|
2187
|
+
const alreadyInstalledExactly = afterRefresh !== undefined &&
|
|
2188
|
+
afterRefresh.enabled &&
|
|
2189
|
+
afterRefresh.compatibility === "compatible" &&
|
|
2190
|
+
bindingKey(afterRefresh.binding) === bindingKey(binding);
|
|
2191
|
+
if (!alreadyInstalledExactly || !deferredEndpointRefresh) {
|
|
2192
|
+
await this.registerOrRebind(params.alias, binding, "endpoint_reobserved", registered.state);
|
|
2193
|
+
}
|
|
1835
2194
|
}
|
|
1836
2195
|
catch (error) {
|
|
1837
2196
|
await adapter.releaseRoute?.(params.threadId).catch(() => undefined);
|
|
@@ -1857,10 +2216,32 @@ export class GatewayService {
|
|
|
1857
2216
|
: "busy");
|
|
1858
2217
|
listenerGeneration = this.requireCurrentNativeCodexGeneration(claudeAdapter, params.alias);
|
|
1859
2218
|
await this.completePendingCodexSuccessionRecoveryLocked(params);
|
|
2219
|
+
this.lockCodexRegistration(params, listenerGeneration);
|
|
2220
|
+
if (deferredEndpointRefresh &&
|
|
2221
|
+
registered.endpointRefresh !== undefined) {
|
|
2222
|
+
await this.onCodexEndpointRefresh(adapter, registered.endpointRefresh, {
|
|
2223
|
+
requiredRoute: {
|
|
2224
|
+
alias: params.alias,
|
|
2225
|
+
threadId: params.threadId,
|
|
2226
|
+
state: registered.state,
|
|
2227
|
+
},
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
1860
2230
|
await this.changed();
|
|
1861
2231
|
}
|
|
1862
2232
|
catch (error) {
|
|
1863
2233
|
if (routeBeforeRegistration !== undefined) {
|
|
2234
|
+
if (deferredEndpointRefresh &&
|
|
2235
|
+
registered.endpointRefresh?.outcome === "compatible") {
|
|
2236
|
+
try {
|
|
2237
|
+
await this.store.markConnectorOffline(registered.endpointRefresh.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
|
|
2238
|
+
this.routeStates.set(params.alias, "stale");
|
|
2239
|
+
await this.changed();
|
|
2240
|
+
}
|
|
2241
|
+
catch {
|
|
2242
|
+
this.dashboardHealthy = false;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
1864
2245
|
// A persisted or live exact route, its queue, and any advertisement
|
|
1865
2246
|
// predate this invocation, so a later status/dashboard failure cannot
|
|
1866
2247
|
// safely delete them as "rollback". A post-restart reactivation also
|
|
@@ -1971,6 +2352,220 @@ export class GatewayService {
|
|
|
1971
2352
|
});
|
|
1972
2353
|
await this.changed();
|
|
1973
2354
|
}
|
|
2355
|
+
finalizeStaleCodexOrphanRemovalLocked(alias) {
|
|
2356
|
+
const aliases = new Set([alias]);
|
|
2357
|
+
this.forgetBinding(alias);
|
|
2358
|
+
this.purgeSuccessionTransientState(aliases);
|
|
2359
|
+
const lock = this.codexRegistrationLocks.get(alias);
|
|
2360
|
+
if (lock !== undefined) {
|
|
2361
|
+
this.codexRegistrationLocks.delete(alias);
|
|
2362
|
+
const state = this.codexSuccessionState;
|
|
2363
|
+
if ((state?.phase === "active_old" && state.active.alias === alias) ||
|
|
2364
|
+
(state?.phase === "active_new" && state.active.alias === alias)) {
|
|
2365
|
+
this.codexSuccessionState = undefined;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
this.codexOrphanCleanupPending.delete(alias);
|
|
2369
|
+
this.codexSuccessionDispatchFrozen.delete(alias);
|
|
2370
|
+
}
|
|
2371
|
+
async removeStaleCodexRegistration(alias) {
|
|
2372
|
+
const pendingCleanup = this.codexOrphanCleanupPending.get(alias);
|
|
2373
|
+
if (pendingCleanup !== undefined &&
|
|
2374
|
+
(await this.store.wasStaleCodexOrphanRemovalCommitted({
|
|
2375
|
+
alias,
|
|
2376
|
+
binding: pendingCleanup.binding,
|
|
2377
|
+
previousSequence: pendingCleanup.previousSequence,
|
|
2378
|
+
}))) {
|
|
2379
|
+
this.finalizeStaleCodexOrphanRemovalLocked(alias);
|
|
2380
|
+
await this.changed();
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const authority = await this.store.inspectStaleCodexOrphanRemovalAuthority(alias);
|
|
2384
|
+
const binding = authority.binding;
|
|
2385
|
+
const aliases = new Set([alias]);
|
|
2386
|
+
const resumesExactCleanup = pendingCleanup !== undefined &&
|
|
2387
|
+
bindingKey(pendingCleanup.binding) === bindingKey(binding) &&
|
|
2388
|
+
pendingCleanup.previousSequence === authority.previousSequence;
|
|
2389
|
+
if (pendingCleanup !== undefined && !resumesExactCleanup) {
|
|
2390
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed after native cleanup.");
|
|
2391
|
+
}
|
|
2392
|
+
if (!this.codexSuccessionIsStable() ||
|
|
2393
|
+
this.pendingCodexSuccessionRecovery !== undefined ||
|
|
2394
|
+
this.codexSuccessionPoisoned.has(alias) ||
|
|
2395
|
+
(this.codexSuccessionDispatchFrozen.has(alias) && !resumesExactCleanup)) {
|
|
2396
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BLOCKED", "Codex orphan recovery cannot run during registration succession.");
|
|
2397
|
+
}
|
|
2398
|
+
const lock = this.codexRegistrationLocks.get(alias);
|
|
2399
|
+
if (lock !== undefined &&
|
|
2400
|
+
(lock.threadId !== binding.routeHandle ||
|
|
2401
|
+
lock.hostId !== binding.hostId ||
|
|
2402
|
+
lock.generation === "unconfirmed" ||
|
|
2403
|
+
!isCodexRegistrationGeneration(lock.generation))) {
|
|
2404
|
+
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener identity cannot be proven for orphan recovery.");
|
|
2405
|
+
}
|
|
2406
|
+
const claudeAdapter = this.adapter("claude", binding.hostId);
|
|
2407
|
+
const codexAdapter = this.adapter("codex", binding.hostId);
|
|
2408
|
+
const currentNative = claudeAdapter.currentNativeCodexPeerGeneration;
|
|
2409
|
+
if (currentNative === undefined) {
|
|
2410
|
+
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The Claude provider cannot prove whether the native Codex listener exists.");
|
|
2411
|
+
}
|
|
2412
|
+
this.codexSuccessionDispatchFrozen.add(alias);
|
|
2413
|
+
this.scheduledDispatchTargets.delete(alias);
|
|
2414
|
+
let nativeGeneration;
|
|
2415
|
+
let nativeQuiesceAttempted = false;
|
|
2416
|
+
let irreversibleCleanupStarted = resumesExactCleanup;
|
|
2417
|
+
try {
|
|
2418
|
+
try {
|
|
2419
|
+
const observed = currentNative.call(claudeAdapter, alias);
|
|
2420
|
+
if (!isCodexRegistrationGeneration(observed)) {
|
|
2421
|
+
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener generation is malformed.");
|
|
2422
|
+
}
|
|
2423
|
+
if (lock !== undefined && lock.generation !== observed) {
|
|
2424
|
+
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener generation changed before orphan recovery.");
|
|
2425
|
+
}
|
|
2426
|
+
nativeGeneration = observed;
|
|
2427
|
+
}
|
|
2428
|
+
catch (error) {
|
|
2429
|
+
const exactAbsence = error instanceof BridgeError &&
|
|
2430
|
+
error.code === "CODEX_PEER_GENERATION_MISMATCH";
|
|
2431
|
+
if (!exactAbsence || (lock !== undefined && !resumesExactCleanup)) {
|
|
2432
|
+
if (error instanceof BridgeError &&
|
|
2433
|
+
error.code === "CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN") {
|
|
2434
|
+
throw error;
|
|
2435
|
+
}
|
|
2436
|
+
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener identity cannot be proven for orphan recovery.");
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
let observeNative;
|
|
2440
|
+
if (nativeGeneration !== undefined) {
|
|
2441
|
+
const quiesce = this.requireSuccessionMethod(claudeAdapter, "quiesceNativeCodexPeerGeneration");
|
|
2442
|
+
// Require the inverse operation before acquiring the quiescence fence
|
|
2443
|
+
// so every pre-unadvertise failure has one exact cleanup path.
|
|
2444
|
+
this.requireSuccessionMethod(claudeAdapter, "resumeNativeCodexPeerGeneration");
|
|
2445
|
+
observeNative = this.requireSuccessionMethod(claudeAdapter, "observeNativeCodexSuccessionBarrier");
|
|
2446
|
+
nativeQuiesceAttempted = true;
|
|
2447
|
+
await quiesce.call(claudeAdapter, nativeGeneration);
|
|
2448
|
+
}
|
|
2449
|
+
// Join every native frame admitted before the exact generation fence,
|
|
2450
|
+
// and every detached terminal receipt write those frames create.
|
|
2451
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
2452
|
+
await this.drainCallbackQueueLocked();
|
|
2453
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
2454
|
+
await this.requireUnchangedStaleCodexOrphan(alias, binding);
|
|
2455
|
+
const observeCodex = codexAdapter.observeRouteSuccessionBarrier;
|
|
2456
|
+
if (observeCodex === undefined) {
|
|
2457
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The Codex provider cannot prove an empty orphan route boundary.");
|
|
2458
|
+
}
|
|
2459
|
+
const beforeRelease = observeCodex.call(codexAdapter, binding.routeHandle);
|
|
2460
|
+
if (beforeRelease.routePresent ||
|
|
2461
|
+
!this.codexRouteSuccessionQuiet(beforeRelease) ||
|
|
2462
|
+
beforeRelease.routeCreationInFlight ||
|
|
2463
|
+
beforeRelease.routeReleaseInFlight) {
|
|
2464
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration still has a live provider or endpoint-refresh claim.");
|
|
2465
|
+
}
|
|
2466
|
+
if (nativeGeneration !== undefined) {
|
|
2467
|
+
const nativeBarrier = await observeNative.call(claudeAdapter, nativeGeneration);
|
|
2468
|
+
if (!this.nativeCodexOrphanBarrierClean(nativeBarrier, nativeGeneration)) {
|
|
2469
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The native Codex listener still owns ingress or receipt work.");
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (!this.successionServiceStateClean(aliases) ||
|
|
2473
|
+
!this.endpointCallbacksQuiet() ||
|
|
2474
|
+
this.detachedReceiptWrites.size !== 0) {
|
|
2475
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration still has a live service claim.");
|
|
2476
|
+
}
|
|
2477
|
+
// The awaited native barrier can admit a provider-side route recovery
|
|
2478
|
+
// claim. Re-sample the Codex boundary after that last await and directly
|
|
2479
|
+
// before the generation-unqualified release call.
|
|
2480
|
+
const releaseBoundary = observeCodex.call(codexAdapter, binding.routeHandle);
|
|
2481
|
+
if (releaseBoundary.routePresent ||
|
|
2482
|
+
!this.codexRouteSuccessionQuiet(releaseBoundary) ||
|
|
2483
|
+
releaseBoundary.routeCreationInFlight ||
|
|
2484
|
+
releaseBoundary.routeReleaseInFlight) {
|
|
2485
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration acquired a provider recovery claim before release.");
|
|
2486
|
+
}
|
|
2487
|
+
// The pre-barrier proves this release targets an absent stale handle and
|
|
2488
|
+
// cannot join an already-observed creation. Revalidate immediately after
|
|
2489
|
+
// the provider join before touching the native advertisement.
|
|
2490
|
+
await codexAdapter.releaseRoute?.(binding.routeHandle);
|
|
2491
|
+
await this.drainCallbackQueueLocked();
|
|
2492
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
2493
|
+
await this.requireUnchangedStaleCodexOrphan(alias, binding);
|
|
2494
|
+
const afterRelease = observeCodex.call(codexAdapter, binding.routeHandle);
|
|
2495
|
+
if (afterRelease.routePresent ||
|
|
2496
|
+
!this.codexRouteSuccessionQuiet(afterRelease) ||
|
|
2497
|
+
afterRelease.routeCreationInFlight ||
|
|
2498
|
+
afterRelease.routeReleaseInFlight ||
|
|
2499
|
+
!this.successionServiceStateClean(aliases) ||
|
|
2500
|
+
!this.endpointCallbacksQuiet() ||
|
|
2501
|
+
this.detachedReceiptWrites.size !== 0) {
|
|
2502
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The stale Codex registration changed at the provider cleanup boundary.");
|
|
2503
|
+
}
|
|
2504
|
+
if (nativeGeneration !== undefined) {
|
|
2505
|
+
const nativeBarrier = await observeNative.call(claudeAdapter, nativeGeneration);
|
|
2506
|
+
if (!this.nativeCodexOrphanBarrierClean(nativeBarrier, nativeGeneration)) {
|
|
2507
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BUSY", "The native Codex listener changed during provider cleanup.");
|
|
2508
|
+
}
|
|
2509
|
+
if (currentNative.call(claudeAdapter, alias) !== nativeGeneration) {
|
|
2510
|
+
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener generation changed before removal.");
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
await this.requireUnchangedStaleCodexOrphan(alias, binding);
|
|
2514
|
+
if (nativeGeneration !== undefined) {
|
|
2515
|
+
const unadvertise = claudeAdapter.unadvertiseNativeCodexPeer;
|
|
2516
|
+
if (unadvertise === undefined) {
|
|
2517
|
+
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener cannot be removed exactly.");
|
|
2518
|
+
}
|
|
2519
|
+
irreversibleCleanupStarted = true;
|
|
2520
|
+
this.codexOrphanCleanupPending.set(alias, {
|
|
2521
|
+
binding: { ...binding },
|
|
2522
|
+
previousSequence: authority.previousSequence,
|
|
2523
|
+
});
|
|
2524
|
+
// The supervisor may remove the exact helper and then report an
|
|
2525
|
+
// ambiguous close failure. Retain retry authority before invoking the
|
|
2526
|
+
// irreversible operation; a retry proves whether the listener remains.
|
|
2527
|
+
await unadvertise.call(claudeAdapter, alias);
|
|
2528
|
+
}
|
|
2529
|
+
// Exact native absence is also an irreversible cleanup boundary: if the
|
|
2530
|
+
// durable mutation fails, only this same binding may resume removal.
|
|
2531
|
+
this.codexOrphanCleanupPending.set(alias, {
|
|
2532
|
+
binding: { ...binding },
|
|
2533
|
+
previousSequence: authority.previousSequence,
|
|
2534
|
+
});
|
|
2535
|
+
irreversibleCleanupStarted = true;
|
|
2536
|
+
const removed = await this.store.removeStaleCodexOrphan({ alias });
|
|
2537
|
+
if (bindingKey(removed.binding) !== bindingKey(binding)) {
|
|
2538
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_MISMATCH", "The stale Codex registration changed during recovery.");
|
|
2539
|
+
}
|
|
2540
|
+
this.finalizeStaleCodexOrphanRemovalLocked(alias);
|
|
2541
|
+
await this.changed();
|
|
2542
|
+
}
|
|
2543
|
+
catch (error) {
|
|
2544
|
+
if (!irreversibleCleanupStarted) {
|
|
2545
|
+
let resumeFailed = false;
|
|
2546
|
+
if (nativeGeneration !== undefined && nativeQuiesceAttempted) {
|
|
2547
|
+
try {
|
|
2548
|
+
const resume = this.requireSuccessionMethod(claudeAdapter, "resumeNativeCodexPeerGeneration");
|
|
2549
|
+
await resume.call(claudeAdapter, nativeGeneration);
|
|
2550
|
+
}
|
|
2551
|
+
catch {
|
|
2552
|
+
resumeFailed = true;
|
|
2553
|
+
this.dashboardHealthy = false;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
if (!resumeFailed) {
|
|
2557
|
+
this.codexSuccessionDispatchFrozen.delete(alias);
|
|
2558
|
+
if (this.routeStates.get(alias) === "idle") {
|
|
2559
|
+
this.scheduleDispatch(alias);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
else {
|
|
2563
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_CLEANUP_FAILED", "The native Codex listener could not be resumed after failed orphan recovery.");
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
throw error;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
1974
2569
|
async discoveryPublicFingerprint() {
|
|
1975
2570
|
const base = await this.store.publicSnapshot();
|
|
1976
2571
|
const snapshot = projectGatewayPublicSnapshot({
|
|
@@ -1993,13 +2588,22 @@ export class GatewayService {
|
|
|
1993
2588
|
const publicBefore = await this.discoveryPublicFingerprint();
|
|
1994
2589
|
this.candidates.clear();
|
|
1995
2590
|
const rowsByAlias = new Map();
|
|
2591
|
+
const collisionByRouteHandle = new Map();
|
|
1996
2592
|
const discoveredCandidates = [];
|
|
1997
2593
|
const observedSelected = new Set();
|
|
1998
2594
|
for (const adapter of this.adapters.filter((item) => item.identity.provider === "claude")) {
|
|
1999
|
-
|
|
2595
|
+
let discovered = {
|
|
2000
2596
|
peers: [],
|
|
2001
2597
|
complete: false,
|
|
2002
2598
|
};
|
|
2599
|
+
try {
|
|
2600
|
+
discovered = (await adapter.discoverClaudePeers?.()) ?? discovered;
|
|
2601
|
+
}
|
|
2602
|
+
catch {
|
|
2603
|
+
// Registry observation is dashboard evidence, not Claude mailbox
|
|
2604
|
+
// authority. The provider still revalidates the exact target before
|
|
2605
|
+
// any write.
|
|
2606
|
+
}
|
|
2003
2607
|
const grouped = new Map();
|
|
2004
2608
|
const byHandle = new Map();
|
|
2005
2609
|
for (const peer of discovered.peers) {
|
|
@@ -2019,6 +2623,8 @@ export class GatewayService {
|
|
|
2019
2623
|
if (peer === undefined)
|
|
2020
2624
|
continue;
|
|
2021
2625
|
if (matches.length !== 1) {
|
|
2626
|
+
for (const match of matches)
|
|
2627
|
+
collisionByRouteHandle.set(match.routeHandle, "PEER_ALIAS_COLLISION");
|
|
2022
2628
|
rowsByAlias.set(alias, {
|
|
2023
2629
|
peer,
|
|
2024
2630
|
adapter,
|
|
@@ -2035,6 +2641,7 @@ export class GatewayService {
|
|
|
2035
2641
|
continue;
|
|
2036
2642
|
}
|
|
2037
2643
|
if ((byHandle.get(peer.routeHandle)?.length ?? 0) !== 1) {
|
|
2644
|
+
collisionByRouteHandle.set(peer.routeHandle, "PEER_SESSION_COLLISION");
|
|
2038
2645
|
rowsByAlias.set(alias, {
|
|
2039
2646
|
peer,
|
|
2040
2647
|
adapter,
|
|
@@ -2060,6 +2667,8 @@ export class GatewayService {
|
|
|
2060
2667
|
const collision = this.routeBindings.get(alias);
|
|
2061
2668
|
if (collision !== undefined &&
|
|
2062
2669
|
bindingKey(collision) !== bindingKey(existing[1])) {
|
|
2670
|
+
collisionByRouteHandle.set(routeHandle, "PEER_ALIAS_COLLISION");
|
|
2671
|
+
collisionByRouteHandle.set(collision.routeHandle, "PEER_ALIAS_COLLISION");
|
|
2063
2672
|
rowsByAlias.set(alias, {
|
|
2064
2673
|
peer: candidate,
|
|
2065
2674
|
adapter,
|
|
@@ -2074,10 +2683,16 @@ export class GatewayService {
|
|
|
2074
2683
|
catch (error) {
|
|
2075
2684
|
if (!(error instanceof BridgeError))
|
|
2076
2685
|
throw error;
|
|
2686
|
+
// Only a real alias collision may wear that receipt; every other
|
|
2687
|
+
// rename failure is an unobserved peer, not a name conflict.
|
|
2688
|
+
const safeErrorCode = error.code === "ROUTE_ALIAS_COLLISION"
|
|
2689
|
+
? "PEER_ALIAS_COLLISION"
|
|
2690
|
+
: "PEER_NOT_OBSERVED";
|
|
2691
|
+
collisionByRouteHandle.set(routeHandle, safeErrorCode);
|
|
2077
2692
|
rowsByAlias.set(alias, {
|
|
2078
2693
|
peer: candidate,
|
|
2079
2694
|
adapter,
|
|
2080
|
-
safeErrorCode
|
|
2695
|
+
safeErrorCode,
|
|
2081
2696
|
});
|
|
2082
2697
|
}
|
|
2083
2698
|
}
|
|
@@ -2146,6 +2761,9 @@ export class GatewayService {
|
|
|
2146
2761
|
observedSelected.has(bindingKey(binding))) {
|
|
2147
2762
|
continue;
|
|
2148
2763
|
}
|
|
2764
|
+
const discoveryError = collisionByRouteHandle.get(binding.routeHandle);
|
|
2765
|
+
if (discoveryError === undefined)
|
|
2766
|
+
continue;
|
|
2149
2767
|
const inspection = await this.store.inspectPrivateRoute(alias);
|
|
2150
2768
|
if (inspection !== undefined &&
|
|
2151
2769
|
inspection.enabled &&
|
|
@@ -2153,9 +2771,9 @@ export class GatewayService {
|
|
|
2153
2771
|
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
2154
2772
|
const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(alias, {
|
|
2155
2773
|
unwrittenOutcome: "failed",
|
|
2156
|
-
safeErrorCode:
|
|
2774
|
+
safeErrorCode: discoveryError,
|
|
2157
2775
|
});
|
|
2158
|
-
const settlements = await this.store.invalidateRoute(binding,
|
|
2776
|
+
const settlements = await this.store.invalidateRoute(binding, discoveryError, inFlightSettlements);
|
|
2159
2777
|
for (const settlement of settlements) {
|
|
2160
2778
|
await this.applyTerminalSettlementLocked(settlement);
|
|
2161
2779
|
}
|
|
@@ -2382,6 +3000,11 @@ export class GatewayService {
|
|
|
2382
3000
|
async resolveSelectedClaudeDestination(selector) {
|
|
2383
3001
|
const discoveryChanged = await this.refreshClaudeDiscovery();
|
|
2384
3002
|
try {
|
|
3003
|
+
const selected = [...this.routeBindings.entries()].find(([alias, binding]) => binding.provider === "claude" &&
|
|
3004
|
+
(alias === selector ||
|
|
3005
|
+
binding.routeHandle.toLowerCase() === selector.toLowerCase()));
|
|
3006
|
+
if (selected !== undefined)
|
|
3007
|
+
return { alias: selected[0], discoveryChanged };
|
|
2385
3008
|
const candidate = this.claudeCandidate(selector);
|
|
2386
3009
|
if (candidate === undefined) {
|
|
2387
3010
|
throw new BridgeError("PEER_NOT_FOUND", "No unique compatible interactive peer matches that current name or session UUID.");
|
|
@@ -2568,6 +3191,7 @@ export class GatewayService {
|
|
|
2568
3191
|
const previous = this.routeBindings.get(alias);
|
|
2569
3192
|
if (previous !== undefined &&
|
|
2570
3193
|
bindingKey(previous) !== bindingKey(binding)) {
|
|
3194
|
+
this.bindingAliases.delete(bindingKey(previous));
|
|
2571
3195
|
this.pendingClaudeReplies.delete(bindingKey(previous));
|
|
2572
3196
|
}
|
|
2573
3197
|
this.routeBindings.set(alias, binding);
|
|
@@ -2695,8 +3319,10 @@ export class GatewayService {
|
|
|
2695
3319
|
enqueuedAt: input.enqueuedAt,
|
|
2696
3320
|
stallAt,
|
|
2697
3321
|
deadlineAt,
|
|
2698
|
-
// Deferred means the provider proved no write.
|
|
2699
|
-
// attempt per 500 ms slice
|
|
3322
|
+
// Deferred means the provider proved no write. This budgets one
|
|
3323
|
+
// attempt per 500 ms slice of this message's immutable deadline; a
|
|
3324
|
+
// Codex-bound retry spends one slice each, while a Claude-bound retry
|
|
3325
|
+
// backs off exponentially and spends far fewer of them.
|
|
2700
3326
|
maxCleanPrewriteRetries: Math.max(0, Math.ceil((deadlineAt - input.enqueuedAt) / 500)),
|
|
2701
3327
|
nativeReceipt: input.nativeReceipt !== undefined,
|
|
2702
3328
|
maxNativeReceiptCleanPrewriteRetries: DELIVERY_ACK_RETRY_DELAYS_MS.length,
|
|
@@ -3313,12 +3939,10 @@ export class GatewayService {
|
|
|
3313
3939
|
`Embassy 自动活跃检查 — ${action.conversationId}]\n` +
|
|
3314
3940
|
"Reply with the result or a status. / 请回复结果或状态。";
|
|
3315
3941
|
try {
|
|
3316
|
-
await this.enqueue(action.ownerAlias, action.workerAlias, text, false,
|
|
3942
|
+
await this.enqueue(action.ownerAlias, action.workerAlias, text, false, {
|
|
3317
3943
|
existingConversationId: action.conversationId,
|
|
3318
|
-
requestedHopCount: 0,
|
|
3319
3944
|
exposeDeliveryToken: false,
|
|
3320
3945
|
skipProgressWatchActivity: true,
|
|
3321
|
-
preserveConversationHop: true,
|
|
3322
3946
|
progressWatchNudge: {
|
|
3323
3947
|
conversationId: action.conversationId,
|
|
3324
3948
|
nudgeNumber: action.nudgeNumber,
|
|
@@ -3551,7 +4175,7 @@ export class GatewayService {
|
|
|
3551
4175
|
await this.verifyCodex(params.fromAlias, params.threadId);
|
|
3552
4176
|
const resolved = await this.resolveSelectedClaudeDestination(params.toAlias);
|
|
3553
4177
|
discoveryChanged = resolved.discoveryChanged;
|
|
3554
|
-
return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply,
|
|
4178
|
+
return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, params.trackIdleMinutes === undefined
|
|
3555
4179
|
? {}
|
|
3556
4180
|
: { trackIdleMinutes: params.trackIdleMinutes }));
|
|
3557
4181
|
}
|
|
@@ -3570,7 +4194,7 @@ export class GatewayService {
|
|
|
3570
4194
|
try {
|
|
3571
4195
|
await this.verifyClaude(params.fromAlias, params.replyAddress);
|
|
3572
4196
|
const steer = this.config.steeringEnabled && params.text.startsWith("STEER:");
|
|
3573
|
-
return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply,
|
|
4197
|
+
return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, {
|
|
3574
4198
|
...(steer ? { steer: true } : {}),
|
|
3575
4199
|
...(params.trackIdleMinutes === undefined
|
|
3576
4200
|
? {}
|
|
@@ -3599,10 +4223,9 @@ export class GatewayService {
|
|
|
3599
4223
|
throw new BridgeError("ROUTE_MISMATCH", "The caller is not a conversation participant.");
|
|
3600
4224
|
const result = caller.kind === "codex" &&
|
|
3601
4225
|
this.nativeIngressByConversation.has(conversation.id)
|
|
3602
|
-
? await this.enqueueNativeReply(conversation, from, params.text,
|
|
3603
|
-
: await this.enqueue(from, to, params.text, false,
|
|
4226
|
+
? await this.enqueueNativeReply(conversation, from, params.text, true, params.trackIdleMinutes)
|
|
4227
|
+
: await this.enqueue(from, to, params.text, false, {
|
|
3604
4228
|
existingConversationId: conversation.id,
|
|
3605
|
-
requestedHopCount: conversation.lastHopCount + 1,
|
|
3606
4229
|
...(params.trackIdleMinutes === undefined
|
|
3607
4230
|
? {}
|
|
3608
4231
|
: { trackIdleMinutes: params.trackIdleMinutes }),
|
|
@@ -3614,7 +4237,7 @@ export class GatewayService {
|
|
|
3614
4237
|
}
|
|
3615
4238
|
});
|
|
3616
4239
|
}
|
|
3617
|
-
async enqueue(sourceAlias, targetAlias, text, expectsReply,
|
|
4240
|
+
async enqueue(sourceAlias, targetAlias, text, expectsReply, options = {}) {
|
|
3618
4241
|
if (this.codexSuccessionPoisoned.has(sourceAlias) ||
|
|
3619
4242
|
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
3620
4243
|
throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "No message can be accepted while Codex succession requires manual recovery.");
|
|
@@ -3637,11 +4260,8 @@ export class GatewayService {
|
|
|
3637
4260
|
targetAlias,
|
|
3638
4261
|
expectsReply,
|
|
3639
4262
|
nextSequence: 0,
|
|
3640
|
-
lastHopCount: 0,
|
|
3641
4263
|
lastActivityAt: this.now().toISOString(),
|
|
3642
4264
|
};
|
|
3643
|
-
const hopCount = options.requestedHopCount ??
|
|
3644
|
-
(isReply ? conversation.lastHopCount + 1 : 0);
|
|
3645
4265
|
const sequence = conversation.nextSequence;
|
|
3646
4266
|
const enqueuedAt = this.now().getTime();
|
|
3647
4267
|
const deliveryToken = (options.exposeDeliveryToken ?? true)
|
|
@@ -3654,7 +4274,6 @@ export class GatewayService {
|
|
|
3654
4274
|
body: text,
|
|
3655
4275
|
dedupeKey: `${conversationId}:${sequence}`,
|
|
3656
4276
|
conversationIdSuffix: conversationId.slice(-8),
|
|
3657
|
-
hopCount,
|
|
3658
4277
|
deadlineAt,
|
|
3659
4278
|
...(options.steer === true ? { steer: true } : {}),
|
|
3660
4279
|
...(options.skipProgressWatchActivity === true
|
|
@@ -3675,15 +4294,10 @@ export class GatewayService {
|
|
|
3675
4294
|
if (queued.pair === true)
|
|
3676
4295
|
conversation.pair = true;
|
|
3677
4296
|
conversation.nextSequence += 1;
|
|
3678
|
-
if (options.preserveConversationHop !== true) {
|
|
3679
|
-
conversation.lastHopCount = hopCount;
|
|
3680
|
-
}
|
|
3681
4297
|
conversation.lastActivityAt = this.now().toISOString();
|
|
3682
4298
|
this.messageContexts.set(queued.messageId, {
|
|
3683
4299
|
conversationId,
|
|
3684
|
-
isReply,
|
|
3685
4300
|
expectsReply,
|
|
3686
|
-
hopCount,
|
|
3687
4301
|
sequence,
|
|
3688
4302
|
targetBindingKey: bindingKey(target),
|
|
3689
4303
|
authorization: "selected_route",
|
|
@@ -3702,7 +4316,6 @@ export class GatewayService {
|
|
|
3702
4316
|
this.pendingClaudeReplies.set(bindingKey(target), {
|
|
3703
4317
|
conversationId,
|
|
3704
4318
|
bindingKey: bindingKey(target),
|
|
3705
|
-
hopCount,
|
|
3706
4319
|
deadlineAt,
|
|
3707
4320
|
tainted: false,
|
|
3708
4321
|
});
|
|
@@ -3716,7 +4329,7 @@ export class GatewayService {
|
|
|
3716
4329
|
...(deliveryToken === undefined ? {} : { deliveryToken }),
|
|
3717
4330
|
};
|
|
3718
4331
|
}
|
|
3719
|
-
async enqueueNativeReply(conversation, sourceAlias, text,
|
|
4332
|
+
async enqueueNativeReply(conversation, sourceAlias, text, exposeDeliveryToken = true, trackIdleMinutes) {
|
|
3720
4333
|
this.pruneTransient();
|
|
3721
4334
|
const capability = this.nativeIngressByConversation.get(conversation.id);
|
|
3722
4335
|
if (capability === undefined ||
|
|
@@ -3740,7 +4353,6 @@ export class GatewayService {
|
|
|
3740
4353
|
body: text,
|
|
3741
4354
|
dedupeKey: `${conversation.id}:${sequence}`,
|
|
3742
4355
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
3743
|
-
hopCount: requestedHopCount,
|
|
3744
4356
|
deadlineAt,
|
|
3745
4357
|
...(conversation.pair === true ? { pair: true } : {}),
|
|
3746
4358
|
progressWatch: progressWatchActivity(text, conversation.id, sourceAlias, trackIdleMinutes),
|
|
@@ -3749,13 +4361,10 @@ export class GatewayService {
|
|
|
3749
4361
|
throw new BridgeError("MESSAGE_REJECTED", "The native reply was not accepted.");
|
|
3750
4362
|
}
|
|
3751
4363
|
conversation.nextSequence += 1;
|
|
3752
|
-
conversation.lastHopCount = requestedHopCount;
|
|
3753
4364
|
conversation.lastActivityAt = this.now().toISOString();
|
|
3754
4365
|
this.messageContexts.set(queued.messageId, {
|
|
3755
4366
|
conversationId: conversation.id,
|
|
3756
|
-
isReply: true,
|
|
3757
4367
|
expectsReply: false,
|
|
3758
|
-
hopCount: requestedHopCount,
|
|
3759
4368
|
sequence,
|
|
3760
4369
|
targetBindingKey: bindingKey(capability.binding),
|
|
3761
4370
|
nativeReplyBinding: { ...capability.binding },
|
|
@@ -3787,7 +4396,7 @@ export class GatewayService {
|
|
|
3787
4396
|
* native-ingress boundary; this preserves the exact connector generation
|
|
3788
4397
|
* without recreating route authority or accepting a new unsolicited send.
|
|
3789
4398
|
*/
|
|
3790
|
-
async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text
|
|
4399
|
+
async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text) {
|
|
3791
4400
|
const target = await this.store.resolveRoute(conversation.sourceAlias);
|
|
3792
4401
|
if (sourceBinding.provider !== "claude" ||
|
|
3793
4402
|
target.provider !== "codex" ||
|
|
@@ -3806,7 +4415,6 @@ export class GatewayService {
|
|
|
3806
4415
|
body: text,
|
|
3807
4416
|
dedupeKey: `${conversation.id}:${sequence}`,
|
|
3808
4417
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
3809
|
-
hopCount: requestedHopCount,
|
|
3810
4418
|
deadlineAt,
|
|
3811
4419
|
authorizedPairTeardownReply: true,
|
|
3812
4420
|
progressWatch: progressWatchActivity(text, conversation.id, conversation.targetAlias),
|
|
@@ -3815,16 +4423,13 @@ export class GatewayService {
|
|
|
3815
4423
|
throw new BridgeError("MESSAGE_REJECTED", "The retained provider reply was not accepted.");
|
|
3816
4424
|
}
|
|
3817
4425
|
conversation.nextSequence += 1;
|
|
3818
|
-
conversation.lastHopCount = requestedHopCount;
|
|
3819
4426
|
conversation.lastActivityAt = this.now().toISOString();
|
|
3820
4427
|
// The old edge has already been removed. Retain only this already-observed
|
|
3821
4428
|
// reply attempt; the conversation no longer grants pair reply authority.
|
|
3822
4429
|
delete conversation.pair;
|
|
3823
4430
|
this.messageContexts.set(queued.messageId, {
|
|
3824
4431
|
conversationId: conversation.id,
|
|
3825
|
-
isReply: true,
|
|
3826
4432
|
expectsReply: false,
|
|
3827
|
-
hopCount: requestedHopCount,
|
|
3828
4433
|
sequence,
|
|
3829
4434
|
targetBindingKey: bindingKey(target),
|
|
3830
4435
|
authorization: "selected_route",
|
|
@@ -3923,6 +4528,42 @@ export class GatewayService {
|
|
|
3923
4528
|
await this.publishNativeHeldLocked(afterCallbacks);
|
|
3924
4529
|
}
|
|
3925
4530
|
}
|
|
4531
|
+
restoreQueuedMessageContextLocked(item, binding) {
|
|
4532
|
+
this.pruneTransient();
|
|
4533
|
+
const generatedConversationId = createGatewayConversationId();
|
|
4534
|
+
const conversationId = item.conversationIdSuffix === undefined
|
|
4535
|
+
? generatedConversationId
|
|
4536
|
+
: `${generatedConversationId.slice(0, -8)}${item.conversationIdSuffix}`;
|
|
4537
|
+
const conversation = {
|
|
4538
|
+
id: conversationId,
|
|
4539
|
+
sourceAlias: item.sourceAlias,
|
|
4540
|
+
targetAlias: item.targetAlias,
|
|
4541
|
+
expectsReply: false,
|
|
4542
|
+
nextSequence: 1,
|
|
4543
|
+
lastActivityAt: this.now().toISOString(),
|
|
4544
|
+
...(item.pair === true ? { pair: true } : {}),
|
|
4545
|
+
};
|
|
4546
|
+
const context = {
|
|
4547
|
+
conversationId,
|
|
4548
|
+
expectsReply: false,
|
|
4549
|
+
sequence: 0,
|
|
4550
|
+
targetBindingKey: bindingKey(binding),
|
|
4551
|
+
authorization: "selected_route",
|
|
4552
|
+
targetAlias: item.targetAlias,
|
|
4553
|
+
deadlineAt: item.deadlineAt,
|
|
4554
|
+
recovered: true,
|
|
4555
|
+
};
|
|
4556
|
+
this.conversations.set(conversationId, conversation);
|
|
4557
|
+
this.messageContexts.set(item.messageId, context);
|
|
4558
|
+
this.armDeliveryTracker({
|
|
4559
|
+
messageId: item.messageId,
|
|
4560
|
+
conversationId,
|
|
4561
|
+
targetAlias: item.targetAlias,
|
|
4562
|
+
enqueuedAt: Date.parse(item.enqueuedAt),
|
|
4563
|
+
deadlineAt: item.deadlineAt,
|
|
4564
|
+
});
|
|
4565
|
+
return context;
|
|
4566
|
+
}
|
|
3926
4567
|
async dispatchOne(targetAlias) {
|
|
3927
4568
|
if (!this.running ||
|
|
3928
4569
|
this.closing ||
|
|
@@ -3949,8 +4590,19 @@ export class GatewayService {
|
|
|
3949
4590
|
}
|
|
3950
4591
|
if (item === undefined)
|
|
3951
4592
|
return;
|
|
3952
|
-
const context = this.messageContexts.get(item.messageId);
|
|
3953
4593
|
const selectedBinding = this.routeBindings.get(targetAlias);
|
|
4594
|
+
let context = this.messageContexts.get(item.messageId);
|
|
4595
|
+
if (context === undefined && selectedBinding === undefined) {
|
|
4596
|
+
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4597
|
+
if (result.status === "settled") {
|
|
4598
|
+
await this.applyTerminalSettlementLocked(result.settlement);
|
|
4599
|
+
}
|
|
4600
|
+
await this.changed();
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
if (context === undefined && selectedBinding !== undefined) {
|
|
4604
|
+
context = this.restoreQueuedMessageContextLocked(item, selectedBinding);
|
|
4605
|
+
}
|
|
3954
4606
|
const binding = context?.nativeReplyBinding ?? selectedBinding;
|
|
3955
4607
|
if (context === undefined) {
|
|
3956
4608
|
const result = await this.store.settleMessage({
|
|
@@ -3974,8 +4626,8 @@ export class GatewayService {
|
|
|
3974
4626
|
}
|
|
3975
4627
|
if (context.authorization === "selected_route") {
|
|
3976
4628
|
const routeState = this.routeStates.get(targetAlias);
|
|
3977
|
-
const dispatchable =
|
|
3978
|
-
|
|
4629
|
+
const dispatchable = binding.provider === "claude" ||
|
|
4630
|
+
routeState === "idle" ||
|
|
3979
4631
|
(binding.provider === "codex" &&
|
|
3980
4632
|
item.steer === true &&
|
|
3981
4633
|
routeState === "busy");
|
|
@@ -3985,6 +4637,14 @@ export class GatewayService {
|
|
|
3985
4637
|
inspection?.state === "stale" ||
|
|
3986
4638
|
inspection?.safeErrorCode === "CODEX_ROUTE_STALE");
|
|
3987
4639
|
if (codexRouteStale) {
|
|
4640
|
+
if (context.recovered === true) {
|
|
4641
|
+
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4642
|
+
if (result.status === "settled") {
|
|
4643
|
+
await this.applyTerminalSettlementLocked(result.settlement);
|
|
4644
|
+
}
|
|
4645
|
+
await this.changed();
|
|
4646
|
+
return;
|
|
4647
|
+
}
|
|
3988
4648
|
await this.advanceDeliveryLocked(item.messageId, {
|
|
3989
4649
|
type: "route_terminated",
|
|
3990
4650
|
at: this.now().getTime(),
|
|
@@ -3995,8 +4655,8 @@ export class GatewayService {
|
|
|
3995
4655
|
this.scheduleDispatch(targetAlias);
|
|
3996
4656
|
return;
|
|
3997
4657
|
}
|
|
3998
|
-
const storedDispatchable =
|
|
3999
|
-
|
|
4658
|
+
const storedDispatchable = binding.provider === "claude" ||
|
|
4659
|
+
inspection?.state === "idle" ||
|
|
4000
4660
|
(binding.provider === "codex" &&
|
|
4001
4661
|
item.steer === true &&
|
|
4002
4662
|
inspection?.state === "busy");
|
|
@@ -4088,6 +4748,8 @@ export class GatewayService {
|
|
|
4088
4748
|
try {
|
|
4089
4749
|
result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
|
|
4090
4750
|
sourceAlias: item.sourceAlias,
|
|
4751
|
+
targetAlias: item.targetAlias,
|
|
4752
|
+
conversationId: context.conversationId,
|
|
4091
4753
|
binding,
|
|
4092
4754
|
authorization: context.authorization,
|
|
4093
4755
|
messageId: item.messageId,
|
|
@@ -4126,12 +4788,23 @@ export class GatewayService {
|
|
|
4126
4788
|
if (result.state === "deferred") {
|
|
4127
4789
|
const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
|
|
4128
4790
|
const now = this.now().getTime();
|
|
4791
|
+
const deferredSafeErrorCode = safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED");
|
|
4792
|
+
const retryDelayMs = binding.provider === "claude"
|
|
4793
|
+
? Math.min(500 * 2 ** (tracker.machine.dispatchAttempt - 1), 300_000)
|
|
4794
|
+
: 500;
|
|
4795
|
+
// Only Claude's backoff is clamped inside the deadline, so a long delay
|
|
4796
|
+
// still buys one last attempt. A Codex-bound retry keeps its exact
|
|
4797
|
+
// unclamped slice: a slice past the deadline must settle now, not wait.
|
|
4798
|
+
const retryAt = binding.provider === "claude"
|
|
4799
|
+
? Math.min(now + retryDelayMs, tracker.deadlineAt - 1)
|
|
4800
|
+
: now + retryDelayMs;
|
|
4129
4801
|
await this.advanceDeliveryLocked(item.messageId, {
|
|
4130
4802
|
type: "dispatch_clean_prewrite_failed",
|
|
4131
4803
|
at: now,
|
|
4132
|
-
retryAt
|
|
4133
|
-
safeErrorCode:
|
|
4804
|
+
retryAt,
|
|
4805
|
+
safeErrorCode: deferredSafeErrorCode,
|
|
4134
4806
|
});
|
|
4807
|
+
tracker.deliverySafeErrorCode = deferredSafeErrorCode;
|
|
4135
4808
|
const postDeferred = this.deliveryTrackers.get(item.messageId)?.machine;
|
|
4136
4809
|
if (postDeferred === undefined ||
|
|
4137
4810
|
isTerminalDeliveryMachine(postDeferred)) {
|
|
@@ -4150,13 +4823,13 @@ export class GatewayService {
|
|
|
4150
4823
|
await this.changed();
|
|
4151
4824
|
return;
|
|
4152
4825
|
}
|
|
4153
|
-
const requeued = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4826
|
+
const requeued = await this.store.requeueInFlightMessage(item.messageId, item.body, deferredSafeErrorCode);
|
|
4154
4827
|
if (this.activeDispatchByTarget.get(currentTargetAlias) === item.messageId) {
|
|
4155
4828
|
this.activeDispatchByTarget.delete(currentTargetAlias);
|
|
4156
4829
|
}
|
|
4157
4830
|
if (requeued.status === "requeued") {
|
|
4158
4831
|
if (context.authorization === "selected_route") {
|
|
4159
|
-
if (!steeringWhileBusy) {
|
|
4832
|
+
if (binding.provider === "codex" && !steeringWhileBusy) {
|
|
4160
4833
|
this.routeStates.set(currentTargetAlias, "idle");
|
|
4161
4834
|
await this.store.observeRoute({
|
|
4162
4835
|
binding,
|
|
@@ -4164,7 +4837,7 @@ export class GatewayService {
|
|
|
4164
4837
|
compatibility: "compatible",
|
|
4165
4838
|
});
|
|
4166
4839
|
}
|
|
4167
|
-
else {
|
|
4840
|
+
else if (binding.provider === "codex") {
|
|
4168
4841
|
this.scheduleHeldRedispatch(currentTargetAlias);
|
|
4169
4842
|
}
|
|
4170
4843
|
await this.setNativeCodexStatus(currentTargetAlias, "waiting");
|
|
@@ -4275,7 +4948,9 @@ export class GatewayService {
|
|
|
4275
4948
|
safeErrorCode: safeCode(event.safeErrorCode, "DISPATCH_OUTCOME_AMBIGUOUS"),
|
|
4276
4949
|
}
|
|
4277
4950
|
: event.state === "transport_written"
|
|
4278
|
-
?
|
|
4951
|
+
? target.provider === "claude"
|
|
4952
|
+
? { type: "provider_released", observedAt: receivedAt }
|
|
4953
|
+
: { type: "transport_written", observedAt: receivedAt }
|
|
4279
4954
|
: event.state === "held"
|
|
4280
4955
|
? { type: "provider_held", observedAt: receivedAt }
|
|
4281
4956
|
: event.state === "released" || event.state === "completed"
|
|
@@ -4367,7 +5042,7 @@ export class GatewayService {
|
|
|
4367
5042
|
conversation !== undefined) {
|
|
4368
5043
|
try {
|
|
4369
5044
|
if (this.nativeIngressByConversation.has(conversation.id)) {
|
|
4370
|
-
await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText,
|
|
5045
|
+
await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, false);
|
|
4371
5046
|
}
|
|
4372
5047
|
else {
|
|
4373
5048
|
const sourceBinding = this.routeBindings.get(conversation.targetAlias);
|
|
@@ -4382,12 +5057,11 @@ export class GatewayService {
|
|
|
4382
5057
|
}));
|
|
4383
5058
|
if ((!sourceStillRoutable || !pairStillActive) &&
|
|
4384
5059
|
sourceBinding?.provider === "claude") {
|
|
4385
|
-
await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText
|
|
5060
|
+
await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText);
|
|
4386
5061
|
}
|
|
4387
5062
|
else {
|
|
4388
|
-
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false,
|
|
5063
|
+
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, {
|
|
4389
5064
|
existingConversationId: conversation.id,
|
|
4390
|
-
requestedHopCount: context.hopCount + 1,
|
|
4391
5065
|
exposeDeliveryToken: false,
|
|
4392
5066
|
});
|
|
4393
5067
|
}
|
|
@@ -4448,9 +5122,8 @@ export class GatewayService {
|
|
|
4448
5122
|
if (conversation === undefined)
|
|
4449
5123
|
return;
|
|
4450
5124
|
this.pendingClaudeReplies.delete(key);
|
|
4451
|
-
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false,
|
|
5125
|
+
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, {
|
|
4452
5126
|
existingConversationId: conversation.id,
|
|
4453
|
-
requestedHopCount: pending.hopCount + 1,
|
|
4454
5127
|
exposeDeliveryToken: false,
|
|
4455
5128
|
});
|
|
4456
5129
|
}
|
|
@@ -4493,7 +5166,6 @@ export class GatewayService {
|
|
|
4493
5166
|
body: event.text,
|
|
4494
5167
|
dedupeKey: `${conversationId}:0`,
|
|
4495
5168
|
conversationIdSuffix: conversationId.slice(-8),
|
|
4496
|
-
hopCount: 0,
|
|
4497
5169
|
deadlineAt,
|
|
4498
5170
|
...(steer ? { steer: true } : {}),
|
|
4499
5171
|
progressWatch: progressWatchActivity(event.text, conversationId, event.sourceAlias),
|
|
@@ -4511,16 +5183,13 @@ export class GatewayService {
|
|
|
4511
5183
|
targetAlias: event.targetAlias,
|
|
4512
5184
|
expectsReply: !steer,
|
|
4513
5185
|
nextSequence: 1,
|
|
4514
|
-
lastHopCount: 0,
|
|
4515
5186
|
lastActivityAt: this.now().toISOString(),
|
|
4516
5187
|
...(queued.pair === true ? { pair: true } : {}),
|
|
4517
5188
|
};
|
|
4518
5189
|
this.conversations.set(conversationId, conversation);
|
|
4519
5190
|
this.messageContexts.set(queued.messageId, {
|
|
4520
5191
|
conversationId,
|
|
4521
|
-
isReply: false,
|
|
4522
5192
|
expectsReply: !steer,
|
|
4523
|
-
hopCount: 0,
|
|
4524
5193
|
sequence: 0,
|
|
4525
5194
|
targetBindingKey: bindingKey(target),
|
|
4526
5195
|
authorization: "selected_route",
|
|
@@ -4586,6 +5255,401 @@ export class GatewayService {
|
|
|
4586
5255
|
}
|
|
4587
5256
|
}
|
|
4588
5257
|
}
|
|
5258
|
+
async onCodexEndpointRefresh(adapter, event, options = {}) {
|
|
5259
|
+
const transitionKey = endpointRefreshCallbackKey(event);
|
|
5260
|
+
if (this.endpointRefreshCallbackPoisoned &&
|
|
5261
|
+
this.activeEndpointRefreshCallbackKey !== transitionKey) {
|
|
5262
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "Conflicting Codex endpoint transitions require a clean broker restart.");
|
|
5263
|
+
}
|
|
5264
|
+
const ownsActiveTransition = this.activeEndpointRefreshCallbackKey === undefined;
|
|
5265
|
+
if (!ownsActiveTransition &&
|
|
5266
|
+
this.activeEndpointRefreshCallbackKey !== transitionKey) {
|
|
5267
|
+
this.poisonEndpointRefreshCallbacks({
|
|
5268
|
+
type: "endpoint_refresh",
|
|
5269
|
+
adapter,
|
|
5270
|
+
value: event,
|
|
5271
|
+
});
|
|
5272
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "Conflicting Codex endpoint transitions require a clean broker restart.");
|
|
5273
|
+
}
|
|
5274
|
+
if (ownsActiveTransition) {
|
|
5275
|
+
this.activeEndpointRefreshCallback = {
|
|
5276
|
+
type: "endpoint_refresh",
|
|
5277
|
+
adapter,
|
|
5278
|
+
value: event,
|
|
5279
|
+
};
|
|
5280
|
+
this.activeEndpointRefreshCallbackKey = transitionKey;
|
|
5281
|
+
}
|
|
5282
|
+
let succeeded = false;
|
|
5283
|
+
let activationDeferred = false;
|
|
5284
|
+
try {
|
|
5285
|
+
activationDeferred = await this.applyCodexEndpointRefresh(adapter, event, transitionKey, options);
|
|
5286
|
+
succeeded = true;
|
|
5287
|
+
return activationDeferred;
|
|
5288
|
+
}
|
|
5289
|
+
finally {
|
|
5290
|
+
if (ownsActiveTransition &&
|
|
5291
|
+
this.activeEndpointRefreshCallbackKey === transitionKey) {
|
|
5292
|
+
this.activeEndpointRefreshCallback = undefined;
|
|
5293
|
+
this.activeEndpointRefreshCallbackKey = undefined;
|
|
5294
|
+
const requestedRetry = this.endpointRefreshRetryRequested;
|
|
5295
|
+
this.endpointRefreshRetryRequested = undefined;
|
|
5296
|
+
if ((!succeeded || activationDeferred) &&
|
|
5297
|
+
!this.endpointRefreshCallbackPoisoned &&
|
|
5298
|
+
requestedRetry !== undefined) {
|
|
5299
|
+
this.endpointRefreshCallback = requestedRetry;
|
|
5300
|
+
this.enqueueCallbackWorkerOnly();
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
async applyCodexEndpointRefresh(adapter, event, transitionKey, options) {
|
|
5306
|
+
if (this.closing ||
|
|
5307
|
+
(event.outcome !== "compatible" && event.outcome !== "incompatible") ||
|
|
5308
|
+
!isCompatibilityAttestation(event.attestation) ||
|
|
5309
|
+
event.attestation.surface !== "codex" ||
|
|
5310
|
+
event.previous.provider !== "codex" ||
|
|
5311
|
+
!PRIVATE_HANDLE.test(event.previous.endpointGeneration) ||
|
|
5312
|
+
adapter.identity.provider !== "codex" ||
|
|
5313
|
+
adapter.identity.hostId !== event.previous.hostId ||
|
|
5314
|
+
!this.config.allowedHosts.includes(event.previous.hostId)) {
|
|
5315
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The Codex endpoint transition failed closed runtime validation.");
|
|
5316
|
+
}
|
|
5317
|
+
if (event.outcome === "compatible") {
|
|
5318
|
+
const routeHandles = Array.isArray(event.routes)
|
|
5319
|
+
? event.routes.map((route) => route.routeHandle)
|
|
5320
|
+
: [];
|
|
5321
|
+
if (event.current.provider !== "codex" ||
|
|
5322
|
+
event.current.hostId !== event.previous.hostId ||
|
|
5323
|
+
!PRIVATE_HANDLE.test(event.current.endpointGeneration) ||
|
|
5324
|
+
event.current.endpointGeneration ===
|
|
5325
|
+
event.previous.endpointGeneration ||
|
|
5326
|
+
adapter.identity.endpointGeneration !==
|
|
5327
|
+
event.current.endpointGeneration ||
|
|
5328
|
+
event.attestation.version !== adapter.protocolVersion ||
|
|
5329
|
+
event.attestation.tier === "incompatible" ||
|
|
5330
|
+
!Array.isArray(event.routes) ||
|
|
5331
|
+
event.routes.length > this.config.limits.maxRoutes ||
|
|
5332
|
+
new Set(routeHandles).size !== routeHandles.length ||
|
|
5333
|
+
event.routes.some((route) => !PRIVATE_HANDLE.test(route.routeHandle) ||
|
|
5334
|
+
!["idle", "busy", "awaiting_approval"].includes(route.state))) {
|
|
5335
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The refreshed Codex provider identity changed before activation.");
|
|
5336
|
+
}
|
|
5337
|
+
if (this.activatedEndpointRefreshes.get(event.current.hostId) ===
|
|
5338
|
+
transitionKey) {
|
|
5339
|
+
return false;
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
else {
|
|
5343
|
+
if (event.candidate.provider !== "codex" ||
|
|
5344
|
+
event.candidate.hostId !== event.previous.hostId ||
|
|
5345
|
+
!PRIVATE_HANDLE.test(event.candidate.endpointGeneration) ||
|
|
5346
|
+
event.candidate.endpointGeneration ===
|
|
5347
|
+
event.previous.endpointGeneration ||
|
|
5348
|
+
adapter.identity.endpointGeneration !==
|
|
5349
|
+
event.previous.endpointGeneration ||
|
|
5350
|
+
event.attestation.tier !== "incompatible") {
|
|
5351
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The incompatible Codex candidate no longer matches the active endpoint.");
|
|
5352
|
+
}
|
|
5353
|
+
if (this.handledIncompatibleEndpointRefreshes.get(event.previous.hostId) === transitionKey) {
|
|
5354
|
+
return false;
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
let inventory = await this.store.inspectPrivateCodexRoutes();
|
|
5358
|
+
const previousRoutes = inventory.filter((route) => route.binding.hostId === event.previous.hostId &&
|
|
5359
|
+
route.binding.endpointGeneration === event.previous.endpointGeneration);
|
|
5360
|
+
const aliases = previousRoutes.map((route) => route.alias);
|
|
5361
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
5362
|
+
const inFlightSettlements = aliases.length === 0
|
|
5363
|
+
? []
|
|
5364
|
+
: await this.planRoutesInFlightSettlementsLocked(aliases, {
|
|
5365
|
+
unwrittenOutcome: "failed",
|
|
5366
|
+
safeErrorCode: "ENDPOINT_GENERATION_CHANGED",
|
|
5367
|
+
});
|
|
5368
|
+
let settlements = [];
|
|
5369
|
+
try {
|
|
5370
|
+
settlements = await this.store.markConnectorOffline(event.previous, "ENDPOINT_GENERATION_CHANGED", inFlightSettlements, { preserveQueued: true });
|
|
5371
|
+
}
|
|
5372
|
+
catch (error) {
|
|
5373
|
+
const alreadyInvalidatedCompatibleTransition = event.outcome === "compatible" &&
|
|
5374
|
+
error instanceof BridgeError &&
|
|
5375
|
+
error.code === "CONNECTOR_NOT_FOUND" &&
|
|
5376
|
+
previousRoutes.every((route) => route.state === "stale" && route.compatibility === "expired");
|
|
5377
|
+
if (!alreadyInvalidatedCompatibleTransition)
|
|
5378
|
+
throw error;
|
|
5379
|
+
}
|
|
5380
|
+
for (const settlement of settlements) {
|
|
5381
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
5382
|
+
}
|
|
5383
|
+
for (const route of previousRoutes) {
|
|
5384
|
+
this.routeStates.set(route.alias, "stale");
|
|
5385
|
+
}
|
|
5386
|
+
await this.store.recordCompatibilityAttestation(event.attestation);
|
|
5387
|
+
if (event.outcome === "incompatible") {
|
|
5388
|
+
await this.requireEndpointRefreshUnconflicted();
|
|
5389
|
+
this.activeCodexIncompatibility = structuredClone(event.attestation);
|
|
5390
|
+
this.addRuntimeAlert(event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE", "error", { provider: "codex", host: event.previous.hostId });
|
|
5391
|
+
this.handledIncompatibleEndpointRefreshes.set(event.previous.hostId, transitionKey);
|
|
5392
|
+
await this.changed();
|
|
5393
|
+
return false;
|
|
5394
|
+
}
|
|
5395
|
+
await this.store.observeConnector({
|
|
5396
|
+
identity: event.current,
|
|
5397
|
+
health: "healthy",
|
|
5398
|
+
compatibility: "compatible",
|
|
5399
|
+
protocol: adapter.protocol,
|
|
5400
|
+
protocolVersion: adapter.protocolVersion,
|
|
5401
|
+
});
|
|
5402
|
+
const refreshedByHandle = new Map(event.routes.map((route) => [route.routeHandle, route.state]));
|
|
5403
|
+
const pendingReanchors = previousRoutes.flatMap((route) => {
|
|
5404
|
+
const state = refreshedByHandle.get(route.binding.routeHandle);
|
|
5405
|
+
return state === undefined
|
|
5406
|
+
? []
|
|
5407
|
+
: [
|
|
5408
|
+
{
|
|
5409
|
+
alias: route.alias,
|
|
5410
|
+
threadId: route.binding.routeHandle,
|
|
5411
|
+
ownerLease: route.binding.ownerLease,
|
|
5412
|
+
state,
|
|
5413
|
+
},
|
|
5414
|
+
];
|
|
5415
|
+
});
|
|
5416
|
+
if (pendingReanchors.length > 0) {
|
|
5417
|
+
try {
|
|
5418
|
+
await this.store.reanchorCodexRoutes({
|
|
5419
|
+
oldEndpoint: event.previous,
|
|
5420
|
+
newEndpoint: event.current,
|
|
5421
|
+
routes: pendingReanchors,
|
|
5422
|
+
});
|
|
5423
|
+
this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
|
|
5424
|
+
}
|
|
5425
|
+
catch (error) {
|
|
5426
|
+
// Persistence may commit and then report an unknown outcome. Because
|
|
5427
|
+
// service/store mutation is serialized, exact G2 ownership for every
|
|
5428
|
+
// requested G1 alias proves this transition durably applied and gives
|
|
5429
|
+
// a later identical retry authority to restore a fail-closed G2 route.
|
|
5430
|
+
const after = await this.store.inspectPrivateCodexRoutes();
|
|
5431
|
+
const committedExactly = pendingReanchors.every((expected) => {
|
|
5432
|
+
const matches = after.filter((route) => route.alias === expected.alias &&
|
|
5433
|
+
route.binding.provider === "codex" &&
|
|
5434
|
+
route.binding.hostId === event.current.hostId &&
|
|
5435
|
+
route.binding.endpointGeneration ===
|
|
5436
|
+
event.current.endpointGeneration &&
|
|
5437
|
+
route.binding.routeHandle === expected.threadId &&
|
|
5438
|
+
route.binding.ownerLease === expected.ownerLease &&
|
|
5439
|
+
route.enabled &&
|
|
5440
|
+
route.compatibility === "compatible");
|
|
5441
|
+
return matches.length === 1;
|
|
5442
|
+
});
|
|
5443
|
+
if (committedExactly) {
|
|
5444
|
+
this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
|
|
5445
|
+
try {
|
|
5446
|
+
await this.store.markConnectorOffline(event.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
|
|
5447
|
+
for (const route of pendingReanchors) {
|
|
5448
|
+
this.routeStates.set(route.alias, "stale");
|
|
5449
|
+
}
|
|
5450
|
+
await this.changed();
|
|
5451
|
+
}
|
|
5452
|
+
catch {
|
|
5453
|
+
this.dashboardHealthy = false;
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
throw error;
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
// A retry may arrive after the store commit but before native status or
|
|
5460
|
+
// provider activation. Re-read exact durable authority and accept only one
|
|
5461
|
+
// unique current-generation owner for every claimed task.
|
|
5462
|
+
inventory = await this.store.inspectPrivateCodexRoutes();
|
|
5463
|
+
if (this.durablyAppliedEndpointRefreshes.get(event.current.hostId) ===
|
|
5464
|
+
transitionKey) {
|
|
5465
|
+
const observedStates = new Map(event.routes.map((route) => [route.routeHandle, route.state]));
|
|
5466
|
+
for (const route of inventory) {
|
|
5467
|
+
const state = observedStates.get(route.binding.routeHandle);
|
|
5468
|
+
if (state === undefined ||
|
|
5469
|
+
route.binding.hostId !== event.current.hostId ||
|
|
5470
|
+
route.binding.endpointGeneration !==
|
|
5471
|
+
event.current.endpointGeneration ||
|
|
5472
|
+
route.state !== "stale" ||
|
|
5473
|
+
route.compatibility !== "expired") {
|
|
5474
|
+
continue;
|
|
5475
|
+
}
|
|
5476
|
+
await this.registerOrRebind(route.alias, route.binding, "endpoint_reobserved", state, route.binding.ownerLease);
|
|
5477
|
+
}
|
|
5478
|
+
inventory = await this.store.inspectPrivateCodexRoutes();
|
|
5479
|
+
}
|
|
5480
|
+
const previousRouteHandles = new Set(previousRoutes.map((route) => route.binding.routeHandle));
|
|
5481
|
+
const activatedRoutes = event.routes.flatMap((observed) => {
|
|
5482
|
+
const matches = inventory.filter((route) => route.binding.provider === "codex" &&
|
|
5483
|
+
route.binding.hostId === event.current.hostId &&
|
|
5484
|
+
route.binding.endpointGeneration ===
|
|
5485
|
+
event.current.endpointGeneration &&
|
|
5486
|
+
route.binding.routeHandle === observed.routeHandle &&
|
|
5487
|
+
route.enabled &&
|
|
5488
|
+
route.compatibility === "compatible");
|
|
5489
|
+
if (matches.length > 1) {
|
|
5490
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "A refreshed Codex task has multiple durable owners.");
|
|
5491
|
+
}
|
|
5492
|
+
if (matches.length === 0) {
|
|
5493
|
+
// A selector-triggered refresh can report the freshly selected task
|
|
5494
|
+
// before registerRoute installs that new alias. It has no prior G1
|
|
5495
|
+
// owner to re-anchor, so activation covers only already-durable tasks;
|
|
5496
|
+
// the caller installs this one immediately after the provider ack.
|
|
5497
|
+
if (!previousRouteHandles.has(observed.routeHandle))
|
|
5498
|
+
return [];
|
|
5499
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "A previously durable Codex task was not re-anchored exactly.");
|
|
5500
|
+
}
|
|
5501
|
+
const match = matches[0];
|
|
5502
|
+
if (match === undefined)
|
|
5503
|
+
return [];
|
|
5504
|
+
return {
|
|
5505
|
+
alias: match.alias,
|
|
5506
|
+
threadId: observed.routeHandle,
|
|
5507
|
+
ownerLease: match.binding.ownerLease,
|
|
5508
|
+
state: observed.state,
|
|
5509
|
+
};
|
|
5510
|
+
});
|
|
5511
|
+
const pendingSelector = this.pendingSelectorEndpointActivations.get(event.current.hostId);
|
|
5512
|
+
const requiredRoute = options.deferActivation === true
|
|
5513
|
+
? undefined
|
|
5514
|
+
: options.requiredRoute ??
|
|
5515
|
+
(pendingSelector?.transitionKey === transitionKey
|
|
5516
|
+
? pendingSelector.route
|
|
5517
|
+
: undefined);
|
|
5518
|
+
if (requiredRoute !== undefined &&
|
|
5519
|
+
!activatedRoutes.some((route) => route.alias === requiredRoute.alias &&
|
|
5520
|
+
route.threadId === requiredRoute.threadId)) {
|
|
5521
|
+
const matches = inventory.filter((route) => route.alias === requiredRoute.alias &&
|
|
5522
|
+
route.binding.provider === "codex" &&
|
|
5523
|
+
route.binding.hostId === event.current.hostId &&
|
|
5524
|
+
route.binding.endpointGeneration ===
|
|
5525
|
+
event.current.endpointGeneration &&
|
|
5526
|
+
route.binding.routeHandle === requiredRoute.threadId &&
|
|
5527
|
+
route.enabled &&
|
|
5528
|
+
route.compatibility === "compatible");
|
|
5529
|
+
const match = matches[0];
|
|
5530
|
+
if (matches.length !== 1 || match === undefined) {
|
|
5531
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The registering Codex task was not durably installed exactly.");
|
|
5532
|
+
}
|
|
5533
|
+
activatedRoutes.push({
|
|
5534
|
+
alias: requiredRoute.alias,
|
|
5535
|
+
threadId: requiredRoute.threadId,
|
|
5536
|
+
ownerLease: match.binding.ownerLease,
|
|
5537
|
+
state: requiredRoute.state,
|
|
5538
|
+
});
|
|
5539
|
+
}
|
|
5540
|
+
if (options.deferActivation === true) {
|
|
5541
|
+
const deferredRoute = options.deferredRoute;
|
|
5542
|
+
if (deferredRoute === undefined) {
|
|
5543
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "A deferred selector transition requires one exact registration.");
|
|
5544
|
+
}
|
|
5545
|
+
const existing = this.pendingSelectorEndpointActivations.get(event.current.hostId);
|
|
5546
|
+
if (existing !== undefined &&
|
|
5547
|
+
(existing.transitionKey !== transitionKey ||
|
|
5548
|
+
existing.route.alias !== deferredRoute.alias ||
|
|
5549
|
+
existing.route.threadId !== deferredRoute.threadId)) {
|
|
5550
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_CONFLICT", "A different selector transition is already awaiting native activation.");
|
|
5551
|
+
}
|
|
5552
|
+
this.pendingSelectorEndpointActivations.set(event.current.hostId, {
|
|
5553
|
+
transitionKey,
|
|
5554
|
+
route: { ...deferredRoute },
|
|
5555
|
+
});
|
|
5556
|
+
return true;
|
|
5557
|
+
}
|
|
5558
|
+
const requireNativeContinuity = (route) => {
|
|
5559
|
+
const registration = this.codexRegistrationLocks.get(route.alias);
|
|
5560
|
+
if (registration === undefined ||
|
|
5561
|
+
registration.hostId !== event.current.hostId ||
|
|
5562
|
+
registration.threadId !== route.threadId ||
|
|
5563
|
+
!isCodexRegistrationGeneration(registration.generation)) {
|
|
5564
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_LISTENER_MISSING", "The refreshed Codex route has no exact live native listener authority.");
|
|
5565
|
+
}
|
|
5566
|
+
const nativeGeneration = this.requireCurrentNativeCodexGeneration(this.adapter("claude", event.current.hostId), route.alias);
|
|
5567
|
+
if (nativeGeneration !== registration.generation) {
|
|
5568
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_LISTENER_MISSING", "The refreshed Codex route's native listener generation changed before activation.");
|
|
5569
|
+
}
|
|
5570
|
+
};
|
|
5571
|
+
try {
|
|
5572
|
+
for (const route of activatedRoutes) {
|
|
5573
|
+
const binding = {
|
|
5574
|
+
...event.current,
|
|
5575
|
+
routeHandle: route.threadId,
|
|
5576
|
+
ownerLease: route.ownerLease,
|
|
5577
|
+
};
|
|
5578
|
+
this.rememberBinding(route.alias, binding, route.state);
|
|
5579
|
+
}
|
|
5580
|
+
for (const route of activatedRoutes) {
|
|
5581
|
+
requireNativeContinuity(route);
|
|
5582
|
+
await this.setNativeCodexStatus(route.alias, route.state === "idle"
|
|
5583
|
+
? "idle"
|
|
5584
|
+
: route.state === "awaiting_approval"
|
|
5585
|
+
? "waiting"
|
|
5586
|
+
: "busy", true);
|
|
5587
|
+
requireNativeContinuity(route);
|
|
5588
|
+
}
|
|
5589
|
+
await this.requireEndpointRefreshUnconflicted();
|
|
5590
|
+
for (const route of activatedRoutes) {
|
|
5591
|
+
const activityKey = `${transitionKey}\0${route.alias}`;
|
|
5592
|
+
if (!this.endpointRefreshActivityKeys.has(activityKey)) {
|
|
5593
|
+
this.endpointRefreshActivityKeys.add(activityKey);
|
|
5594
|
+
while (this.endpointRefreshActivityKeys.size > RUNTIME_ACTIVITY_CAPACITY) {
|
|
5595
|
+
const oldest = this.endpointRefreshActivityKeys.values().next().value;
|
|
5596
|
+
if (oldest === undefined)
|
|
5597
|
+
break;
|
|
5598
|
+
this.endpointRefreshActivityKeys.delete(oldest);
|
|
5599
|
+
}
|
|
5600
|
+
this.activitySequence += 1;
|
|
5601
|
+
this.activityEvents.push({
|
|
5602
|
+
sequence: this.activitySequence,
|
|
5603
|
+
timestamp: this.now().toISOString(),
|
|
5604
|
+
kind: "endpoint",
|
|
5605
|
+
action: "endpoint_refreshed",
|
|
5606
|
+
outcome: "accepted",
|
|
5607
|
+
aliases: [route.alias],
|
|
5608
|
+
operatorAction: false,
|
|
5609
|
+
});
|
|
5610
|
+
while (this.activityEvents.length > RUNTIME_ACTIVITY_CAPACITY) {
|
|
5611
|
+
this.activityEvents.shift();
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
if (route.state === "idle")
|
|
5615
|
+
this.scheduleDispatch(route.alias);
|
|
5616
|
+
}
|
|
5617
|
+
await this.changed();
|
|
5618
|
+
await this.requireEndpointRefreshUnconflicted();
|
|
5619
|
+
for (const route of activatedRoutes) {
|
|
5620
|
+
requireNativeContinuity(route);
|
|
5621
|
+
}
|
|
5622
|
+
// Provider writes remain closed until every durable and native transition
|
|
5623
|
+
// above is complete. This acknowledgement is deliberately the final
|
|
5624
|
+
// fallible activation step.
|
|
5625
|
+
const accept = adapter.acceptCompatibilityAttestation;
|
|
5626
|
+
if (accept === undefined) {
|
|
5627
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_ACTIVATION_UNAVAILABLE", "The Codex provider cannot acknowledge the validated endpoint transition.");
|
|
5628
|
+
}
|
|
5629
|
+
accept.call(adapter, event.attestation);
|
|
5630
|
+
}
|
|
5631
|
+
catch (error) {
|
|
5632
|
+
try {
|
|
5633
|
+
await this.store.markConnectorOffline(event.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
|
|
5634
|
+
for (const route of activatedRoutes) {
|
|
5635
|
+
this.routeStates.set(route.alias, "stale");
|
|
5636
|
+
}
|
|
5637
|
+
await this.changed();
|
|
5638
|
+
}
|
|
5639
|
+
catch {
|
|
5640
|
+
this.dashboardHealthy = false;
|
|
5641
|
+
}
|
|
5642
|
+
throw error;
|
|
5643
|
+
}
|
|
5644
|
+
this.activatedEndpointRefreshes.set(event.current.hostId, transitionKey);
|
|
5645
|
+
if (this.pendingSelectorEndpointActivations.get(event.current.hostId)
|
|
5646
|
+
?.transitionKey === transitionKey) {
|
|
5647
|
+
this.pendingSelectorEndpointActivations.delete(event.current.hostId);
|
|
5648
|
+
}
|
|
5649
|
+
this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
|
|
5650
|
+
this.activeCodexIncompatibility = undefined;
|
|
5651
|
+
return false;
|
|
5652
|
+
}
|
|
4589
5653
|
async onRouteState(source, event) {
|
|
4590
5654
|
if (this.closing)
|
|
4591
5655
|
return;
|
|
@@ -4655,15 +5719,27 @@ export class GatewayService {
|
|
|
4655
5719
|
this.scheduleDispatch(alias);
|
|
4656
5720
|
}
|
|
4657
5721
|
}
|
|
4658
|
-
async setNativeCodexStatus(alias, status) {
|
|
5722
|
+
async setNativeCodexStatus(alias, status, requireSuccess = false) {
|
|
4659
5723
|
const binding = this.routeBindings.get(alias);
|
|
4660
5724
|
if (binding?.provider !== "codex")
|
|
4661
5725
|
return;
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
5726
|
+
const adapter = this.adapter("claude", binding.hostId);
|
|
5727
|
+
const update = adapter.updateNativeCodexPeerStatus;
|
|
5728
|
+
if (update === undefined) {
|
|
5729
|
+
if (requireSuccess) {
|
|
5730
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_STATUS_FAILED", "The native listener does not support refreshed Codex status activation.", true);
|
|
5731
|
+
}
|
|
5732
|
+
return;
|
|
5733
|
+
}
|
|
5734
|
+
try {
|
|
5735
|
+
await update.call(adapter, alias, status);
|
|
5736
|
+
}
|
|
5737
|
+
catch (error) {
|
|
4665
5738
|
this.dashboardHealthy = false;
|
|
4666
|
-
|
|
5739
|
+
if (requireSuccess) {
|
|
5740
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_STATUS_FAILED", "The refreshed Codex route could not be activated in the native listener.", true);
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
4667
5743
|
}
|
|
4668
5744
|
rejectDetachedNativeReceipt(hostId, receiptHandle, diagnosticCode) {
|
|
4669
5745
|
const work = this.settleDetachedNativeReceipt(hostId, receiptHandle, diagnosticCode).finally(() => {
|
|
@@ -4748,15 +5824,29 @@ export class GatewayService {
|
|
|
4748
5824
|
async publicSnapshotLocked() {
|
|
4749
5825
|
const base = await this.store.publicSnapshot();
|
|
4750
5826
|
const compatibilityChecks = [];
|
|
4751
|
-
for (const
|
|
4752
|
-
|
|
4753
|
-
|
|
5827
|
+
for (const surface of compatibilitySurfaces) {
|
|
5828
|
+
if (surface === "codex" && this.activeCodexIncompatibility !== undefined) {
|
|
5829
|
+
compatibilityChecks.push(structuredClone(this.activeCodexIncompatibility));
|
|
4754
5830
|
continue;
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
5831
|
+
}
|
|
5832
|
+
const currentVersions = [
|
|
5833
|
+
...new Set(this.adapters.flatMap((adapter) => {
|
|
5834
|
+
const observation = adapter.compatibilitySurface?.();
|
|
5835
|
+
return observation?.surface === surface ? [observation.version] : [];
|
|
5836
|
+
})),
|
|
5837
|
+
].sort((left, right) => left.localeCompare(right));
|
|
5838
|
+
const currentAttestations = (await Promise.all(currentVersions.map(async (version) => await this.store.inspectCompatibilityAttestation(surface, version))))
|
|
5839
|
+
.filter((attestation) => attestation !== undefined)
|
|
5840
|
+
.sort((left, right) => {
|
|
5841
|
+
const checkedAt = right.checkedAt.localeCompare(left.checkedAt);
|
|
5842
|
+
if (checkedAt !== 0)
|
|
5843
|
+
return checkedAt;
|
|
5844
|
+
return left.version.localeCompare(right.version);
|
|
5845
|
+
});
|
|
5846
|
+
const selected = currentAttestations[0];
|
|
5847
|
+
if (selected !== undefined)
|
|
5848
|
+
compatibilityChecks.push(selected);
|
|
4758
5849
|
}
|
|
4759
|
-
compatibilityChecks.sort((left, right) => left.surface.localeCompare(right.surface));
|
|
4760
5850
|
const peers = this.availablePeers.map((peer) => ({ ...peer }));
|
|
4761
5851
|
if (!arePublicAvailablePeerSnapshots(peers)) {
|
|
4762
5852
|
throw new BridgeError("INVALID_TRANSIENT_INVENTORY", "The transient peer inventory is unsafe.");
|
|
@@ -4782,21 +5872,6 @@ export class GatewayService {
|
|
|
4782
5872
|
},
|
|
4783
5873
|
];
|
|
4784
5874
|
});
|
|
4785
|
-
const certificationAlerts = compatibilityChecks.flatMap((attestation) => {
|
|
4786
|
-
const certification = attestation.certification;
|
|
4787
|
-
if (certification?.outcome !== "fail")
|
|
4788
|
-
return [];
|
|
4789
|
-
const adapter = this.adapters.find((candidate) => candidate.identity.provider === attestation.surface);
|
|
4790
|
-
return [
|
|
4791
|
-
{
|
|
4792
|
-
code: "COMPATIBILITY_CERTIFICATION_FAILED",
|
|
4793
|
-
severity: "error",
|
|
4794
|
-
timestamp: certification.certifiedAt,
|
|
4795
|
-
provider: attestation.surface,
|
|
4796
|
-
host: adapter?.identity.hostId ?? "this-mac",
|
|
4797
|
-
},
|
|
4798
|
-
];
|
|
4799
|
-
});
|
|
4800
5875
|
return projectGatewayPublicSnapshot({
|
|
4801
5876
|
...base,
|
|
4802
5877
|
...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
|
|
@@ -4812,7 +5887,6 @@ export class GatewayService {
|
|
|
4812
5887
|
alerts: [
|
|
4813
5888
|
...base.alerts,
|
|
4814
5889
|
...this.runtimeAlerts.map((alert) => ({ ...alert })),
|
|
4815
|
-
...certificationAlerts,
|
|
4816
5890
|
...stalledAlerts,
|
|
4817
5891
|
],
|
|
4818
5892
|
});
|