agent-embassy 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +9 -9
- package/README.zh-CN.md +9 -9
- package/SECURITY.md +34 -77
- package/dist/src/gateway/acp-client.d.ts +117 -0
- package/dist/src/gateway/acp-client.js +382 -0
- package/dist/src/gateway/acp-client.js.map +1 -0
- package/dist/src/gateway/acp-provider.d.ts +65 -0
- package/dist/src/gateway/acp-provider.js +163 -0
- package/dist/src/gateway/acp-provider.js.map +1 -0
- package/dist/src/gateway/claude-helper-protocol.d.ts +3 -1
- package/dist/src/gateway/claude-helper-protocol.js +15 -5
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +3 -1
- package/dist/src/gateway/claude-helper-supervisor.js +16 -2
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-helper.js +4 -3
- package/dist/src/gateway/claude-helper.js.map +1 -1
- package/dist/src/gateway/claude-peer.d.ts +1 -1
- package/dist/src/gateway/claude-peer.js +11 -13
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +0 -20
- package/dist/src/gateway/claude-runtime.js +6 -127
- 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 +3 -1
- package/dist/src/gateway/cli-copy.en.js +5 -2
- 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 +3 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +5 -2
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +2 -2
- package/dist/src/gateway/cli.js +73 -6
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +5 -74
- package/dist/src/gateway/codex-app-server.js +9 -599
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-doctor.d.ts +33 -0
- package/dist/src/gateway/codex-doctor.js +148 -0
- package/dist/src/gateway/codex-doctor.js.map +1 -0
- package/dist/src/gateway/codex-local-transport.d.ts +13 -26
- package/dist/src/gateway/codex-local-transport.js +31 -76
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +6 -66
- package/dist/src/gateway/compatibility.js +14 -296
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/config.d.ts +9 -0
- package/dist/src/gateway/config.js +6 -2
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +16 -1
- package/dist/src/gateway/control.js +128 -59
- 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 +19 -44
- package/dist/src/gateway/dashboard-copy.en.js +42 -67
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +19 -44
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +19 -44
- package/dist/src/gateway/dashboard-copy.zh-CN.js +42 -67
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +41 -76
- package/dist/src/gateway/dashboard-model.js +167 -257
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.d.ts +0 -3
- package/dist/src/gateway/dashboard.js +77 -76
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/deepseek-detect.d.ts +7 -6
- package/dist/src/gateway/deepseek-detect.js +31 -115
- package/dist/src/gateway/deepseek-detect.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +175 -621
- package/dist/src/gateway/live-dashboard-command.js +2 -4
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +1 -2
- package/dist/src/gateway/live-dashboard-http.js +14 -15
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +1 -4
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +3 -2
- package/dist/src/gateway/provenance-envelope.js +44 -11
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +19 -85
- package/dist/src/gateway/providers.js +132 -835
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +8 -9
- package/dist/src/gateway/server.js +63 -99
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +48 -51
- package/dist/src/gateway/service.js +748 -540
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +20 -29
- package/dist/src/gateway/store.js +306 -838
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +63 -54
- package/dist/src/gateway/types.js +51 -91
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +11 -10
- package/docs/CONFIGURATION.zh-CN.md +10 -9
- package/docs/DASHBOARD.md +4 -38
- package/docs/DASHBOARD.zh-CN.md +4 -2
- package/docs/GATEWAY-ARCHITECTURE.md +61 -119
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +6 -6
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { BridgeError } from "../errors.js";
|
|
3
3
|
import { KeyedMutex } from "../mutex.js";
|
|
4
|
-
import {
|
|
5
|
-
import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, } from "./control.js";
|
|
4
|
+
import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, pairParamAliases, pairParamThreadAttestation, } from "./control.js";
|
|
6
5
|
import { publishGatewayDashboard } from "./dashboard.js";
|
|
7
6
|
import { createDeliveryMachine, isTerminalDeliveryMachine, projectDelivery, projectDeliveryWakeups, transitionDelivery, } from "./delivery-machine.js";
|
|
8
7
|
import { createCodexRegistrationSuccession, transitionCodexRegistrationSuccession, } from "./codex-registration-succession.js";
|
|
9
8
|
import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
|
|
10
9
|
import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
|
|
11
10
|
import { GatewayStore, } from "./store.js";
|
|
12
|
-
import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, arePublicAvailablePeerSnapshots,
|
|
11
|
+
import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, gatewayRegistrationIngressPrefixes, arePublicAvailablePeerSnapshots, directionId, parseDirection, projectGatewayPublicSnapshot, CONNECTOR_OBSERVATION_STALE_AFTER_MS, } from "./types.js";
|
|
13
12
|
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
14
13
|
const CLAUDE_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
15
14
|
const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
@@ -22,6 +21,8 @@ const NATIVE_HELD_THRESHOLD_MS = 1_000;
|
|
|
22
21
|
const RUNTIME_ACTIVITY_CAPACITY = 256;
|
|
23
22
|
const DELIVERY_SETTLEMENT_RETRY_MS = 250;
|
|
24
23
|
const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
|
|
24
|
+
const CLAUDE_DISCOVERY_REFRESH_MS = 30_000;
|
|
25
|
+
const CODEX_DOCTOR_REFRESH_MS = 30_000;
|
|
25
26
|
const BOOT_REACTIVATION_TRUST_FAILURE_CODES = new Set([
|
|
26
27
|
"HOME_INVALID",
|
|
27
28
|
"MANAGED_CODEX_INVALID",
|
|
@@ -77,39 +78,13 @@ function bindingKey(binding) {
|
|
|
77
78
|
].join("\0");
|
|
78
79
|
}
|
|
79
80
|
function endpointRefreshCallbackKey(event) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
name: probe.name,
|
|
88
|
-
outcome: probe.outcome,
|
|
89
|
-
...(probe.safeErrorCode === undefined
|
|
90
|
-
? {}
|
|
91
|
-
: { safeErrorCode: probe.safeErrorCode }),
|
|
92
|
-
})),
|
|
93
|
-
...(event.attestation.safeErrorCode === undefined
|
|
94
|
-
? {}
|
|
95
|
-
: { safeErrorCode: event.attestation.safeErrorCode }),
|
|
96
|
-
};
|
|
97
|
-
return event.outcome === "compatible"
|
|
98
|
-
? JSON.stringify({
|
|
99
|
-
outcome: event.outcome,
|
|
100
|
-
previous: event.previous,
|
|
101
|
-
current: event.current,
|
|
102
|
-
attestation,
|
|
103
|
-
routes: [...event.routes]
|
|
104
|
-
.map((route) => ({ ...route }))
|
|
105
|
-
.sort((left, right) => left.routeHandle.localeCompare(right.routeHandle)),
|
|
106
|
-
})
|
|
107
|
-
: JSON.stringify({
|
|
108
|
-
outcome: event.outcome,
|
|
109
|
-
previous: event.previous,
|
|
110
|
-
candidate: event.candidate,
|
|
111
|
-
attestation,
|
|
112
|
-
});
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
previous: event.previous,
|
|
83
|
+
current: event.current,
|
|
84
|
+
routes: [...event.routes]
|
|
85
|
+
.map((route) => ({ ...route }))
|
|
86
|
+
.sort((left, right) => left.routeHandle.localeCompare(right.routeHandle)),
|
|
87
|
+
});
|
|
113
88
|
}
|
|
114
89
|
function stableLease(provider, value) {
|
|
115
90
|
return `lease_${createHash("sha256").update(provider).update("\0").update(value).digest("base64url")}`;
|
|
@@ -149,13 +124,12 @@ export class GatewayService {
|
|
|
149
124
|
config;
|
|
150
125
|
store;
|
|
151
126
|
adapters;
|
|
152
|
-
compatibilityObservers;
|
|
153
|
-
compatibilitySurfaceDefinitions;
|
|
154
127
|
publishDashboard;
|
|
155
128
|
now;
|
|
156
129
|
timers;
|
|
157
130
|
nativePeerCwd;
|
|
158
131
|
successionGeneration;
|
|
132
|
+
codexDoctor;
|
|
159
133
|
mutex = new KeyedMutex();
|
|
160
134
|
routeBindings = new Map();
|
|
161
135
|
routeStates = new Map();
|
|
@@ -176,9 +150,6 @@ export class GatewayService {
|
|
|
176
150
|
deliveryTokens = new Map();
|
|
177
151
|
runtimeAlerts = [];
|
|
178
152
|
activityEvents = [];
|
|
179
|
-
/** Startup probe failures that must preflight cross-provider mutations. */
|
|
180
|
-
incompatibleProviderSurfaces = new Map();
|
|
181
|
-
activeCodexIncompatibility;
|
|
182
153
|
activitySequence = 0;
|
|
183
154
|
detachedReceiptWrites = new Set();
|
|
184
155
|
nativeIngressByConversation = new Map();
|
|
@@ -202,8 +173,6 @@ export class GatewayService {
|
|
|
202
173
|
durablyAppliedEndpointRefreshes = new Map();
|
|
203
174
|
/** Selector transitions staged until their exact registration owns a helper. */
|
|
204
175
|
pendingSelectorEndpointActivations = new Map();
|
|
205
|
-
/** Last incompatible transition already projected per allowed Codex host. */
|
|
206
|
-
handledIncompatibleEndpointRefreshes = new Map();
|
|
207
176
|
/** Process-local dedupe for automatic public activity rows across retries. */
|
|
208
177
|
endpointRefreshActivityKeys = new Set();
|
|
209
178
|
callbackWorker;
|
|
@@ -211,8 +180,17 @@ export class GatewayService {
|
|
|
211
180
|
callbackCapacity;
|
|
212
181
|
deliveryCallbackReserve;
|
|
213
182
|
deliveryTokenCapacity;
|
|
183
|
+
providerDispatchInFlightMessageIds = new Set();
|
|
184
|
+
connectorFreshnessDeadlineAt = new Map();
|
|
214
185
|
lifecycleTimer;
|
|
186
|
+
lifecyclePublishPending = false;
|
|
215
187
|
nextDashboardRefreshAt;
|
|
188
|
+
nextClaudeDiscoveryAt;
|
|
189
|
+
claudeDiscoveryScheduled = false;
|
|
190
|
+
claudeDiscoveryInFlight;
|
|
191
|
+
codexDoctorTimer;
|
|
192
|
+
codexDoctorInFlight;
|
|
193
|
+
codexDoctorResult;
|
|
216
194
|
nextProgressWatchAt;
|
|
217
195
|
control;
|
|
218
196
|
/** Coarse controller mutation revision exposed by `health`. */
|
|
@@ -247,9 +225,6 @@ export class GatewayService {
|
|
|
247
225
|
constructor(options) {
|
|
248
226
|
this.config = options.config;
|
|
249
227
|
this.adapters = [...(options.adapters ?? [])];
|
|
250
|
-
this.compatibilityObservers = [...(options.compatibilityObservers ?? [])];
|
|
251
|
-
this.compatibilitySurfaceDefinitions =
|
|
252
|
-
options.compatibilitySurfaceDefinitions ?? compatibilitySurfaceDefinitions;
|
|
253
228
|
this.publishDashboard = options.publishDashboard ?? publishGatewayDashboard;
|
|
254
229
|
this.now = options.now ?? (() => new Date());
|
|
255
230
|
this.store =
|
|
@@ -262,6 +237,7 @@ export class GatewayService {
|
|
|
262
237
|
this.successionGeneration =
|
|
263
238
|
options.successionGeneration ??
|
|
264
239
|
createCodexRegistrationGeneration;
|
|
240
|
+
this.codexDoctor = options.codexDoctor;
|
|
265
241
|
this.callbackCapacity = Math.max(64, options.config.limits.maxRoutes +
|
|
266
242
|
options.config.limits.maxInFlightMessages * 8);
|
|
267
243
|
// A callback flood must never crowd out write evidence. One selected route
|
|
@@ -297,28 +273,50 @@ export class GatewayService {
|
|
|
297
273
|
}
|
|
298
274
|
seen.add(key);
|
|
299
275
|
}
|
|
300
|
-
if (this.adapters.length > 0) {
|
|
301
|
-
await this.runAutomaticCompatibilityProbesLocked();
|
|
302
|
-
assertStartActive();
|
|
303
|
-
}
|
|
304
276
|
for (const adapter of this.adapters) {
|
|
305
277
|
const observation = await adapter.initialize(this.callbacksFor(adapter));
|
|
306
278
|
assertStartActive();
|
|
279
|
+
this.captureRegistryObservation(adapter, adapter.latestRegistryObservation?.());
|
|
307
280
|
await this.store.observeConnector({
|
|
308
281
|
identity: adapter.identity,
|
|
309
282
|
health: observation.health,
|
|
310
|
-
compatibility: observation.compatibility,
|
|
311
283
|
protocol: adapter.protocol,
|
|
312
284
|
protocolVersion: adapter.protocolVersion,
|
|
313
285
|
...(observation.safeErrorCode === undefined
|
|
314
286
|
? {}
|
|
315
287
|
: { safeErrorCode: safeCode(observation.safeErrorCode, "ADAPTER_DEGRADED") }),
|
|
316
288
|
});
|
|
289
|
+
this.recordConnectorFreshness(adapter.identity, observation.health);
|
|
290
|
+
if (observation.ownedRoute !== undefined) {
|
|
291
|
+
const route = observation.ownedRoute;
|
|
292
|
+
const prefix = gatewayRegistrationIngressPrefixes[adapter.identity.provider];
|
|
293
|
+
if (prefix === undefined || !route.alias.startsWith(prefix) ||
|
|
294
|
+
!route.alias.endsWith(`@${adapter.identity.hostId}`) ||
|
|
295
|
+
!PUBLIC_ALIAS.test(route.alias) || !PRIVATE_HANDLE.test(route.routeHandle) ||
|
|
296
|
+
route.state !== "idle") {
|
|
297
|
+
throw new BridgeError("GATEWAY_OWNED_ROUTE_INVALID", "A config-declared provider route has invalid identity evidence.");
|
|
298
|
+
}
|
|
299
|
+
const binding = {
|
|
300
|
+
...adapter.identity,
|
|
301
|
+
routeHandle: route.routeHandle,
|
|
302
|
+
ownerLease: stableLease(adapter.identity.provider, `${adapter.identity.hostId}\0${route.alias}`),
|
|
303
|
+
};
|
|
304
|
+
await this.registerOrRebind(route.alias, binding, "endpoint_reobserved", route.state, binding.ownerLease, "explicit_opt_in");
|
|
305
|
+
this.rememberBinding(route.alias, binding, route.state);
|
|
306
|
+
bootReactivatedIdleAliases.push(route.alias);
|
|
307
|
+
}
|
|
317
308
|
assertStartActive();
|
|
318
309
|
}
|
|
310
|
+
await this.refreshClaudeDiscovery({ incrementRevision: false }).catch(() => false);
|
|
311
|
+
await this.refreshCodexDoctor();
|
|
312
|
+
for (const [alias, binding] of this.routeBindings) {
|
|
313
|
+
if (binding.provider === "claude" &&
|
|
314
|
+
this.routeStates.get(alias) === "idle") {
|
|
315
|
+
bootReactivatedIdleAliases.push(alias);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
319
318
|
await this.recoverCodexSuccessionAfterRestartLocked();
|
|
320
|
-
bootReactivatedIdleAliases
|
|
321
|
-
await this.reactivateRetainedCodexRoutesAfterRestartLocked();
|
|
319
|
+
bootReactivatedIdleAliases.push(...(await this.reactivateRetainedCodexRoutesAfterRestartLocked()));
|
|
322
320
|
assertStartActive();
|
|
323
321
|
const control = await startGatewayControlServer({
|
|
324
322
|
stateDir: this.store.rootDir,
|
|
@@ -337,8 +335,11 @@ export class GatewayService {
|
|
|
337
335
|
this.control = control;
|
|
338
336
|
this.running = true;
|
|
339
337
|
this.nextDashboardRefreshAt = undefined;
|
|
338
|
+
this.nextClaudeDiscoveryAt =
|
|
339
|
+
this.now().getTime() + CLAUDE_DISCOVERY_REFRESH_MS;
|
|
340
340
|
await this.refreshNextProgressWatchWakeLocked();
|
|
341
341
|
this.scheduleLifecycleWakeLocked();
|
|
342
|
+
this.scheduleCodexDoctor();
|
|
342
343
|
for (const alias of bootReactivatedIdleAliases) {
|
|
343
344
|
this.scheduleDispatch(alias);
|
|
344
345
|
}
|
|
@@ -367,6 +368,12 @@ export class GatewayService {
|
|
|
367
368
|
const control = this.control;
|
|
368
369
|
this.control = undefined;
|
|
369
370
|
let closeFailed = false;
|
|
371
|
+
if (this.codexDoctorTimer !== undefined) {
|
|
372
|
+
this.timers.clearTimeout(this.codexDoctorTimer);
|
|
373
|
+
this.codexDoctorTimer = undefined;
|
|
374
|
+
}
|
|
375
|
+
await this.codexDoctorInFlight?.catch(() => undefined);
|
|
376
|
+
await this.claudeDiscoveryInFlight?.catch(() => undefined);
|
|
370
377
|
if (control !== undefined) {
|
|
371
378
|
try {
|
|
372
379
|
await control.close();
|
|
@@ -383,6 +390,9 @@ export class GatewayService {
|
|
|
383
390
|
this.timers.clearTimeout(this.lifecycleTimer);
|
|
384
391
|
this.lifecycleTimer = undefined;
|
|
385
392
|
}
|
|
393
|
+
this.nextClaudeDiscoveryAt = undefined;
|
|
394
|
+
this.lifecyclePublishPending = false;
|
|
395
|
+
this.connectorFreshnessDeadlineAt.clear();
|
|
386
396
|
const quiesceResults = await Promise.allSettled(this.adapters.map(async (adapter) => await adapter.quiesceNativeInbound?.()));
|
|
387
397
|
if (quiesceResults.some((result) => result.status === "rejected")) {
|
|
388
398
|
closeFailed = true;
|
|
@@ -395,6 +405,13 @@ export class GatewayService {
|
|
|
395
405
|
for (const messageId of [...this.messageContexts.keys()]) {
|
|
396
406
|
if (retainedQueuedMessageIds.has(messageId))
|
|
397
407
|
continue;
|
|
408
|
+
if (this.providerDispatchInFlightMessageIds.has(messageId)) {
|
|
409
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
410
|
+
type: "dispatch_ambiguous",
|
|
411
|
+
at: this.now().getTime(),
|
|
412
|
+
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
398
415
|
const queued = await this.store.settleQueuedMessage({
|
|
399
416
|
messageId,
|
|
400
417
|
state: "cancelled",
|
|
@@ -452,6 +469,7 @@ export class GatewayService {
|
|
|
452
469
|
this.activeDispatchByTarget.clear();
|
|
453
470
|
this.scheduledDispatchTargets.clear();
|
|
454
471
|
this.dispatchRunnerTargets.clear();
|
|
472
|
+
this.providerDispatchInFlightMessageIds.clear();
|
|
455
473
|
for (const timer of this.heldRedispatchTimers.values()) {
|
|
456
474
|
this.timers.clearTimeout(timer);
|
|
457
475
|
}
|
|
@@ -460,7 +478,6 @@ export class GatewayService {
|
|
|
460
478
|
this.deliveryTrackers.clear();
|
|
461
479
|
this.deliveryTokens.clear();
|
|
462
480
|
this.runtimeAlerts.length = 0;
|
|
463
|
-
this.incompatibleProviderSurfaces.clear();
|
|
464
481
|
this.registryEvidenceByConnector.clear();
|
|
465
482
|
this.registryAdaptersWithParseableRecords.clear();
|
|
466
483
|
this.detachedReceiptWrites.clear();
|
|
@@ -476,7 +493,6 @@ export class GatewayService {
|
|
|
476
493
|
this.activatedEndpointRefreshes.clear();
|
|
477
494
|
this.durablyAppliedEndpointRefreshes.clear();
|
|
478
495
|
this.pendingSelectorEndpointActivations.clear();
|
|
479
|
-
this.handledIncompatibleEndpointRefreshes.clear();
|
|
480
496
|
this.endpointRefreshActivityKeys.clear();
|
|
481
497
|
this.codexOrphanCleanupPending.clear();
|
|
482
498
|
this.candidates.clear();
|
|
@@ -536,12 +552,12 @@ export class GatewayService {
|
|
|
536
552
|
pair: async (params) => await this.exclusiveDecision(async () => this.pairRoutes(params), {
|
|
537
553
|
kind: "pairing",
|
|
538
554
|
action: "routes_paired",
|
|
539
|
-
aliases: [params
|
|
555
|
+
aliases: [...pairParamAliases(params)],
|
|
540
556
|
}),
|
|
541
557
|
unpair: async (params) => await this.exclusiveDecision(async () => this.unpairRoutes(params), {
|
|
542
558
|
kind: "pairing",
|
|
543
559
|
action: "routes_unpaired",
|
|
544
|
-
aliases: [params
|
|
560
|
+
aliases: [...pairParamAliases(params)],
|
|
545
561
|
}),
|
|
546
562
|
listSnapshot: async () => (await this.observeSnapshot()).snapshot,
|
|
547
563
|
observeSnapshot: async () => await this.observeSnapshot(),
|
|
@@ -572,91 +588,6 @@ export class GatewayService {
|
|
|
572
588
|
async snapshot() {
|
|
573
589
|
return (await this.observeSnapshot()).snapshot;
|
|
574
590
|
}
|
|
575
|
-
async runAutomaticCompatibilityProbesLocked() {
|
|
576
|
-
const entries = [
|
|
577
|
-
...this.adapters.map((adapter) => {
|
|
578
|
-
if (adapter.compatibilitySurface === undefined ||
|
|
579
|
-
adapter.runCompatibilityProbes === undefined) {
|
|
580
|
-
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Every local provider must expose the bounded compatibility probe contract.");
|
|
581
|
-
}
|
|
582
|
-
return {
|
|
583
|
-
observer: adapter,
|
|
584
|
-
adapter,
|
|
585
|
-
};
|
|
586
|
-
}),
|
|
587
|
-
...this.compatibilityObservers.map((observer) => ({ observer })),
|
|
588
|
-
];
|
|
589
|
-
const observedEntries = entries.map((entry) => ({
|
|
590
|
-
...entry,
|
|
591
|
-
observation: entry.observer.compatibilitySurface(),
|
|
592
|
-
}));
|
|
593
|
-
const bySurface = new Map(observedEntries.map((entry) => [entry.observation.surface, entry]));
|
|
594
|
-
if (bySurface.size !== entries.length) {
|
|
595
|
-
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Each compatibility observer must expose one distinct surface.");
|
|
596
|
-
}
|
|
597
|
-
const declared = new Set(this.compatibilitySurfaceDefinitions.map(({ surface }) => surface));
|
|
598
|
-
if ([...bySurface.keys()].some((surface) => !declared.has(surface))) {
|
|
599
|
-
throw new BridgeError("COMPAT_SURFACE_UNDECLARED", "Every compatibility observer must use a declared surface.");
|
|
600
|
-
}
|
|
601
|
-
const forbiddenCodexThreadIds = (await this.store.inspectPrivateCodexRoutes()).map((route) => route.binding.routeHandle);
|
|
602
|
-
const probeContext = {
|
|
603
|
-
stateRoot: this.store.rootDir,
|
|
604
|
-
forbiddenCodexThreadIds,
|
|
605
|
-
};
|
|
606
|
-
const attestations = [];
|
|
607
|
-
for (const definition of this.compatibilitySurfaceDefinitions) {
|
|
608
|
-
const { surface } = definition;
|
|
609
|
-
const entry = bySurface.get(surface);
|
|
610
|
-
if (entry === undefined) {
|
|
611
|
-
if (!definition.required)
|
|
612
|
-
continue;
|
|
613
|
-
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
|
|
614
|
-
}
|
|
615
|
-
const probes = entry.adapter === undefined
|
|
616
|
-
? await entry.observer.runCompatibilityProbes()
|
|
617
|
-
: await entry.adapter.runCompatibilityProbes(probeContext);
|
|
618
|
-
let writeProbePassed = false;
|
|
619
|
-
if (entry.adapter !== undefined) {
|
|
620
|
-
const writeProbe = entry.adapter.latestWriteCompatibilityProbeObservation?.(entry.adapter.identity.endpointGeneration);
|
|
621
|
-
writeProbePassed = writeProbe?.outcome === "pass";
|
|
622
|
-
if (writeProbe?.outcome === "fail") {
|
|
623
|
-
this.addRuntimeAlert(writeProbe.safeErrorCode, writeProbe.safeErrorCode === "CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED" ||
|
|
624
|
-
writeProbe.safeErrorCode === "CODEX_WRITE_PROBE_CLEANUP_UNCONFIRMED"
|
|
625
|
-
? "error"
|
|
626
|
-
: "warning", { provider: "codex", host: entry.adapter.identity.hostId });
|
|
627
|
-
}
|
|
628
|
-
this.captureRegistryObservation(entry.adapter, entry.adapter.latestRegistryObservation?.());
|
|
629
|
-
}
|
|
630
|
-
const attestation = evaluateCompatibilityAttestation({
|
|
631
|
-
surface,
|
|
632
|
-
version: entry.observation.version,
|
|
633
|
-
checkedAt: this.now().toISOString(),
|
|
634
|
-
certifiedVersions: certifiedCompatibilityVersions[surface],
|
|
635
|
-
probes,
|
|
636
|
-
});
|
|
637
|
-
await this.store.recordCompatibilityAttestation(attestation);
|
|
638
|
-
entry.observer.acceptCompatibilityAttestation?.(attestation);
|
|
639
|
-
if (attestation.tier === "incompatible" && entry.adapter !== undefined) {
|
|
640
|
-
const code = attestation.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED";
|
|
641
|
-
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey(entry.adapter.identity.provider, entry.adapter.identity.hostId), code);
|
|
642
|
-
if (surface === "codex") {
|
|
643
|
-
this.activeCodexIncompatibility = structuredClone(attestation);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
else if (surface === "codex" &&
|
|
647
|
-
attestation.tier === "schema_attested" &&
|
|
648
|
-
!(compatibilityCoversWrites(attestation) && writeProbePassed) &&
|
|
649
|
-
entry.adapter !== undefined) {
|
|
650
|
-
// A live but untested App Server schema is useful for observation only.
|
|
651
|
-
// Fence retained boot reactivation before it can trigger endpoint
|
|
652
|
-
// refresh churn; an explicit registration may still re-probe a later,
|
|
653
|
-
// certified generation through the provider's bounded refresh path.
|
|
654
|
-
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", entry.adapter.identity.hostId), "CODEX_MONITOR_ONLY");
|
|
655
|
-
}
|
|
656
|
-
attestations.push(attestation);
|
|
657
|
-
}
|
|
658
|
-
return attestations;
|
|
659
|
-
}
|
|
660
591
|
captureRegistryObservation(adapter, observation) {
|
|
661
592
|
if (adapter.identity.provider !== "claude" ||
|
|
662
593
|
observation === undefined ||
|
|
@@ -703,6 +634,14 @@ export class GatewayService {
|
|
|
703
634
|
const registry = this.registryEvidenceByConnector.get(`${connector.provider}\0${connector.host}`);
|
|
704
635
|
return {
|
|
705
636
|
...connector,
|
|
637
|
+
...(connector.provider === "codex" &&
|
|
638
|
+
this.codexDoctorResult !== undefined
|
|
639
|
+
? {
|
|
640
|
+
codexDoctor: {
|
|
641
|
+
conditions: this.codexDoctorConditions(connector),
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
: {}),
|
|
706
645
|
...(connector.provider !== "claude" || registry === undefined
|
|
707
646
|
? {}
|
|
708
647
|
: {
|
|
@@ -714,6 +653,70 @@ export class GatewayService {
|
|
|
714
653
|
};
|
|
715
654
|
});
|
|
716
655
|
}
|
|
656
|
+
recordConnectorFreshness(identity, health) {
|
|
657
|
+
const key = `${identity.provider}\0${identity.hostId}`;
|
|
658
|
+
if (health === "healthy") {
|
|
659
|
+
this.connectorFreshnessDeadlineAt.set(key, this.now().getTime() + CONNECTOR_OBSERVATION_STALE_AFTER_MS + 1);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
this.connectorFreshnessDeadlineAt.delete(key);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
codexDoctorConditions(connector) {
|
|
666
|
+
const attachment = this.codexDoctorResult?.conditions[0] ?? "unknown";
|
|
667
|
+
const stale = connector.observationAgeMs === undefined ||
|
|
668
|
+
connector.observationAgeMs > CONNECTOR_OBSERVATION_STALE_AFTER_MS;
|
|
669
|
+
if (attachment === "split_brain" || attachment === "orphaned") {
|
|
670
|
+
return stale ? [attachment, "observation_stale"] : [attachment];
|
|
671
|
+
}
|
|
672
|
+
if (stale)
|
|
673
|
+
return ["observation_stale", attachment];
|
|
674
|
+
return [attachment];
|
|
675
|
+
}
|
|
676
|
+
async refreshCodexDoctor() {
|
|
677
|
+
if (this.codexDoctor === undefined)
|
|
678
|
+
return false;
|
|
679
|
+
const next = await this.codexDoctor().catch(() => ({ conditions: ["unknown"] }));
|
|
680
|
+
const changed = JSON.stringify(next.conditions) !==
|
|
681
|
+
JSON.stringify(this.codexDoctorResult?.conditions);
|
|
682
|
+
this.codexDoctorResult = next;
|
|
683
|
+
return changed;
|
|
684
|
+
}
|
|
685
|
+
scheduleCodexDoctor() {
|
|
686
|
+
if (!this.running ||
|
|
687
|
+
this.closing ||
|
|
688
|
+
this.codexDoctor === undefined ||
|
|
689
|
+
this.codexDoctorTimer !== undefined ||
|
|
690
|
+
this.codexDoctorInFlight !== undefined) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const timer = this.timers.setTimeout(() => {
|
|
694
|
+
if (this.codexDoctorTimer !== timer)
|
|
695
|
+
return;
|
|
696
|
+
this.codexDoctorTimer = undefined;
|
|
697
|
+
const operation = this.refreshCodexDoctor()
|
|
698
|
+
.then(async (changed) => {
|
|
699
|
+
if (!changed || this.closing)
|
|
700
|
+
return;
|
|
701
|
+
await this.mutex.run("service", async () => {
|
|
702
|
+
this.revision += 1;
|
|
703
|
+
await this.publish();
|
|
704
|
+
});
|
|
705
|
+
})
|
|
706
|
+
.catch(() => {
|
|
707
|
+
this.dashboardHealthy = false;
|
|
708
|
+
});
|
|
709
|
+
this.codexDoctorInFlight = operation;
|
|
710
|
+
void operation.finally(() => {
|
|
711
|
+
if (this.codexDoctorInFlight === operation) {
|
|
712
|
+
this.codexDoctorInFlight = undefined;
|
|
713
|
+
}
|
|
714
|
+
this.scheduleCodexDoctor();
|
|
715
|
+
});
|
|
716
|
+
}, CODEX_DOCTOR_REFRESH_MS);
|
|
717
|
+
this.codexDoctorTimer = timer;
|
|
718
|
+
timer.unref?.();
|
|
719
|
+
}
|
|
717
720
|
async observeSnapshot() {
|
|
718
721
|
return await this.mutex.run("service", async () => {
|
|
719
722
|
const changed = await this.processLifecycleLocked();
|
|
@@ -746,7 +749,11 @@ export class GatewayService {
|
|
|
746
749
|
fingerprintSnapshot(snapshot) {
|
|
747
750
|
// Closed snapshot validation bounds every array and field before this
|
|
748
751
|
// serialization. Omitting generatedAt is the sole semantic exception.
|
|
749
|
-
const { generatedAt: _generatedAt, ...
|
|
752
|
+
const { generatedAt: _generatedAt, connectors, ...snapshotWithoutGeneratedAt } = snapshot;
|
|
753
|
+
const semanticSnapshot = {
|
|
754
|
+
...snapshotWithoutGeneratedAt,
|
|
755
|
+
connectors: connectors.map(({ observationAgeMs: _observationAgeMs, ...connector }) => connector),
|
|
756
|
+
};
|
|
750
757
|
const encoded = JSON.stringify(semanticSnapshot);
|
|
751
758
|
if (Buffer.byteLength(encoded, "utf8") >
|
|
752
759
|
GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET) {
|
|
@@ -884,50 +891,30 @@ export class GatewayService {
|
|
|
884
891
|
});
|
|
885
892
|
},
|
|
886
893
|
onEndpointRefresh: (event) => {
|
|
894
|
+
if (!Array.isArray(event.routes))
|
|
895
|
+
return;
|
|
896
|
+
const routeHandles = event.routes.map((route) => route.routeHandle);
|
|
887
897
|
if (!this.running ||
|
|
888
898
|
adapter.identity.provider !== "codex" ||
|
|
889
899
|
event.previous.provider !== "codex" ||
|
|
890
900
|
event.previous.hostId !== adapter.identity.hostId ||
|
|
891
|
-
|
|
892
|
-
event.
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
if (event.outcome === "compatible") {
|
|
896
|
-
const routeHandles = event.routes.map((route) => route.routeHandle);
|
|
897
|
-
if (event.current.provider !== "codex" ||
|
|
898
|
-
event.current.hostId !== event.previous.hostId ||
|
|
899
|
-
event.current.endpointGeneration !==
|
|
900
|
-
adapter.identity.endpointGeneration ||
|
|
901
|
-
event.attestation.version !== adapter.protocolVersion ||
|
|
902
|
-
event.attestation.tier === "incompatible" ||
|
|
903
|
-
routeHandles.length > this.config.limits.maxRoutes ||
|
|
904
|
-
new Set(routeHandles).size !== routeHandles.length ||
|
|
905
|
-
routeHandles.some((routeHandle) => !PRIVATE_HANDLE.test(routeHandle))) {
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
else if (event.candidate.provider !== "codex" ||
|
|
910
|
-
event.candidate.hostId !== event.previous.hostId ||
|
|
911
|
-
event.previous.endpointGeneration !==
|
|
901
|
+
event.current.provider !== "codex" ||
|
|
902
|
+
event.current.hostId !== event.previous.hostId ||
|
|
903
|
+
event.current.endpointGeneration !==
|
|
912
904
|
adapter.identity.endpointGeneration ||
|
|
913
|
-
|
|
905
|
+
routeHandles.length > this.config.limits.maxRoutes ||
|
|
906
|
+
new Set(routeHandles).size !== routeHandles.length ||
|
|
907
|
+
routeHandles.some((routeHandle) => !PRIVATE_HANDLE.test(routeHandle))) {
|
|
914
908
|
return;
|
|
915
909
|
}
|
|
916
910
|
this.enqueueCallback({
|
|
917
911
|
type: "endpoint_refresh",
|
|
918
912
|
adapter,
|
|
919
|
-
value:
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
routes: event.routes.map((route) => ({ ...route })),
|
|
925
|
-
}
|
|
926
|
-
: {
|
|
927
|
-
...event,
|
|
928
|
-
previous: { ...event.previous },
|
|
929
|
-
candidate: { ...event.candidate },
|
|
930
|
-
},
|
|
913
|
+
value: {
|
|
914
|
+
previous: { ...event.previous },
|
|
915
|
+
current: { ...event.current },
|
|
916
|
+
routes: event.routes.map((route) => ({ ...route })),
|
|
917
|
+
},
|
|
931
918
|
});
|
|
932
919
|
},
|
|
933
920
|
};
|
|
@@ -1032,10 +1019,10 @@ export class GatewayService {
|
|
|
1032
1019
|
this.endpointRefreshConflictPending);
|
|
1033
1020
|
}
|
|
1034
1021
|
enqueueCallbackWorkerOnly() {
|
|
1035
|
-
if (this.callbackScheduled)
|
|
1022
|
+
if (this.callbackScheduled || this.callbackWorker !== undefined)
|
|
1036
1023
|
return;
|
|
1037
1024
|
this.callbackScheduled = true;
|
|
1038
|
-
|
|
1025
|
+
queueMicrotask(() => {
|
|
1039
1026
|
this.callbackScheduled = false;
|
|
1040
1027
|
const worker = this.mutex.run("service", async () => {
|
|
1041
1028
|
await this.drainCallbackQueueLocked();
|
|
@@ -1074,7 +1061,7 @@ export class GatewayService {
|
|
|
1074
1061
|
await this.onDelivery(event.source, event.value, event.receivedAt);
|
|
1075
1062
|
}
|
|
1076
1063
|
else if (event.type === "route") {
|
|
1077
|
-
if (pendingRefresh
|
|
1064
|
+
if (pendingRefresh !== undefined &&
|
|
1078
1065
|
event.source.provider === "codex" &&
|
|
1079
1066
|
event.source.hostId === pendingRefresh.current.hostId &&
|
|
1080
1067
|
event.source.endpointGeneration ===
|
|
@@ -1204,12 +1191,7 @@ export class GatewayService {
|
|
|
1204
1191
|
await this.drainDetachedReceiptWritesLocked();
|
|
1205
1192
|
}
|
|
1206
1193
|
async failClosedEndpointRefreshConflict(boundary) {
|
|
1207
|
-
const identities = [
|
|
1208
|
-
boundary.value.previous,
|
|
1209
|
-
...(boundary.value.outcome === "compatible"
|
|
1210
|
-
? [boundary.value.current]
|
|
1211
|
-
: []),
|
|
1212
|
-
];
|
|
1194
|
+
const identities = [boundary.value.previous, boundary.value.current];
|
|
1213
1195
|
for (const identity of identities) {
|
|
1214
1196
|
const routes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.binding.hostId === identity.hostId &&
|
|
1215
1197
|
route.binding.endpointGeneration === identity.endpointGeneration);
|
|
@@ -1311,23 +1293,6 @@ export class GatewayService {
|
|
|
1311
1293
|
throw new BridgeError("PROVIDER_UNAVAILABLE", "The selected provider is unavailable.", true);
|
|
1312
1294
|
return adapter;
|
|
1313
1295
|
}
|
|
1314
|
-
providerSurfaceKey(provider, host) {
|
|
1315
|
-
return `${provider}@${host}`;
|
|
1316
|
-
}
|
|
1317
|
-
incompatibleProviderCode(provider, host) {
|
|
1318
|
-
return this.incompatibleProviderSurfaces.get(this.providerSurfaceKey(provider, host));
|
|
1319
|
-
}
|
|
1320
|
-
/** Reject before either half of a cross-provider mutation can acquire state. */
|
|
1321
|
-
assertCrossProviderMutationCompatible(host, options = { allowCodexRefresh: false }) {
|
|
1322
|
-
const claudeCode = this.incompatibleProviderCode("claude", host);
|
|
1323
|
-
if (claudeCode !== undefined) {
|
|
1324
|
-
throw new BridgeError(claudeCode, "The Claude provider is observation-only until Embassy restarts with a compatible runtime.");
|
|
1325
|
-
}
|
|
1326
|
-
const codexCode = this.incompatibleProviderCode("codex", host);
|
|
1327
|
-
if (codexCode !== undefined && !options.allowCodexRefresh) {
|
|
1328
|
-
throw new BridgeError(codexCode, "The Codex provider is observation-only until a compatible endpoint is re-observed.");
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
1296
|
async assertClaudeWorkspaceDisjoint(adapter, routeHandle) {
|
|
1332
1297
|
if (adapter.identity.provider !== "claude") {
|
|
1333
1298
|
throw new BridgeError("CLAUDE_PROVIDER_MISMATCH", "The selected workspace guard does not belong to Claude.");
|
|
@@ -1385,7 +1350,6 @@ export class GatewayService {
|
|
|
1385
1350
|
for (const route of retained) {
|
|
1386
1351
|
if (!route.enabled ||
|
|
1387
1352
|
route.state !== "stale" ||
|
|
1388
|
-
route.compatibility !== "expired" ||
|
|
1389
1353
|
this.pendingCodexSuccessionRecovery?.journal.new.alias === route.alias ||
|
|
1390
1354
|
this.codexSuccessionPoisoned.has(route.alias)) {
|
|
1391
1355
|
continue;
|
|
@@ -1398,13 +1362,6 @@ export class GatewayService {
|
|
|
1398
1362
|
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1399
1363
|
continue;
|
|
1400
1364
|
}
|
|
1401
|
-
if (this.incompatibleProviderCode("codex", route.binding.hostId) !==
|
|
1402
|
-
undefined ||
|
|
1403
|
-
this.incompatibleProviderCode("claude", route.binding.hostId) !==
|
|
1404
|
-
undefined) {
|
|
1405
|
-
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
1365
|
let routeSelected = false;
|
|
1409
1366
|
let advertiseAttempted = false;
|
|
1410
1367
|
let storeReactivated = false;
|
|
@@ -1442,19 +1399,20 @@ export class GatewayService {
|
|
|
1442
1399
|
storeReactivated =
|
|
1443
1400
|
afterStaging !== undefined &&
|
|
1444
1401
|
afterStaging.enabled &&
|
|
1445
|
-
afterStaging.
|
|
1402
|
+
afterStaging.state !== "stale" &&
|
|
1446
1403
|
bindingKey(afterStaging.binding) === bindingKey(binding);
|
|
1447
1404
|
}
|
|
1448
1405
|
advertiseAttempted = true;
|
|
1449
|
-
const advertise = claudeAdapter.
|
|
1406
|
+
const advertise = claudeAdapter.advertiseNativeSourcePeer;
|
|
1450
1407
|
if (advertise === undefined) {
|
|
1451
1408
|
throw new BridgeError("CODEX_BOOT_REACTIVATION_UNAVAILABLE", "The retained Codex task cannot rebuild its native advertisement.");
|
|
1452
1409
|
}
|
|
1453
1410
|
await advertise.call(claudeAdapter, {
|
|
1454
1411
|
alias: route.alias,
|
|
1412
|
+
sourceProvider: "codex",
|
|
1455
1413
|
cwd: this.nativePeerCwd,
|
|
1456
1414
|
});
|
|
1457
|
-
await claudeAdapter.
|
|
1415
|
+
await claudeAdapter.updateNativeSourcePeerStatus?.(route.alias, selected.state === "idle"
|
|
1458
1416
|
? "idle"
|
|
1459
1417
|
: selected.state === "awaiting_approval"
|
|
1460
1418
|
? "waiting"
|
|
@@ -1498,7 +1456,7 @@ export class GatewayService {
|
|
|
1498
1456
|
let cleanupFailed = false;
|
|
1499
1457
|
if (advertiseAttempted) {
|
|
1500
1458
|
await claudeAdapter
|
|
1501
|
-
.
|
|
1459
|
+
.unadvertiseNativeSourcePeer?.(route.alias)
|
|
1502
1460
|
.catch(() => {
|
|
1503
1461
|
cleanupFailed = true;
|
|
1504
1462
|
});
|
|
@@ -1630,7 +1588,7 @@ export class GatewayService {
|
|
|
1630
1588
|
oldInspection.binding.ownerLease !==
|
|
1631
1589
|
stableLease("codex", `${lock.hostId}\0${lock.threadId}`) ||
|
|
1632
1590
|
!oldInspection.enabled ||
|
|
1633
|
-
oldInspection.
|
|
1591
|
+
oldInspection.state === "stale") {
|
|
1634
1592
|
throw new BridgeError("CODEX_SUCCESSION_OWNER_MISMATCH", "The active Codex registration is no longer exact and compatible.");
|
|
1635
1593
|
}
|
|
1636
1594
|
const claudeAdapter = this.adapter("claude", params.hostId);
|
|
@@ -1939,7 +1897,7 @@ export class GatewayService {
|
|
|
1939
1897
|
this.rememberBinding(effect.registration.alias, execution.newBinding, state);
|
|
1940
1898
|
this.codexRegistrationLocks.delete(this.successionOldRegistration(execution).alias);
|
|
1941
1899
|
this.codexRegistrationLocks.set(effect.registration.alias, Object.freeze({ ...effect.registration }));
|
|
1942
|
-
await claudeAdapter.
|
|
1900
|
+
await claudeAdapter.updateNativeSourcePeerStatus?.(effect.registration.alias, state === "idle"
|
|
1943
1901
|
? "idle"
|
|
1944
1902
|
: state === "awaiting_approval"
|
|
1945
1903
|
? "waiting"
|
|
@@ -2084,8 +2042,8 @@ export class GatewayService {
|
|
|
2084
2042
|
throw new BridgeError("CODEX_SUCCESSION_DRAIN_INCOMPLETE", "Interrupted generations cannot close before pending work and receipts are drained. Restart the gateway with embassy serve.");
|
|
2085
2043
|
}
|
|
2086
2044
|
const results = await Promise.allSettled([
|
|
2087
|
-
claudeAdapter.
|
|
2088
|
-
claudeAdapter.
|
|
2045
|
+
claudeAdapter.unadvertiseNativeSourcePeer?.(execution.oldRegistration.alias),
|
|
2046
|
+
claudeAdapter.unadvertiseNativeSourcePeer?.(execution.newRegistration.alias),
|
|
2089
2047
|
codexAdapter.releaseRoute?.(execution.oldRegistration.threadId),
|
|
2090
2048
|
codexAdapter.releaseRoute?.(execution.newRegistration.threadId),
|
|
2091
2049
|
]);
|
|
@@ -2310,9 +2268,6 @@ export class GatewayService {
|
|
|
2310
2268
|
if (!params.alias.startsWith("codex-")) {
|
|
2311
2269
|
throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
|
|
2312
2270
|
}
|
|
2313
|
-
this.assertCrossProviderMutationCompatible(params.hostId, {
|
|
2314
|
-
allowCodexRefresh: params.succeedsAlias === undefined,
|
|
2315
|
-
});
|
|
2316
2271
|
if (this.codexSuccessionPoisoned.has(params.alias) ||
|
|
2317
2272
|
this.codexOrphanCleanupPending.has(params.alias) ||
|
|
2318
2273
|
(params.succeedsAlias !== undefined &&
|
|
@@ -2383,7 +2338,7 @@ export class GatewayService {
|
|
|
2383
2338
|
const afterRefresh = await this.store.inspectPrivateRoute(params.alias);
|
|
2384
2339
|
const alreadyInstalledExactly = afterRefresh !== undefined &&
|
|
2385
2340
|
afterRefresh.enabled &&
|
|
2386
|
-
afterRefresh.
|
|
2341
|
+
afterRefresh.state !== "stale" &&
|
|
2387
2342
|
bindingKey(afterRefresh.binding) === bindingKey(binding);
|
|
2388
2343
|
if (!alreadyInstalledExactly || !deferredEndpointRefresh) {
|
|
2389
2344
|
await this.registerOrRebind(params.alias, binding, "endpoint_reobserved", registered.state);
|
|
@@ -2398,15 +2353,16 @@ export class GatewayService {
|
|
|
2398
2353
|
try {
|
|
2399
2354
|
this.rememberBinding(params.alias, binding, registered.state);
|
|
2400
2355
|
const claudeAdapter = this.adapter("claude", params.hostId);
|
|
2401
|
-
const advertise = claudeAdapter.
|
|
2356
|
+
const advertise = claudeAdapter.advertiseNativeSourcePeer;
|
|
2402
2357
|
if (advertise !== undefined) {
|
|
2403
2358
|
advertiseAttempted = true;
|
|
2404
2359
|
await advertise.call(claudeAdapter, {
|
|
2405
2360
|
alias: params.alias,
|
|
2361
|
+
sourceProvider: "codex",
|
|
2406
2362
|
cwd: this.nativePeerCwd,
|
|
2407
2363
|
});
|
|
2408
2364
|
}
|
|
2409
|
-
await claudeAdapter.
|
|
2365
|
+
await claudeAdapter.updateNativeSourcePeerStatus?.(params.alias, registered.state === "idle"
|
|
2410
2366
|
? "idle"
|
|
2411
2367
|
: registered.state === "awaiting_approval"
|
|
2412
2368
|
? "waiting"
|
|
@@ -2428,8 +2384,7 @@ export class GatewayService {
|
|
|
2428
2384
|
}
|
|
2429
2385
|
catch (error) {
|
|
2430
2386
|
if (routeBeforeRegistration !== undefined) {
|
|
2431
|
-
if (deferredEndpointRefresh &&
|
|
2432
|
-
registered.endpointRefresh?.outcome === "compatible") {
|
|
2387
|
+
if (deferredEndpointRefresh && registered.endpointRefresh !== undefined) {
|
|
2433
2388
|
try {
|
|
2434
2389
|
await this.store.markConnectorOffline(registered.endpointRefresh.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
|
|
2435
2390
|
this.routeStates.set(params.alias, "stale");
|
|
@@ -2511,7 +2466,7 @@ export class GatewayService {
|
|
|
2511
2466
|
});
|
|
2512
2467
|
if (advertiseAttempted) {
|
|
2513
2468
|
await this.adapter("claude", params.hostId)
|
|
2514
|
-
.
|
|
2469
|
+
.unadvertiseNativeSourcePeer?.(params.alias)
|
|
2515
2470
|
.catch(() => {
|
|
2516
2471
|
cleanupFailed = true;
|
|
2517
2472
|
});
|
|
@@ -2538,7 +2493,7 @@ export class GatewayService {
|
|
|
2538
2493
|
}
|
|
2539
2494
|
this.forgetBinding(params.alias);
|
|
2540
2495
|
await this.adapter("claude", host)
|
|
2541
|
-
.
|
|
2496
|
+
.unadvertiseNativeSourcePeer?.(params.alias)
|
|
2542
2497
|
.catch(() => {
|
|
2543
2498
|
this.dashboardHealthy = false;
|
|
2544
2499
|
});
|
|
@@ -2709,7 +2664,7 @@ export class GatewayService {
|
|
|
2709
2664
|
}
|
|
2710
2665
|
await this.requireUnchangedStaleCodexOrphan(alias, binding);
|
|
2711
2666
|
if (nativeGeneration !== undefined) {
|
|
2712
|
-
const unadvertise = claudeAdapter.
|
|
2667
|
+
const unadvertise = claudeAdapter.unadvertiseNativeSourcePeer;
|
|
2713
2668
|
if (unadvertise === undefined) {
|
|
2714
2669
|
throw new BridgeError("CODEX_ORPHAN_NATIVE_IDENTITY_UNPROVEN", "The native Codex listener cannot be removed exactly.");
|
|
2715
2670
|
}
|
|
@@ -2773,7 +2728,7 @@ export class GatewayService {
|
|
|
2773
2728
|
return JSON.stringify({
|
|
2774
2729
|
inboundMode: snapshot.inboundMode,
|
|
2775
2730
|
health: snapshot.health,
|
|
2776
|
-
connectors: snapshot.connectors.map(({ lastSeenAt: _lastSeenAt, ...connector }) => connector),
|
|
2731
|
+
connectors: snapshot.connectors.map(({ lastSeenAt: _lastSeenAt, observationAgeMs: _observationAgeMs, ...connector }) => connector),
|
|
2777
2732
|
availablePeers: snapshot.availablePeers.map(({ lastSeenAt: _lastSeenAt, ...peer }) => peer),
|
|
2778
2733
|
routes: snapshot.routes.map(({ lastSeenAt: _lastSeenAt, ...route }) => route),
|
|
2779
2734
|
messages: snapshot.messages,
|
|
@@ -2782,7 +2737,116 @@ export class GatewayService {
|
|
|
2782
2737
|
truncation: snapshot.truncation,
|
|
2783
2738
|
});
|
|
2784
2739
|
}
|
|
2785
|
-
|
|
2740
|
+
schedulePeriodicClaudeDiscovery() {
|
|
2741
|
+
if (this.closing ||
|
|
2742
|
+
this.claudeDiscoveryScheduled ||
|
|
2743
|
+
this.claudeDiscoveryInFlight !== undefined) {
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
this.claudeDiscoveryScheduled = true;
|
|
2747
|
+
queueMicrotask(() => {
|
|
2748
|
+
this.claudeDiscoveryScheduled = false;
|
|
2749
|
+
if (this.closing || !this.running)
|
|
2750
|
+
return;
|
|
2751
|
+
const operation = this.refreshClaudeDiscoveryPeriodically().catch(() => {
|
|
2752
|
+
this.dashboardHealthy = false;
|
|
2753
|
+
});
|
|
2754
|
+
this.claudeDiscoveryInFlight = operation;
|
|
2755
|
+
void operation.finally(() => {
|
|
2756
|
+
if (this.claudeDiscoveryInFlight === operation) {
|
|
2757
|
+
this.claudeDiscoveryInFlight = undefined;
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
async refreshClaudeDiscoveryPeriodically() {
|
|
2763
|
+
const observations = (await Promise.all(this.adapters
|
|
2764
|
+
.filter((adapter) => adapter.identity.provider === "claude")
|
|
2765
|
+
.map(async (adapter) => {
|
|
2766
|
+
try {
|
|
2767
|
+
const snapshot = await adapter.discoverClaudePeers?.();
|
|
2768
|
+
return snapshot === undefined ? undefined : { adapter, snapshot };
|
|
2769
|
+
}
|
|
2770
|
+
catch {
|
|
2771
|
+
return undefined;
|
|
2772
|
+
}
|
|
2773
|
+
}))).filter((item) => item !== undefined);
|
|
2774
|
+
if (this.closing)
|
|
2775
|
+
return;
|
|
2776
|
+
const preparedRebinds = [];
|
|
2777
|
+
const persisted = await this.store.inspectPrivateClaudeRoutes();
|
|
2778
|
+
for (const route of persisted) {
|
|
2779
|
+
if (!route.enabled || route.state !== "stale")
|
|
2780
|
+
continue;
|
|
2781
|
+
const observation = observations.find(({ adapter, snapshot }) => snapshot.complete &&
|
|
2782
|
+
adapter.identity.hostId === route.binding.hostId);
|
|
2783
|
+
if (observation === undefined)
|
|
2784
|
+
continue;
|
|
2785
|
+
const peers = observation.snapshot.peers.filter((peer) => peer.kind === "interactive" &&
|
|
2786
|
+
PUBLIC_ALIAS.test(peer.alias) &&
|
|
2787
|
+
peer.alias.endsWith(`@${observation.adapter.identity.hostId}`));
|
|
2788
|
+
const candidates = peers.filter((peer) => peer.routeHandle === route.binding.routeHandle);
|
|
2789
|
+
const peer = candidates[0];
|
|
2790
|
+
if (candidates.length !== 1 ||
|
|
2791
|
+
peer === undefined ||
|
|
2792
|
+
peers.filter((item) => item.alias === peer.alias).length !== 1) {
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
const candidate = { ...peer, adapter: observation.adapter };
|
|
2796
|
+
try {
|
|
2797
|
+
this.validatePersistedClaudeCandidate(candidate, route);
|
|
2798
|
+
await this.assertClaudeWorkspaceDisjoint(candidate.adapter, candidate.routeHandle);
|
|
2799
|
+
const selected = await candidate.adapter.selectRoute({
|
|
2800
|
+
alias: candidate.alias,
|
|
2801
|
+
routeHandle: candidate.routeHandle,
|
|
2802
|
+
});
|
|
2803
|
+
if (selected.routeHandle !== candidate.routeHandle) {
|
|
2804
|
+
throw new BridgeError("ROUTE_MISMATCH", "The peer identity changed during selection.");
|
|
2805
|
+
}
|
|
2806
|
+
preparedRebinds.push({
|
|
2807
|
+
candidate,
|
|
2808
|
+
persisted: route,
|
|
2809
|
+
selectedState: selected.state,
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
catch {
|
|
2813
|
+
await candidate.adapter.releaseRoute?.(candidate.routeHandle).catch(() => undefined);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
const consumed = new Set();
|
|
2817
|
+
try {
|
|
2818
|
+
await this.mutex.run("service", async () => {
|
|
2819
|
+
if (this.closing || !this.running)
|
|
2820
|
+
return;
|
|
2821
|
+
const changed = await this.refreshClaudeDiscovery({
|
|
2822
|
+
incrementRevision: false,
|
|
2823
|
+
observations,
|
|
2824
|
+
preparedRebinds,
|
|
2825
|
+
consumedPreparedRebinds: consumed,
|
|
2826
|
+
});
|
|
2827
|
+
if (changed) {
|
|
2828
|
+
this.revision += 1;
|
|
2829
|
+
await this.publish();
|
|
2830
|
+
}
|
|
2831
|
+
this.scheduleLifecycleWakeLocked();
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
finally {
|
|
2835
|
+
const releasable = await this.mutex.run("service", async () => preparedRebinds.filter((prepared) => {
|
|
2836
|
+
if (consumed.has(prepared))
|
|
2837
|
+
return false;
|
|
2838
|
+
const binding = {
|
|
2839
|
+
...prepared.candidate.adapter.identity,
|
|
2840
|
+
routeHandle: prepared.candidate.routeHandle,
|
|
2841
|
+
ownerLease: stableLease("claude", prepared.candidate.routeHandle),
|
|
2842
|
+
};
|
|
2843
|
+
const alias = this.bindingAliases.get(bindingKey(binding));
|
|
2844
|
+
return alias === undefined || this.routeStates.get(alias) === "stale";
|
|
2845
|
+
}));
|
|
2846
|
+
await Promise.allSettled(releasable.map(async (prepared) => await prepared.candidate.adapter.releaseRoute?.(prepared.candidate.routeHandle)));
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
async refreshClaudeDiscovery(options = {}) {
|
|
2786
2850
|
const publicBefore = await this.discoveryPublicFingerprint();
|
|
2787
2851
|
this.candidates.clear();
|
|
2788
2852
|
const rowsByAlias = new Map();
|
|
@@ -2794,14 +2858,33 @@ export class GatewayService {
|
|
|
2794
2858
|
peers: [],
|
|
2795
2859
|
complete: false,
|
|
2796
2860
|
};
|
|
2861
|
+
let discoverySucceeded = false;
|
|
2797
2862
|
try {
|
|
2798
|
-
|
|
2863
|
+
const observed = options.observations?.find((item) => item.adapter === adapter);
|
|
2864
|
+
if (observed !== undefined) {
|
|
2865
|
+
discovered = observed.snapshot;
|
|
2866
|
+
discoverySucceeded = true;
|
|
2867
|
+
}
|
|
2868
|
+
else if (options.observations === undefined &&
|
|
2869
|
+
adapter.discoverClaudePeers !== undefined) {
|
|
2870
|
+
discovered = await adapter.discoverClaudePeers();
|
|
2871
|
+
discoverySucceeded = true;
|
|
2872
|
+
}
|
|
2799
2873
|
}
|
|
2800
2874
|
catch {
|
|
2801
2875
|
// Registry observation is dashboard evidence, not Claude mailbox
|
|
2802
2876
|
// authority. The provider still revalidates the exact target before
|
|
2803
2877
|
// any write.
|
|
2804
2878
|
}
|
|
2879
|
+
if (discoverySucceeded && discovered.complete) {
|
|
2880
|
+
await this.store.observeConnector({
|
|
2881
|
+
identity: adapter.identity,
|
|
2882
|
+
health: "healthy",
|
|
2883
|
+
protocol: adapter.protocol,
|
|
2884
|
+
protocolVersion: adapter.protocolVersion,
|
|
2885
|
+
});
|
|
2886
|
+
this.recordConnectorFreshness(adapter.identity, "healthy");
|
|
2887
|
+
}
|
|
2805
2888
|
this.captureRegistryObservation(adapter, discovered.registry ?? adapter.latestRegistryObservation?.());
|
|
2806
2889
|
const grouped = new Map();
|
|
2807
2890
|
const byHandle = new Map();
|
|
@@ -2899,7 +2982,6 @@ export class GatewayService {
|
|
|
2899
2982
|
for (const route of persisted) {
|
|
2900
2983
|
if (!route.enabled ||
|
|
2901
2984
|
route.state !== "stale" ||
|
|
2902
|
-
route.compatibility !== "expired" ||
|
|
2903
2985
|
!CLAUDE_SESSION_ID.test(route.binding.routeHandle) ||
|
|
2904
2986
|
route.binding.ownerLease !==
|
|
2905
2987
|
stableLease("claude", route.binding.routeHandle)) {
|
|
@@ -2913,7 +2995,21 @@ export class GatewayService {
|
|
|
2913
2995
|
const candidate = matches[0];
|
|
2914
2996
|
if (candidate === undefined)
|
|
2915
2997
|
continue;
|
|
2916
|
-
|
|
2998
|
+
const prepared = options.preparedRebinds?.find((item) => item.candidate.adapter === candidate.adapter &&
|
|
2999
|
+
item.candidate.routeHandle === candidate.routeHandle &&
|
|
3000
|
+
item.persisted.binding.ownerLease === route.binding.ownerLease);
|
|
3001
|
+
const activated = prepared === undefined
|
|
3002
|
+
? options.observations === undefined &&
|
|
3003
|
+
await this.activatePersistedClaudeCandidate(candidate, route, "peer_identity_reobserved").then(() => true, () => false)
|
|
3004
|
+
: await this.commitPreparedClaudeRebind(prepared, "peer_identity_reobserved").then(() => {
|
|
3005
|
+
options.consumedPreparedRebinds?.add(prepared);
|
|
3006
|
+
return true;
|
|
3007
|
+
}, () => false);
|
|
3008
|
+
if (activated &&
|
|
3009
|
+
this.running &&
|
|
3010
|
+
this.routeStates.get(candidate.alias) === "idle") {
|
|
3011
|
+
this.scheduleDispatch(candidate.alias);
|
|
3012
|
+
}
|
|
2917
3013
|
}
|
|
2918
3014
|
const rows = [];
|
|
2919
3015
|
for (const [alias, row] of rowsByAlias) {
|
|
@@ -2923,8 +3019,7 @@ export class GatewayService {
|
|
|
2923
3019
|
alias,
|
|
2924
3020
|
provider: "claude",
|
|
2925
3021
|
host: row.adapter.identity.hostId,
|
|
2926
|
-
state: "
|
|
2927
|
-
compatibility: "incompatible",
|
|
3022
|
+
state: "offline",
|
|
2928
3023
|
validated: false,
|
|
2929
3024
|
selected: false,
|
|
2930
3025
|
safeErrorCode: row.safeErrorCode,
|
|
@@ -2942,7 +3037,6 @@ export class GatewayService {
|
|
|
2942
3037
|
provider: "claude",
|
|
2943
3038
|
host: row.adapter.identity.hostId,
|
|
2944
3039
|
state: row.peer.state,
|
|
2945
|
-
compatibility: row.peer.compatibility,
|
|
2946
3040
|
validated: true,
|
|
2947
3041
|
selected,
|
|
2948
3042
|
});
|
|
@@ -2951,7 +3045,6 @@ export class GatewayService {
|
|
|
2951
3045
|
await this.store.observeRoute({
|
|
2952
3046
|
binding: selectedBinding,
|
|
2953
3047
|
state: row.peer.state,
|
|
2954
|
-
compatibility: "compatible",
|
|
2955
3048
|
});
|
|
2956
3049
|
}
|
|
2957
3050
|
}
|
|
@@ -2966,7 +3059,7 @@ export class GatewayService {
|
|
|
2966
3059
|
const inspection = await this.store.inspectPrivateRoute(alias);
|
|
2967
3060
|
if (inspection !== undefined &&
|
|
2968
3061
|
inspection.enabled &&
|
|
2969
|
-
inspection.
|
|
3062
|
+
inspection.state !== "stale") {
|
|
2970
3063
|
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
2971
3064
|
const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(alias, {
|
|
2972
3065
|
unwrittenOutcome: "failed",
|
|
@@ -2986,7 +3079,7 @@ export class GatewayService {
|
|
|
2986
3079
|
}
|
|
2987
3080
|
this.availablePeers = rows.sort((left, right) => left.alias.localeCompare(right.alias));
|
|
2988
3081
|
const changed = publicBefore !== (await this.discoveryPublicFingerprint());
|
|
2989
|
-
if (changed)
|
|
3082
|
+
if (changed && options.incrementRevision !== false)
|
|
2990
3083
|
this.revision += 1;
|
|
2991
3084
|
return changed;
|
|
2992
3085
|
}
|
|
@@ -3040,17 +3133,7 @@ export class GatewayService {
|
|
|
3040
3133
|
return this.candidates.get(selector);
|
|
3041
3134
|
}
|
|
3042
3135
|
async activatePersistedClaudeCandidate(candidate, persisted, reason) {
|
|
3043
|
-
|
|
3044
|
-
if (!persisted.enabled ||
|
|
3045
|
-
persisted.state !== "stale" ||
|
|
3046
|
-
persisted.compatibility !== "expired" ||
|
|
3047
|
-
persisted.binding.provider !== "claude" ||
|
|
3048
|
-
candidate.adapter.identity.provider !== "claude" ||
|
|
3049
|
-
persisted.binding.hostId !== candidate.adapter.identity.hostId ||
|
|
3050
|
-
persisted.binding.routeHandle !== candidate.routeHandle ||
|
|
3051
|
-
persisted.binding.ownerLease !== expectedLease) {
|
|
3052
|
-
throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "The persisted Claude selection does not match the exact live session identity.");
|
|
3053
|
-
}
|
|
3136
|
+
this.validatePersistedClaudeCandidate(candidate, persisted);
|
|
3054
3137
|
await this.assertClaudeWorkspaceDisjoint(candidate.adapter, candidate.routeHandle);
|
|
3055
3138
|
try {
|
|
3056
3139
|
const selected = await candidate.adapter.selectRoute({
|
|
@@ -3060,23 +3143,7 @@ export class GatewayService {
|
|
|
3060
3143
|
if (selected.routeHandle !== candidate.routeHandle) {
|
|
3061
3144
|
throw new BridgeError("ROUTE_MISMATCH", "The peer identity changed during selection.");
|
|
3062
3145
|
}
|
|
3063
|
-
|
|
3064
|
-
...candidate.adapter.identity,
|
|
3065
|
-
routeHandle: candidate.routeHandle,
|
|
3066
|
-
ownerLease: expectedLease,
|
|
3067
|
-
};
|
|
3068
|
-
await this.store.rebindStaleRoute({
|
|
3069
|
-
alias: persisted.alias,
|
|
3070
|
-
newAlias: candidate.alias,
|
|
3071
|
-
currentOwnerLease: persisted.binding.ownerLease,
|
|
3072
|
-
newBinding: binding,
|
|
3073
|
-
reason,
|
|
3074
|
-
state: selected.state,
|
|
3075
|
-
});
|
|
3076
|
-
if (persisted.alias !== candidate.alias) {
|
|
3077
|
-
this.forgetBinding(persisted.alias);
|
|
3078
|
-
}
|
|
3079
|
-
this.rememberBinding(candidate.alias, binding, selected.state);
|
|
3146
|
+
await this.commitPreparedClaudeRebind({ candidate, persisted, selectedState: selected.state }, reason);
|
|
3080
3147
|
}
|
|
3081
3148
|
catch (error) {
|
|
3082
3149
|
await candidate.adapter.releaseRoute?.(candidate.routeHandle).catch(() => {
|
|
@@ -3085,6 +3152,44 @@ export class GatewayService {
|
|
|
3085
3152
|
throw error;
|
|
3086
3153
|
}
|
|
3087
3154
|
}
|
|
3155
|
+
validatePersistedClaudeCandidate(candidate, persisted) {
|
|
3156
|
+
const expectedLease = stableLease("claude", candidate.routeHandle);
|
|
3157
|
+
if (!persisted.enabled ||
|
|
3158
|
+
persisted.state !== "stale" ||
|
|
3159
|
+
persisted.binding.provider !== "claude" ||
|
|
3160
|
+
candidate.adapter.identity.provider !== "claude" ||
|
|
3161
|
+
persisted.binding.hostId !== candidate.adapter.identity.hostId ||
|
|
3162
|
+
persisted.binding.routeHandle !== candidate.routeHandle ||
|
|
3163
|
+
persisted.binding.ownerLease !== expectedLease) {
|
|
3164
|
+
throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "The persisted Claude selection does not match the exact live session identity.");
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
async commitPreparedClaudeRebind(prepared, reason) {
|
|
3168
|
+
const { candidate, selectedState } = prepared;
|
|
3169
|
+
const persisted = (await this.store.inspectPrivateRoute(prepared.persisted.alias)) ??
|
|
3170
|
+
(await this.store.inspectPrivateRoute(candidate.alias));
|
|
3171
|
+
if (persisted === undefined) {
|
|
3172
|
+
throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "The persisted Claude selection changed before re-observation committed.");
|
|
3173
|
+
}
|
|
3174
|
+
this.validatePersistedClaudeCandidate(candidate, persisted);
|
|
3175
|
+
const binding = {
|
|
3176
|
+
...candidate.adapter.identity,
|
|
3177
|
+
routeHandle: candidate.routeHandle,
|
|
3178
|
+
ownerLease: stableLease("claude", candidate.routeHandle),
|
|
3179
|
+
};
|
|
3180
|
+
await this.store.rebindStaleRoute({
|
|
3181
|
+
alias: persisted.alias,
|
|
3182
|
+
newAlias: candidate.alias,
|
|
3183
|
+
currentOwnerLease: persisted.binding.ownerLease,
|
|
3184
|
+
newBinding: binding,
|
|
3185
|
+
reason,
|
|
3186
|
+
state: selectedState,
|
|
3187
|
+
});
|
|
3188
|
+
if (persisted.alias !== candidate.alias) {
|
|
3189
|
+
this.forgetBinding(persisted.alias);
|
|
3190
|
+
}
|
|
3191
|
+
this.rememberBinding(candidate.alias, binding, selectedState);
|
|
3192
|
+
}
|
|
3088
3193
|
async selectClaudeCandidate(candidate, persisted) {
|
|
3089
3194
|
if (persisted !== undefined) {
|
|
3090
3195
|
await this.activatePersistedClaudeCandidate(candidate, persisted, "peer_explicitly_reselected");
|
|
@@ -3109,7 +3214,6 @@ export class GatewayService {
|
|
|
3109
3214
|
binding,
|
|
3110
3215
|
registrationMode: "selected_live_peer",
|
|
3111
3216
|
state: selected.state,
|
|
3112
|
-
compatibility: "compatible",
|
|
3113
3217
|
});
|
|
3114
3218
|
this.rememberBinding(candidate.alias, binding, selected.state);
|
|
3115
3219
|
}
|
|
@@ -3161,7 +3265,6 @@ export class GatewayService {
|
|
|
3161
3265
|
binding,
|
|
3162
3266
|
registrationMode: "selected_live_peer",
|
|
3163
3267
|
state: selectedState,
|
|
3164
|
-
compatibility: "compatible",
|
|
3165
3268
|
},
|
|
3166
3269
|
inFlightSettlements,
|
|
3167
3270
|
});
|
|
@@ -3234,47 +3337,83 @@ export class GatewayService {
|
|
|
3234
3337
|
}
|
|
3235
3338
|
return candidate.alias;
|
|
3236
3339
|
}
|
|
3237
|
-
|
|
3238
|
-
const
|
|
3340
|
+
assertPairThreadAttestation(params) {
|
|
3341
|
+
const attestation = pairParamThreadAttestation(params);
|
|
3342
|
+
const legacyAlias = "codexAlias" in params ? params.codexAlias : undefined;
|
|
3343
|
+
if (attestation === undefined && legacyAlias === undefined)
|
|
3344
|
+
return;
|
|
3345
|
+
const binding = this.routeBindings.get(attestation?.alias ?? legacyAlias);
|
|
3239
3346
|
if (binding?.provider !== "codex" ||
|
|
3240
|
-
(
|
|
3241
|
-
binding.routeHandle !== params.codexThreadId)) {
|
|
3347
|
+
(attestation !== undefined && binding.routeHandle !== attestation.threadId)) {
|
|
3242
3348
|
throw new BridgeError("CODEX_CALLER_MISMATCH", "The inherited Codex task does not own the requested pair endpoint.");
|
|
3243
3349
|
}
|
|
3244
3350
|
}
|
|
3245
3351
|
async pairRoutes(params) {
|
|
3246
|
-
this.
|
|
3247
|
-
|
|
3352
|
+
this.assertPairThreadAttestation(params);
|
|
3353
|
+
if ("claudeAlias" in params) {
|
|
3354
|
+
await this.selectAndPairClaude(params.claudeAlias, params.codexAlias);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
await this.assertLiveConsentEndpoints(params.aliases);
|
|
3358
|
+
const added = await this.store.addConsentEdge({ aliases: params.aliases });
|
|
3359
|
+
if (added.created)
|
|
3360
|
+
await this.changed();
|
|
3361
|
+
else
|
|
3362
|
+
await this.publish();
|
|
3248
3363
|
}
|
|
3249
3364
|
async unpairRoutes(params) {
|
|
3250
|
-
this.
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3365
|
+
this.assertPairThreadAttestation(params);
|
|
3366
|
+
const aliases = pairParamAliases(params);
|
|
3367
|
+
if (!("claudeAlias" in params))
|
|
3368
|
+
await this.assertLiveConsentEndpoints(aliases);
|
|
3369
|
+
await this.removeConsentEdge(aliases);
|
|
3370
|
+
}
|
|
3371
|
+
async assertLiveConsentEndpoints(aliases) {
|
|
3372
|
+
const routes = await Promise.all(aliases.map((alias) => this.store.inspectPrivateRoute(alias)));
|
|
3373
|
+
if (routes[0] === undefined || routes[1] === undefined) {
|
|
3374
|
+
throw new BridgeError("CONSENT_EDGE_ROUTE_NOT_FOUND", "Both consent endpoints must be registered.");
|
|
3375
|
+
}
|
|
3376
|
+
for (const route of routes) {
|
|
3377
|
+
const live = this.routeBindings.get(route.alias);
|
|
3378
|
+
if (!route.enabled ||
|
|
3379
|
+
route.state === "stale" ||
|
|
3380
|
+
live === undefined ||
|
|
3381
|
+
bindingKey(live) !== bindingKey(route.binding) ||
|
|
3382
|
+
this.adapter(route.binding.provider, route.binding.hostId).identity.endpointGeneration !==
|
|
3383
|
+
route.binding.endpointGeneration) {
|
|
3384
|
+
throw new BridgeError("ROUTE_UNAVAILABLE", "Both consent endpoints must be exact positively observed live routes.");
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
if (routes[0].binding.provider === routes[1].binding.provider || routes[0].binding.hostId !== routes[1].binding.hostId) {
|
|
3388
|
+
throw new BridgeError("INVALID_CONSENT_EDGE", "A consent edge must connect distinct providers on the same host.");
|
|
3389
|
+
}
|
|
3390
|
+
return routes;
|
|
3253
3391
|
}
|
|
3254
|
-
async
|
|
3392
|
+
async removeConsentEdge(aliases) {
|
|
3255
3393
|
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
3256
|
-
const
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
};
|
|
3260
|
-
const inFlightSettlements = await this.planPairInFlightSettlementsLocked(pair, {
|
|
3394
|
+
const routes = await Promise.all(aliases.map((alias) => this.store.inspectPrivateRoute(alias)));
|
|
3395
|
+
const edge = { aliases };
|
|
3396
|
+
const inFlightSettlements = await this.planConsentEdgeInFlightSettlementsLocked(edge, {
|
|
3261
3397
|
unwrittenOutcome: "cancelled",
|
|
3262
3398
|
safeErrorCode: "PAIR_REMOVED",
|
|
3263
3399
|
});
|
|
3264
|
-
const result = await this.store.
|
|
3265
|
-
...
|
|
3400
|
+
const result = await this.store.removeConsentEdge({
|
|
3401
|
+
...edge,
|
|
3266
3402
|
inFlightSettlements,
|
|
3267
3403
|
});
|
|
3268
3404
|
for (const settlement of result.settlements) {
|
|
3269
3405
|
await this.applyTerminalSettlementLocked(settlement);
|
|
3270
3406
|
}
|
|
3271
|
-
this.
|
|
3272
|
-
|
|
3273
|
-
const
|
|
3407
|
+
this.purgeConsentEdgeCapabilitiesLocked(aliases);
|
|
3408
|
+
for (const alias of result.unreferencedAliases) {
|
|
3409
|
+
const selected = routes.find((route) => route?.alias === alias);
|
|
3410
|
+
if (selected?.binding.provider !== "claude" || selected.registrationMode !== "selected_live_peer")
|
|
3411
|
+
continue;
|
|
3412
|
+
const settlements = await this.store.unregisterRoute(alias, selected.binding.ownerLease);
|
|
3274
3413
|
for (const settlement of settlements) {
|
|
3275
3414
|
await this.applyTerminalSettlementLocked(settlement);
|
|
3276
3415
|
}
|
|
3277
|
-
this.forgetBinding(
|
|
3416
|
+
this.forgetBinding(alias);
|
|
3278
3417
|
await this.adapters
|
|
3279
3418
|
.find((adapter) => adapter.identity.provider === "claude" &&
|
|
3280
3419
|
adapter.identity.hostId === selected.binding.hostId)
|
|
@@ -3282,7 +3421,7 @@ export class GatewayService {
|
|
|
3282
3421
|
.catch(() => {
|
|
3283
3422
|
this.dashboardHealthy = false;
|
|
3284
3423
|
});
|
|
3285
|
-
this.availablePeers = this.availablePeers.map((peer) => peer.alias ===
|
|
3424
|
+
this.availablePeers = this.availablePeers.map((peer) => peer.alias === alias ? { ...peer, selected: false } : peer);
|
|
3286
3425
|
}
|
|
3287
3426
|
await this.changed();
|
|
3288
3427
|
}
|
|
@@ -3291,17 +3430,6 @@ export class GatewayService {
|
|
|
3291
3430
|
}
|
|
3292
3431
|
async selectAndPairClaude(selector, codexAlias) {
|
|
3293
3432
|
const codexBinding = this.routeBindings.get(codexAlias);
|
|
3294
|
-
const retainedCodexRoute = codexBinding?.provider === "codex"
|
|
3295
|
-
? undefined
|
|
3296
|
-
: await this.store.inspectPrivateRoute(codexAlias);
|
|
3297
|
-
const codexHost = codexBinding?.provider === "codex"
|
|
3298
|
-
? codexBinding.hostId
|
|
3299
|
-
: retainedCodexRoute?.binding.provider === "codex"
|
|
3300
|
-
? retainedCodexRoute.binding.hostId
|
|
3301
|
-
: undefined;
|
|
3302
|
-
if (codexHost !== undefined) {
|
|
3303
|
-
this.assertCrossProviderMutationCompatible(codexHost);
|
|
3304
|
-
}
|
|
3305
3433
|
const codexAdapter = codexBinding?.provider === "codex"
|
|
3306
3434
|
? this.adapters.find((adapter) => adapter.identity.provider === "codex" &&
|
|
3307
3435
|
adapter.identity.hostId === codexBinding.hostId)
|
|
@@ -3341,10 +3469,7 @@ export class GatewayService {
|
|
|
3341
3469
|
}
|
|
3342
3470
|
let paired;
|
|
3343
3471
|
try {
|
|
3344
|
-
paired = await this.store.
|
|
3345
|
-
claudeAlias: candidate.alias,
|
|
3346
|
-
codexAlias,
|
|
3347
|
-
});
|
|
3472
|
+
paired = await this.store.addConsentEdge({ aliases: [candidate.alias, codexAlias] });
|
|
3348
3473
|
}
|
|
3349
3474
|
catch (error) {
|
|
3350
3475
|
if (!alreadyLive &&
|
|
@@ -3384,7 +3509,7 @@ export class GatewayService {
|
|
|
3384
3509
|
async unselectClaude(params) {
|
|
3385
3510
|
const codexAlias = await this.inferCodexAlias(params.codexThreadId);
|
|
3386
3511
|
const selected = await this.selectedClaudeRoute(params.alias);
|
|
3387
|
-
await this.
|
|
3512
|
+
await this.removeConsentEdge([selected.alias, codexAlias]);
|
|
3388
3513
|
}
|
|
3389
3514
|
async selectedClaudeRoute(selector) {
|
|
3390
3515
|
const persisted = await this.store.inspectPrivateClaudeRoutes();
|
|
@@ -3396,14 +3521,13 @@ export class GatewayService {
|
|
|
3396
3521
|
throw new BridgeError("PEER_NOT_FOUND", "No selected Claude session matches that selector.");
|
|
3397
3522
|
return selected;
|
|
3398
3523
|
}
|
|
3399
|
-
async registerOrRebind(alias, binding, reason, state, currentOwnerLease = binding.ownerLease) {
|
|
3524
|
+
async registerOrRebind(alias, binding, reason, state, currentOwnerLease = binding.ownerLease, registrationMode = binding.provider === "codex" ? "explicit_opt_in" : "selected_live_peer") {
|
|
3400
3525
|
try {
|
|
3401
3526
|
await this.store.registerRoute({
|
|
3402
3527
|
alias,
|
|
3403
3528
|
binding,
|
|
3404
|
-
registrationMode
|
|
3529
|
+
registrationMode,
|
|
3405
3530
|
state,
|
|
3406
|
-
compatibility: "compatible",
|
|
3407
3531
|
});
|
|
3408
3532
|
}
|
|
3409
3533
|
catch (error) {
|
|
@@ -3473,14 +3597,13 @@ export class GatewayService {
|
|
|
3473
3597
|
await this.store.observeConnector({
|
|
3474
3598
|
identity: adapter.identity,
|
|
3475
3599
|
health: "healthy",
|
|
3476
|
-
compatibility: "compatible",
|
|
3477
3600
|
protocol: adapter.protocol,
|
|
3478
3601
|
protocolVersion: adapter.protocolVersion,
|
|
3479
3602
|
});
|
|
3603
|
+
this.recordConnectorFreshness(adapter.identity, "healthy");
|
|
3480
3604
|
await this.store.observeRoute({
|
|
3481
3605
|
binding,
|
|
3482
3606
|
state,
|
|
3483
|
-
compatibility: "compatible",
|
|
3484
3607
|
});
|
|
3485
3608
|
return binding;
|
|
3486
3609
|
}
|
|
@@ -3783,7 +3906,15 @@ export class GatewayService {
|
|
|
3783
3906
|
unwrittenOutcome: input.unwrittenOutcome,
|
|
3784
3907
|
safeErrorCode: input.safeErrorCode,
|
|
3785
3908
|
};
|
|
3786
|
-
const
|
|
3909
|
+
const stateAtTermination = tracker.pendingTerminalEvent === undefined &&
|
|
3910
|
+
this.providerDispatchInFlightMessageIds.has(messageId)
|
|
3911
|
+
? transitionDelivery(tracker.machine, {
|
|
3912
|
+
type: "dispatch_ambiguous",
|
|
3913
|
+
at: observedAt,
|
|
3914
|
+
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
3915
|
+
}).state
|
|
3916
|
+
: tracker.machine;
|
|
3917
|
+
const transition = transitionDelivery(stateAtTermination, event);
|
|
3787
3918
|
const settlement = transition.effects.find((effect) => effect.type === "settle_delivery");
|
|
3788
3919
|
if (settlement === undefined) {
|
|
3789
3920
|
throw new BridgeError("ROUTE_TERMINATION_STATE_MISMATCH", "An affected in-flight delivery could not produce an exact terminal route settlement.", true);
|
|
@@ -3796,8 +3927,8 @@ export class GatewayService {
|
|
|
3796
3927
|
};
|
|
3797
3928
|
});
|
|
3798
3929
|
}
|
|
3799
|
-
async
|
|
3800
|
-
const affected = await this.store.
|
|
3930
|
+
async planConsentEdgeInFlightSettlementsLocked(edge, input) {
|
|
3931
|
+
const affected = await this.store.inspectAffectedConsentEdgeInFlightMessages(edge);
|
|
3801
3932
|
const observedAt = this.now().getTime();
|
|
3802
3933
|
return affected.map(({ messageId }) => {
|
|
3803
3934
|
const tracker = this.deliveryTrackers.get(messageId);
|
|
@@ -3815,7 +3946,15 @@ export class GatewayService {
|
|
|
3815
3946
|
unwrittenOutcome: input.unwrittenOutcome,
|
|
3816
3947
|
safeErrorCode: input.safeErrorCode,
|
|
3817
3948
|
};
|
|
3818
|
-
const
|
|
3949
|
+
const stateAtTermination = tracker.pendingTerminalEvent === undefined &&
|
|
3950
|
+
this.providerDispatchInFlightMessageIds.has(messageId)
|
|
3951
|
+
? transitionDelivery(tracker.machine, {
|
|
3952
|
+
type: "dispatch_ambiguous",
|
|
3953
|
+
at: observedAt,
|
|
3954
|
+
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
3955
|
+
}).state
|
|
3956
|
+
: tracker.machine;
|
|
3957
|
+
const transition = transitionDelivery(stateAtTermination, event);
|
|
3819
3958
|
const settlement = transition.effects.find((effect) => effect.type === "settle_delivery");
|
|
3820
3959
|
if (settlement === undefined) {
|
|
3821
3960
|
throw new BridgeError("PAIR_TERMINATION_STATE_MISMATCH", "An affected in-flight delivery could not produce an exact terminal pair settlement.", true);
|
|
@@ -3828,9 +3967,8 @@ export class GatewayService {
|
|
|
3828
3967
|
};
|
|
3829
3968
|
});
|
|
3830
3969
|
}
|
|
3831
|
-
|
|
3832
|
-
const matches = (sourceAlias, targetAlias) => (sourceAlias
|
|
3833
|
-
(sourceAlias === pair.codexAlias && targetAlias === pair.claudeAlias);
|
|
3970
|
+
purgeConsentEdgeCapabilitiesLocked(aliases) {
|
|
3971
|
+
const matches = (sourceAlias, targetAlias) => aliases.includes(sourceAlias) && aliases.includes(targetAlias);
|
|
3834
3972
|
const conversationIds = new Set();
|
|
3835
3973
|
for (const [conversationId, conversation] of this.conversations) {
|
|
3836
3974
|
if (conversation.pair !== true ||
|
|
@@ -4181,6 +4319,17 @@ export class GatewayService {
|
|
|
4181
4319
|
async processLifecycleLocked() {
|
|
4182
4320
|
const now = this.now().getTime();
|
|
4183
4321
|
let changed = false;
|
|
4322
|
+
if (this.nextClaudeDiscoveryAt !== undefined &&
|
|
4323
|
+
this.nextClaudeDiscoveryAt <= now) {
|
|
4324
|
+
this.nextClaudeDiscoveryAt = now + CLAUDE_DISCOVERY_REFRESH_MS;
|
|
4325
|
+
this.schedulePeriodicClaudeDiscovery();
|
|
4326
|
+
}
|
|
4327
|
+
for (const [key, deadlineAt] of this.connectorFreshnessDeadlineAt) {
|
|
4328
|
+
if (deadlineAt > now)
|
|
4329
|
+
continue;
|
|
4330
|
+
this.connectorFreshnessDeadlineAt.delete(key);
|
|
4331
|
+
changed = true;
|
|
4332
|
+
}
|
|
4184
4333
|
if (await this.processProgressWatchesLocked(now))
|
|
4185
4334
|
changed = true;
|
|
4186
4335
|
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
@@ -4318,6 +4467,14 @@ export class GatewayService {
|
|
|
4318
4467
|
if (this.nextProgressWatchAt !== undefined) {
|
|
4319
4468
|
consider(this.nextProgressWatchAt);
|
|
4320
4469
|
}
|
|
4470
|
+
if (this.nextClaudeDiscoveryAt !== undefined) {
|
|
4471
|
+
consider(this.nextClaudeDiscoveryAt);
|
|
4472
|
+
}
|
|
4473
|
+
for (const deadlineAt of this.connectorFreshnessDeadlineAt.values()) {
|
|
4474
|
+
consider(deadlineAt);
|
|
4475
|
+
}
|
|
4476
|
+
if (this.lifecyclePublishPending)
|
|
4477
|
+
consider(now);
|
|
4321
4478
|
if (hasNonterminal) {
|
|
4322
4479
|
this.nextDashboardRefreshAt ??= now + DELIVERY_DASHBOARD_REFRESH_MS;
|
|
4323
4480
|
consider(this.nextDashboardRefreshAt);
|
|
@@ -4333,10 +4490,21 @@ export class GatewayService {
|
|
|
4333
4490
|
this.lifecycleTimer = undefined;
|
|
4334
4491
|
void this.mutex
|
|
4335
4492
|
.run("service", async () => {
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4493
|
+
try {
|
|
4494
|
+
const changed = await this.processLifecycleLocked();
|
|
4495
|
+
if (changed) {
|
|
4496
|
+
this.revision += 1;
|
|
4497
|
+
this.lifecyclePublishPending = true;
|
|
4498
|
+
}
|
|
4499
|
+
if (this.lifecyclePublishPending) {
|
|
4500
|
+
await this.publish();
|
|
4501
|
+
this.lifecyclePublishPending = false;
|
|
4502
|
+
}
|
|
4503
|
+
this.scheduleLifecycleWakeLocked();
|
|
4504
|
+
}
|
|
4505
|
+
catch {
|
|
4506
|
+
this.dashboardHealthy = false;
|
|
4507
|
+
this.scheduleLifecycleRetryLocked();
|
|
4340
4508
|
}
|
|
4341
4509
|
})
|
|
4342
4510
|
.catch(() => {
|
|
@@ -4346,6 +4514,23 @@ export class GatewayService {
|
|
|
4346
4514
|
this.lifecycleTimer = timer;
|
|
4347
4515
|
timer.unref?.();
|
|
4348
4516
|
}
|
|
4517
|
+
scheduleLifecycleRetryLocked() {
|
|
4518
|
+
if (!this.running || this.closing || this.lifecycleTimer !== undefined) {
|
|
4519
|
+
return;
|
|
4520
|
+
}
|
|
4521
|
+
const timer = this.timers.setTimeout(() => {
|
|
4522
|
+
if (this.lifecycleTimer !== timer)
|
|
4523
|
+
return;
|
|
4524
|
+
this.lifecycleTimer = undefined;
|
|
4525
|
+
void this.mutex.run("service", async () => {
|
|
4526
|
+
this.scheduleLifecycleWakeLocked();
|
|
4527
|
+
}).catch(() => {
|
|
4528
|
+
this.dashboardHealthy = false;
|
|
4529
|
+
});
|
|
4530
|
+
}, DELIVERY_SETTLEMENT_RETRY_MS);
|
|
4531
|
+
this.lifecycleTimer = timer;
|
|
4532
|
+
timer.unref?.();
|
|
4533
|
+
}
|
|
4349
4534
|
async deliveryStatus(token) {
|
|
4350
4535
|
return await this.mutex.run("service", async () => {
|
|
4351
4536
|
if (!DELIVERY_TOKEN.test(token))
|
|
@@ -4614,12 +4799,13 @@ export class GatewayService {
|
|
|
4614
4799
|
* native-ingress boundary; this preserves the exact connector generation
|
|
4615
4800
|
* without recreating route authority or accepting a new unsolicited send.
|
|
4616
4801
|
*/
|
|
4617
|
-
async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text) {
|
|
4802
|
+
async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, expectedTargetBinding, text) {
|
|
4618
4803
|
const target = await this.store.resolveRoute(conversation.sourceAlias);
|
|
4619
4804
|
if (sourceBinding.provider !== "claude" ||
|
|
4620
|
-
target.provider
|
|
4805
|
+
target.provider === "claude" ||
|
|
4806
|
+
target.provider !== expectedTargetBinding.provider || target.ownerLease !== expectedTargetBinding.ownerLease ||
|
|
4621
4807
|
sourceBinding.hostId !== target.hostId) {
|
|
4622
|
-
throw new BridgeError("RECOVERED_REPLY_ROUTE_MISMATCH", "The retained provider reply no longer matches an exact same-host
|
|
4808
|
+
throw new BridgeError("RECOVERED_REPLY_ROUTE_MISMATCH", "The retained provider reply no longer matches an exact same-host non-Claude target.");
|
|
4623
4809
|
}
|
|
4624
4810
|
const sequence = conversation.nextSequence;
|
|
4625
4811
|
const enqueuedAt = this.now().getTime();
|
|
@@ -4650,6 +4836,7 @@ export class GatewayService {
|
|
|
4650
4836
|
expectsReply: false,
|
|
4651
4837
|
sequence,
|
|
4652
4838
|
targetBindingKey: bindingKey(target),
|
|
4839
|
+
sourceBinding: { ...sourceBinding },
|
|
4653
4840
|
authorization: "selected_route",
|
|
4654
4841
|
targetAlias: conversation.sourceAlias,
|
|
4655
4842
|
deadlineAt,
|
|
@@ -4662,7 +4849,7 @@ export class GatewayService {
|
|
|
4662
4849
|
deadlineAt,
|
|
4663
4850
|
});
|
|
4664
4851
|
await this.changed();
|
|
4665
|
-
await this.
|
|
4852
|
+
await this.setNativeSourceStatus(conversation.sourceAlias, "waiting");
|
|
4666
4853
|
this.scheduleDispatch(conversation.sourceAlias);
|
|
4667
4854
|
}
|
|
4668
4855
|
pruneTransient() {
|
|
@@ -4703,7 +4890,7 @@ export class GatewayService {
|
|
|
4703
4890
|
break;
|
|
4704
4891
|
}
|
|
4705
4892
|
}
|
|
4706
|
-
async
|
|
4893
|
+
async awaitDispatchWithNativeHeld(messageId, operation) {
|
|
4707
4894
|
const outcome = operation.then((value) => ({ kind: "result", value }), (error) => ({ kind: "error", error }));
|
|
4708
4895
|
const unwrap = (settled) => {
|
|
4709
4896
|
if (settled.kind === "error")
|
|
@@ -4711,7 +4898,7 @@ export class GatewayService {
|
|
|
4711
4898
|
return settled.value;
|
|
4712
4899
|
};
|
|
4713
4900
|
while (true) {
|
|
4714
|
-
const current = this.deliveryTrackers.get(
|
|
4901
|
+
const current = this.deliveryTrackers.get(messageId);
|
|
4715
4902
|
if (current === undefined ||
|
|
4716
4903
|
current.nativeReceipt === undefined ||
|
|
4717
4904
|
current.nativeHeldSent ||
|
|
@@ -4736,14 +4923,15 @@ export class GatewayService {
|
|
|
4736
4923
|
}
|
|
4737
4924
|
// A terminal callback may have arrived before the provider's dispatch
|
|
4738
4925
|
// promise resolved. Let that exact evidence win before emitting held.
|
|
4739
|
-
await this.
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4926
|
+
await this.mutex.run("service", async () => {
|
|
4927
|
+
await this.drainDeliveryCallbacksForMessageLocked(messageId);
|
|
4928
|
+
const afterCallbacks = this.deliveryTrackers.get(messageId);
|
|
4929
|
+
if (afterCallbacks !== undefined &&
|
|
4930
|
+
!isTerminalDeliveryMachine(afterCallbacks.machine) &&
|
|
4931
|
+
this.now().getTime() < afterCallbacks.deadlineAt) {
|
|
4932
|
+
await this.publishNativeHeldLocked(afterCallbacks);
|
|
4933
|
+
}
|
|
4934
|
+
});
|
|
4747
4935
|
}
|
|
4748
4936
|
}
|
|
4749
4937
|
restoreQueuedMessageContextLocked(item, binding, canonicalConversationId) {
|
|
@@ -4784,7 +4972,7 @@ export class GatewayService {
|
|
|
4784
4972
|
});
|
|
4785
4973
|
return context;
|
|
4786
4974
|
}
|
|
4787
|
-
async
|
|
4975
|
+
async prepareDispatchLocked(targetAlias) {
|
|
4788
4976
|
if (!this.running ||
|
|
4789
4977
|
this.closing ||
|
|
4790
4978
|
this.codexSuccessionDispatchFrozen.has(targetAlias) ||
|
|
@@ -4882,7 +5070,6 @@ export class GatewayService {
|
|
|
4882
5070
|
!dispatchable ||
|
|
4883
5071
|
inspection === undefined ||
|
|
4884
5072
|
!inspection.enabled ||
|
|
4885
|
-
inspection.compatibility !== "compatible" ||
|
|
4886
5073
|
!storedDispatchable ||
|
|
4887
5074
|
bindingKey(inspection.binding) !== bindingKey(binding) ||
|
|
4888
5075
|
bindingKey(selectedBinding) !== bindingKey(binding)) {
|
|
@@ -4972,13 +5159,35 @@ export class GatewayService {
|
|
|
4972
5159
|
})).markerActive
|
|
4973
5160
|
: false;
|
|
4974
5161
|
}
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
5162
|
+
const sourceBinding = context.sourceBinding ??
|
|
5163
|
+
(await this.store.inspectPrivateRoute(item.sourceAlias))?.binding;
|
|
5164
|
+
const parsedDirection = parseDirection(item.direction);
|
|
5165
|
+
if (sourceBinding === undefined ||
|
|
5166
|
+
parsedDirection?.sourceProvider !== sourceBinding.provider ||
|
|
5167
|
+
parsedDirection.targetProvider !== binding.provider ||
|
|
5168
|
+
item.direction !== directionId(sourceBinding.provider, binding.provider)) {
|
|
5169
|
+
await this.finishDelivery({
|
|
5170
|
+
messageId: item.messageId,
|
|
5171
|
+
state: "failed",
|
|
5172
|
+
safeErrorCode: "MESSAGE_DIRECTION_BINDING_MISMATCH",
|
|
5173
|
+
});
|
|
5174
|
+
return;
|
|
5175
|
+
}
|
|
5176
|
+
context.sourceBinding ??= { ...sourceBinding };
|
|
5177
|
+
this.scheduleLifecycleWakeLocked();
|
|
5178
|
+
return {
|
|
5179
|
+
targetAlias,
|
|
5180
|
+
messageId: item.messageId,
|
|
5181
|
+
body: item.body,
|
|
5182
|
+
binding: { ...binding },
|
|
5183
|
+
authorization: context.authorization,
|
|
5184
|
+
steeringWhileBusy,
|
|
5185
|
+
input: {
|
|
4978
5186
|
sourceAlias: item.sourceAlias,
|
|
5187
|
+
sourceProvider: sourceBinding.provider,
|
|
4979
5188
|
targetAlias: item.targetAlias,
|
|
4980
5189
|
conversationId: context.conversationId,
|
|
4981
|
-
binding,
|
|
5190
|
+
binding: { ...binding },
|
|
4982
5191
|
authorization: context.authorization,
|
|
4983
5192
|
messageId: item.messageId,
|
|
4984
5193
|
text: item.body,
|
|
@@ -4988,21 +5197,43 @@ export class GatewayService {
|
|
|
4988
5197
|
...(item.steer === true && item.queuedAhead !== undefined
|
|
4989
5198
|
? { queuedAhead: item.queuedAhead }
|
|
4990
5199
|
: {}),
|
|
4991
|
-
...(progressWatchActive
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
5200
|
+
...(progressWatchActive ? { progressWatchActive: true } : {}),
|
|
5201
|
+
},
|
|
5202
|
+
};
|
|
5203
|
+
}
|
|
5204
|
+
async commitDispatchLocked(prepared, outcome) {
|
|
5205
|
+
const { targetAlias, messageId, body, binding, authorization, steeringWhileBusy, } = prepared;
|
|
5206
|
+
await this.drainDeliveryCallbacksForMessageLocked(messageId);
|
|
5207
|
+
if (await this.processLifecycleLocked()) {
|
|
5208
|
+
await this.changed();
|
|
4995
5209
|
}
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5210
|
+
const context = this.messageContexts.get(messageId);
|
|
5211
|
+
const tracker = this.deliveryTrackers.get(messageId);
|
|
5212
|
+
if (context === undefined || tracker === undefined) {
|
|
5213
|
+
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
5214
|
+
return;
|
|
5215
|
+
}
|
|
5216
|
+
const currentTargetAlias = this.bindingAliases.get(context.targetBindingKey) ??
|
|
5217
|
+
this.bindingAliases.get(bindingKey(binding)) ??
|
|
5218
|
+
targetAlias;
|
|
5219
|
+
const currentSelectedBinding = authorization === "selected_route"
|
|
5220
|
+
? this.routeBindings.get(currentTargetAlias)
|
|
5221
|
+
: undefined;
|
|
5222
|
+
const currentSelectedRoute = authorization === "selected_route"
|
|
5223
|
+
? await this.store.inspectPrivateRoute(currentTargetAlias)
|
|
5224
|
+
: undefined;
|
|
5225
|
+
const authorityStillCurrent = !(context.authorization !== authorization ||
|
|
5226
|
+
context.targetBindingKey !== bindingKey(binding) ||
|
|
5227
|
+
(authorization === "selected_route" &&
|
|
5228
|
+
(currentSelectedBinding === undefined ||
|
|
5229
|
+
bindingKey(currentSelectedBinding) !== bindingKey(binding) ||
|
|
5230
|
+
currentSelectedRoute === undefined ||
|
|
5231
|
+
!currentSelectedRoute.enabled ||
|
|
5232
|
+
currentSelectedRoute.state === "stale" ||
|
|
5233
|
+
bindingKey(currentSelectedRoute.binding) !== bindingKey(binding) ||
|
|
5234
|
+
this.routeStates.get(currentTargetAlias) === "stale")));
|
|
5235
|
+
if (outcome.kind === "error") {
|
|
5236
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
5006
5237
|
type: "dispatch_ambiguous",
|
|
5007
5238
|
at: this.now().getTime(),
|
|
5008
5239
|
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
@@ -5011,16 +5242,8 @@ export class GatewayService {
|
|
|
5011
5242
|
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
5012
5243
|
return;
|
|
5013
5244
|
}
|
|
5014
|
-
|
|
5015
|
-
if (await this.processLifecycleLocked()) {
|
|
5016
|
-
await this.changed();
|
|
5017
|
-
}
|
|
5018
|
-
if (!this.messageContexts.has(item.messageId)) {
|
|
5019
|
-
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
5020
|
-
return;
|
|
5021
|
-
}
|
|
5245
|
+
const result = outcome.result;
|
|
5022
5246
|
if (result.state === "deferred") {
|
|
5023
|
-
const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
|
|
5024
5247
|
const now = this.now().getTime();
|
|
5025
5248
|
const deferredSafeErrorCode = safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED");
|
|
5026
5249
|
const retryDelayMs = binding.provider === "claude"
|
|
@@ -5032,14 +5255,14 @@ export class GatewayService {
|
|
|
5032
5255
|
const retryAt = binding.provider === "claude"
|
|
5033
5256
|
? Math.min(now + retryDelayMs, tracker.deadlineAt - 1)
|
|
5034
5257
|
: now + retryDelayMs;
|
|
5035
|
-
await this.advanceDeliveryLocked(
|
|
5258
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
5036
5259
|
type: "dispatch_clean_prewrite_failed",
|
|
5037
5260
|
at: now,
|
|
5038
5261
|
retryAt,
|
|
5039
5262
|
safeErrorCode: deferredSafeErrorCode,
|
|
5040
5263
|
});
|
|
5041
5264
|
tracker.deliverySafeErrorCode = deferredSafeErrorCode;
|
|
5042
|
-
const postDeferred = this.deliveryTrackers.get(
|
|
5265
|
+
const postDeferred = this.deliveryTrackers.get(messageId)?.machine;
|
|
5043
5266
|
if (postDeferred === undefined ||
|
|
5044
5267
|
isTerminalDeliveryMachine(postDeferred)) {
|
|
5045
5268
|
await this.changed();
|
|
@@ -5050,32 +5273,33 @@ export class GatewayService {
|
|
|
5050
5273
|
// evidence. Only the reducer's explicit queued retry phase may return
|
|
5051
5274
|
// the body to the store queue.
|
|
5052
5275
|
if (postDeferred.phase !== "queued") {
|
|
5053
|
-
await this.advanceDeliveryLocked(
|
|
5276
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
5054
5277
|
type: "await_terminal",
|
|
5055
5278
|
at: this.now().getTime(),
|
|
5056
5279
|
});
|
|
5057
5280
|
await this.changed();
|
|
5058
5281
|
return;
|
|
5059
5282
|
}
|
|
5060
|
-
const requeued = await this.store.requeueInFlightMessage(
|
|
5061
|
-
if (this.activeDispatchByTarget.get(currentTargetAlias) ===
|
|
5283
|
+
const requeued = await this.store.requeueInFlightMessage(messageId, body, deferredSafeErrorCode);
|
|
5284
|
+
if (this.activeDispatchByTarget.get(currentTargetAlias) === messageId) {
|
|
5062
5285
|
this.activeDispatchByTarget.delete(currentTargetAlias);
|
|
5063
5286
|
}
|
|
5064
5287
|
if (requeued.status === "requeued") {
|
|
5065
|
-
if (context.authorization === "selected_route"
|
|
5288
|
+
if (context.authorization === "selected_route" &&
|
|
5289
|
+
authorityStillCurrent) {
|
|
5066
5290
|
if (binding.provider === "codex" && !steeringWhileBusy) {
|
|
5067
5291
|
this.routeStates.set(currentTargetAlias, "idle");
|
|
5068
5292
|
await this.store.observeRoute({
|
|
5069
5293
|
binding,
|
|
5070
5294
|
state: "idle",
|
|
5071
|
-
compatibility: "compatible",
|
|
5072
5295
|
});
|
|
5073
5296
|
}
|
|
5074
5297
|
else if (binding.provider === "codex") {
|
|
5075
5298
|
this.scheduleHeldRedispatch(currentTargetAlias);
|
|
5076
5299
|
}
|
|
5077
|
-
await this.
|
|
5300
|
+
await this.setNativeSourceStatus(currentTargetAlias, "waiting");
|
|
5078
5301
|
}
|
|
5302
|
+
await this.publishNativeHeldLocked(tracker);
|
|
5079
5303
|
}
|
|
5080
5304
|
else if (requeued.status === "settled") {
|
|
5081
5305
|
await this.applyTerminalSettlementLocked(requeued.settlement);
|
|
@@ -5083,20 +5307,30 @@ export class GatewayService {
|
|
|
5083
5307
|
await this.changed();
|
|
5084
5308
|
return;
|
|
5085
5309
|
}
|
|
5310
|
+
if (!authorityStillCurrent) {
|
|
5311
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
5312
|
+
type: "dispatch_ambiguous",
|
|
5313
|
+
at: this.now().getTime(),
|
|
5314
|
+
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
5315
|
+
});
|
|
5316
|
+
await this.changed();
|
|
5317
|
+
this.scheduleDispatch(currentTargetAlias);
|
|
5318
|
+
return;
|
|
5319
|
+
}
|
|
5086
5320
|
if (result.state === "pending" || result.state === "accepted") {
|
|
5087
5321
|
if (result.state === "accepted" &&
|
|
5088
|
-
(await this.acceptProviderDeliveryLocked(
|
|
5322
|
+
(await this.acceptProviderDeliveryLocked(messageId))) {
|
|
5089
5323
|
await this.changed();
|
|
5090
5324
|
}
|
|
5091
5325
|
else if (result.state === "pending") {
|
|
5092
|
-
await this.advanceDeliveryLocked(
|
|
5326
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
5093
5327
|
type: "await_terminal",
|
|
5094
5328
|
at: this.now().getTime(),
|
|
5095
5329
|
});
|
|
5096
5330
|
}
|
|
5097
5331
|
}
|
|
5098
5332
|
else {
|
|
5099
|
-
await this.finishDelivery({ messageId
|
|
5333
|
+
await this.finishDelivery({ messageId, state: result.state, ...(result.safeErrorCode === undefined ? {} : { safeErrorCode: result.safeErrorCode }), ...(result.replyText === undefined ? {} : { replyText: result.replyText }) });
|
|
5100
5334
|
}
|
|
5101
5335
|
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
5102
5336
|
}
|
|
@@ -5110,27 +5344,100 @@ export class GatewayService {
|
|
|
5110
5344
|
return;
|
|
5111
5345
|
this.scheduledDispatchTargets.add(targetAlias);
|
|
5112
5346
|
setImmediate(() => {
|
|
5113
|
-
this.
|
|
5114
|
-
.
|
|
5347
|
+
void this.runDispatch(targetAlias).catch(() => {
|
|
5348
|
+
this.dashboardHealthy = false;
|
|
5349
|
+
});
|
|
5350
|
+
});
|
|
5351
|
+
}
|
|
5352
|
+
async runDispatch(targetAlias) {
|
|
5353
|
+
let runnerStarted = false;
|
|
5354
|
+
let initialScheduleConsumed = false;
|
|
5355
|
+
let preparedMessageId;
|
|
5356
|
+
let commitCompleted = false;
|
|
5357
|
+
try {
|
|
5358
|
+
const prepared = await this.mutex.run("service", async () => {
|
|
5115
5359
|
this.scheduledDispatchTargets.delete(targetAlias);
|
|
5360
|
+
initialScheduleConsumed = true;
|
|
5116
5361
|
if (this.closing ||
|
|
5117
5362
|
this.codexSuccessionDispatchFrozen.has(targetAlias) ||
|
|
5118
5363
|
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
5119
5364
|
return;
|
|
5120
5365
|
}
|
|
5121
5366
|
this.dispatchRunnerTargets.add(targetAlias);
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
finally {
|
|
5367
|
+
runnerStarted = true;
|
|
5368
|
+
const candidate = await this.prepareDispatchLocked(targetAlias);
|
|
5369
|
+
if (candidate === undefined) {
|
|
5126
5370
|
this.dispatchRunnerTargets.delete(targetAlias);
|
|
5127
5371
|
this.scheduleLifecycleWakeLocked();
|
|
5372
|
+
runnerStarted = false;
|
|
5128
5373
|
}
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5374
|
+
else {
|
|
5375
|
+
preparedMessageId = candidate.messageId;
|
|
5376
|
+
this.providerDispatchInFlightMessageIds.add(candidate.messageId);
|
|
5377
|
+
}
|
|
5378
|
+
return candidate;
|
|
5132
5379
|
});
|
|
5133
|
-
|
|
5380
|
+
if (prepared === undefined)
|
|
5381
|
+
return;
|
|
5382
|
+
let outcome;
|
|
5383
|
+
try {
|
|
5384
|
+
const adapter = this.adapter(prepared.binding.provider, prepared.binding.hostId);
|
|
5385
|
+
outcome = {
|
|
5386
|
+
kind: "result",
|
|
5387
|
+
result: await this.awaitDispatchWithNativeHeld(prepared.messageId, adapter.dispatch(prepared.input)),
|
|
5388
|
+
};
|
|
5389
|
+
}
|
|
5390
|
+
catch {
|
|
5391
|
+
outcome = { kind: "error" };
|
|
5392
|
+
}
|
|
5393
|
+
await this.mutex.run("service", async () => {
|
|
5394
|
+
await this.commitDispatchLocked(prepared, outcome);
|
|
5395
|
+
commitCompleted = true;
|
|
5396
|
+
this.providerDispatchInFlightMessageIds.delete(prepared.messageId);
|
|
5397
|
+
preparedMessageId = undefined;
|
|
5398
|
+
this.dispatchRunnerTargets.delete(targetAlias);
|
|
5399
|
+
runnerStarted = false;
|
|
5400
|
+
this.scheduleLifecycleWakeLocked();
|
|
5401
|
+
});
|
|
5402
|
+
}
|
|
5403
|
+
finally {
|
|
5404
|
+
await this.mutex.run("service", async () => {
|
|
5405
|
+
if (preparedMessageId !== undefined) {
|
|
5406
|
+
let releaseUncertaintyMarker = commitCompleted;
|
|
5407
|
+
if (!commitCompleted) {
|
|
5408
|
+
const tracker = this.deliveryTrackers.get(preparedMessageId);
|
|
5409
|
+
if (tracker !== undefined &&
|
|
5410
|
+
!isTerminalDeliveryMachine(tracker.machine)) {
|
|
5411
|
+
try {
|
|
5412
|
+
await this.advanceDeliveryLocked(preparedMessageId, {
|
|
5413
|
+
type: "dispatch_ambiguous",
|
|
5414
|
+
at: this.now().getTime(),
|
|
5415
|
+
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
5416
|
+
});
|
|
5417
|
+
await this.changed();
|
|
5418
|
+
releaseUncertaintyMarker = true;
|
|
5419
|
+
}
|
|
5420
|
+
catch {
|
|
5421
|
+
this.dashboardHealthy = false;
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
else {
|
|
5425
|
+
releaseUncertaintyMarker = true;
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
if (releaseUncertaintyMarker) {
|
|
5429
|
+
this.providerDispatchInFlightMessageIds.delete(preparedMessageId);
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
if (runnerStarted) {
|
|
5433
|
+
this.dispatchRunnerTargets.delete(targetAlias);
|
|
5434
|
+
}
|
|
5435
|
+
if (!initialScheduleConsumed) {
|
|
5436
|
+
this.scheduledDispatchTargets.delete(targetAlias);
|
|
5437
|
+
}
|
|
5438
|
+
this.scheduleLifecycleWakeLocked();
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5134
5441
|
}
|
|
5135
5442
|
scheduleHeldRedispatch(targetAlias) {
|
|
5136
5443
|
if (this.closing || this.heldRedispatchTimers.has(targetAlias))
|
|
@@ -5285,13 +5592,12 @@ export class GatewayService {
|
|
|
5285
5592
|
.then(() => true)
|
|
5286
5593
|
.catch(() => false);
|
|
5287
5594
|
const pairStillActive = conversation.pair !== true ||
|
|
5288
|
-
(await this.store.
|
|
5289
|
-
|
|
5290
|
-
codexAlias: conversation.sourceAlias,
|
|
5595
|
+
(await this.store.hasConsentEdge({
|
|
5596
|
+
aliases: [conversation.targetAlias, conversation.sourceAlias],
|
|
5291
5597
|
}));
|
|
5292
5598
|
if ((!sourceStillRoutable || !pairStillActive) &&
|
|
5293
|
-
sourceBinding?.provider === "claude") {
|
|
5294
|
-
await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText);
|
|
5599
|
+
sourceBinding?.provider === "claude" && context.sourceBinding !== undefined) {
|
|
5600
|
+
await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, context.sourceBinding, replyText);
|
|
5295
5601
|
}
|
|
5296
5602
|
else {
|
|
5297
5603
|
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, {
|
|
@@ -5384,7 +5690,7 @@ export class GatewayService {
|
|
|
5384
5690
|
};
|
|
5385
5691
|
const collision = this.routeBindings.get(event.sourceAlias);
|
|
5386
5692
|
if (target === undefined ||
|
|
5387
|
-
target.provider
|
|
5693
|
+
target.provider === "claude" ||
|
|
5388
5694
|
target.hostId !== event.endpoint.hostId ||
|
|
5389
5695
|
(collision !== undefined &&
|
|
5390
5696
|
bindingKey(collision) !== bindingKey(sourceBinding))) {
|
|
@@ -5426,6 +5732,7 @@ export class GatewayService {
|
|
|
5426
5732
|
expectsReply: !steer,
|
|
5427
5733
|
sequence: 0,
|
|
5428
5734
|
targetBindingKey: bindingKey(target),
|
|
5735
|
+
sourceBinding: { ...sourceBinding },
|
|
5429
5736
|
authorization: "selected_route",
|
|
5430
5737
|
targetAlias: event.targetAlias,
|
|
5431
5738
|
deadlineAt,
|
|
@@ -5451,13 +5758,17 @@ export class GatewayService {
|
|
|
5451
5758
|
deadlineAt,
|
|
5452
5759
|
});
|
|
5453
5760
|
await this.changed();
|
|
5454
|
-
await this.
|
|
5455
|
-
//
|
|
5456
|
-
//
|
|
5457
|
-
// fast
|
|
5458
|
-
|
|
5761
|
+
await this.setNativeSourceStatus(event.targetAlias, "waiting");
|
|
5762
|
+
// Provider I/O runs outside the service mutex. The lifecycle timer emits
|
|
5763
|
+
// `held` only if this exact body remains nonterminal at its prompt
|
|
5764
|
+
// boundary; a fast provider result therefore still produces one
|
|
5765
|
+
// terminal acknowledgement and never freezes control-plane callbacks.
|
|
5766
|
+
this.scheduleDispatch(event.targetAlias);
|
|
5459
5767
|
const tracker = this.deliveryTrackers.get(queued.messageId);
|
|
5460
|
-
|
|
5768
|
+
const routeState = this.routeStates.get(event.targetAlias);
|
|
5769
|
+
if (tracker?.machine.phase === "queued" &&
|
|
5770
|
+
routeState !== "idle" &&
|
|
5771
|
+
(!steer || routeState !== "busy")) {
|
|
5461
5772
|
await this.publishNativeHeldLocked(tracker);
|
|
5462
5773
|
}
|
|
5463
5774
|
}
|
|
@@ -5521,7 +5832,7 @@ export class GatewayService {
|
|
|
5521
5832
|
return activationDeferred;
|
|
5522
5833
|
}
|
|
5523
5834
|
finally {
|
|
5524
|
-
if (!succeeded
|
|
5835
|
+
if (!succeeded) {
|
|
5525
5836
|
adapter.releaseEndpointRefreshSelectorClaim?.(event.current.endpointGeneration);
|
|
5526
5837
|
}
|
|
5527
5838
|
if (ownsActiveTransition &&
|
|
@@ -5540,56 +5851,29 @@ export class GatewayService {
|
|
|
5540
5851
|
}
|
|
5541
5852
|
}
|
|
5542
5853
|
async applyCodexEndpointRefresh(adapter, event, transitionKey, options) {
|
|
5854
|
+
const routeHandles = Array.isArray(event.routes)
|
|
5855
|
+
? event.routes.map((route) => route.routeHandle)
|
|
5856
|
+
: [];
|
|
5543
5857
|
if (this.closing ||
|
|
5544
|
-
(event.outcome !== "compatible" && event.outcome !== "incompatible") ||
|
|
5545
|
-
!isCompatibilityAttestation(event.attestation) ||
|
|
5546
|
-
event.attestation.surface !== "codex" ||
|
|
5547
5858
|
event.previous.provider !== "codex" ||
|
|
5548
5859
|
!PRIVATE_HANDLE.test(event.previous.endpointGeneration) ||
|
|
5549
5860
|
adapter.identity.provider !== "codex" ||
|
|
5550
5861
|
adapter.identity.hostId !== event.previous.hostId ||
|
|
5551
|
-
!this.config.allowedHosts.includes(event.previous.hostId)
|
|
5862
|
+
!this.config.allowedHosts.includes(event.previous.hostId) ||
|
|
5863
|
+
event.current.provider !== "codex" ||
|
|
5864
|
+
event.current.hostId !== event.previous.hostId ||
|
|
5865
|
+
!PRIVATE_HANDLE.test(event.current.endpointGeneration) ||
|
|
5866
|
+
event.current.endpointGeneration === event.previous.endpointGeneration ||
|
|
5867
|
+
adapter.identity.endpointGeneration !== event.current.endpointGeneration ||
|
|
5868
|
+
!Array.isArray(event.routes) ||
|
|
5869
|
+
event.routes.length > this.config.limits.maxRoutes ||
|
|
5870
|
+
new Set(routeHandles).size !== routeHandles.length ||
|
|
5871
|
+
event.routes.some((route) => !PRIVATE_HANDLE.test(route.routeHandle) ||
|
|
5872
|
+
!["idle", "busy", "awaiting_approval"].includes(route.state))) {
|
|
5552
5873
|
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The Codex endpoint transition failed closed runtime validation.");
|
|
5553
5874
|
}
|
|
5554
|
-
if (event.
|
|
5555
|
-
|
|
5556
|
-
? event.routes.map((route) => route.routeHandle)
|
|
5557
|
-
: [];
|
|
5558
|
-
if (event.current.provider !== "codex" ||
|
|
5559
|
-
event.current.hostId !== event.previous.hostId ||
|
|
5560
|
-
!PRIVATE_HANDLE.test(event.current.endpointGeneration) ||
|
|
5561
|
-
event.current.endpointGeneration ===
|
|
5562
|
-
event.previous.endpointGeneration ||
|
|
5563
|
-
adapter.identity.endpointGeneration !==
|
|
5564
|
-
event.current.endpointGeneration ||
|
|
5565
|
-
event.attestation.version !== adapter.protocolVersion ||
|
|
5566
|
-
event.attestation.tier === "incompatible" ||
|
|
5567
|
-
!Array.isArray(event.routes) ||
|
|
5568
|
-
event.routes.length > this.config.limits.maxRoutes ||
|
|
5569
|
-
new Set(routeHandles).size !== routeHandles.length ||
|
|
5570
|
-
event.routes.some((route) => !PRIVATE_HANDLE.test(route.routeHandle) ||
|
|
5571
|
-
!["idle", "busy", "awaiting_approval"].includes(route.state))) {
|
|
5572
|
-
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The refreshed Codex provider identity changed before activation.");
|
|
5573
|
-
}
|
|
5574
|
-
if (this.activatedEndpointRefreshes.get(event.current.hostId) ===
|
|
5575
|
-
transitionKey) {
|
|
5576
|
-
return false;
|
|
5577
|
-
}
|
|
5578
|
-
}
|
|
5579
|
-
else {
|
|
5580
|
-
if (event.candidate.provider !== "codex" ||
|
|
5581
|
-
event.candidate.hostId !== event.previous.hostId ||
|
|
5582
|
-
!PRIVATE_HANDLE.test(event.candidate.endpointGeneration) ||
|
|
5583
|
-
event.candidate.endpointGeneration ===
|
|
5584
|
-
event.previous.endpointGeneration ||
|
|
5585
|
-
adapter.identity.endpointGeneration !==
|
|
5586
|
-
event.previous.endpointGeneration ||
|
|
5587
|
-
event.attestation.tier !== "incompatible") {
|
|
5588
|
-
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The incompatible Codex candidate no longer matches the active endpoint.");
|
|
5589
|
-
}
|
|
5590
|
-
if (this.handledIncompatibleEndpointRefreshes.get(event.previous.hostId) === transitionKey) {
|
|
5591
|
-
return false;
|
|
5592
|
-
}
|
|
5875
|
+
if (this.activatedEndpointRefreshes.get(event.current.hostId) === transitionKey) {
|
|
5876
|
+
return false;
|
|
5593
5877
|
}
|
|
5594
5878
|
let inventory = await this.store.inspectPrivateCodexRoutes();
|
|
5595
5879
|
const previousRoutes = inventory.filter((route) => route.binding.hostId === event.previous.hostId &&
|
|
@@ -5607,11 +5891,10 @@ export class GatewayService {
|
|
|
5607
5891
|
settlements = await this.store.markConnectorOffline(event.previous, "ENDPOINT_GENERATION_CHANGED", inFlightSettlements, { preserveQueued: true });
|
|
5608
5892
|
}
|
|
5609
5893
|
catch (error) {
|
|
5610
|
-
const
|
|
5611
|
-
error instanceof BridgeError &&
|
|
5894
|
+
const alreadyInvalidatedTransition = error instanceof BridgeError &&
|
|
5612
5895
|
error.code === "CONNECTOR_NOT_FOUND" &&
|
|
5613
|
-
previousRoutes.every((route) => route.state === "stale"
|
|
5614
|
-
if (!
|
|
5896
|
+
previousRoutes.every((route) => route.state === "stale");
|
|
5897
|
+
if (!alreadyInvalidatedTransition)
|
|
5615
5898
|
throw error;
|
|
5616
5899
|
}
|
|
5617
5900
|
for (const settlement of settlements) {
|
|
@@ -5620,50 +5903,13 @@ export class GatewayService {
|
|
|
5620
5903
|
for (const route of previousRoutes) {
|
|
5621
5904
|
this.routeStates.set(route.alias, "stale");
|
|
5622
5905
|
}
|
|
5623
|
-
await this.store.recordCompatibilityAttestation(event.attestation);
|
|
5624
|
-
if (event.outcome === "incompatible") {
|
|
5625
|
-
await this.requireEndpointRefreshUnconflicted();
|
|
5626
|
-
this.activeCodexIncompatibility = structuredClone(event.attestation);
|
|
5627
|
-
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.previous.hostId), event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE");
|
|
5628
|
-
this.addRuntimeAlert(event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE", "error", { provider: "codex", host: event.previous.hostId });
|
|
5629
|
-
this.handledIncompatibleEndpointRefreshes.set(event.previous.hostId, transitionKey);
|
|
5630
|
-
await this.changed();
|
|
5631
|
-
return false;
|
|
5632
|
-
}
|
|
5633
|
-
if (event.attestation.tier === "schema_attested" &&
|
|
5634
|
-
!(compatibilityCoversWrites(event.attestation) &&
|
|
5635
|
-
adapter.latestWriteCompatibilityProbeObservation?.(event.current.endpointGeneration)?.outcome === "pass")) {
|
|
5636
|
-
// A passing untested-version schema is useful observation, but it is not
|
|
5637
|
-
// durable route or write authority. Settle the provider's pending probe
|
|
5638
|
-
// so a later generation can be examined, while leaving every G1 route
|
|
5639
|
-
// stale and refusing selector/registration activation.
|
|
5640
|
-
await this.requireEndpointRefreshUnconflicted();
|
|
5641
|
-
await this.store.observeConnector({
|
|
5642
|
-
identity: event.current,
|
|
5643
|
-
health: "degraded",
|
|
5644
|
-
compatibility: "unknown",
|
|
5645
|
-
protocol: adapter.protocol,
|
|
5646
|
-
protocolVersion: adapter.protocolVersion,
|
|
5647
|
-
safeErrorCode: "CODEX_MONITOR_ONLY",
|
|
5648
|
-
});
|
|
5649
|
-
adapter.acceptCompatibilityAttestation(event.attestation);
|
|
5650
|
-
this.activeCodexIncompatibility = undefined;
|
|
5651
|
-
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.current.hostId), "CODEX_MONITOR_ONLY");
|
|
5652
|
-
this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
|
|
5653
|
-
await this.changed();
|
|
5654
|
-
if (options.deferActivation === true ||
|
|
5655
|
-
options.requiredRoute !== undefined) {
|
|
5656
|
-
throw new BridgeError("CODEX_MONITOR_ONLY", "This Codex App Server passed read-only schema probes but cannot authorize a task route or message write.");
|
|
5657
|
-
}
|
|
5658
|
-
return false;
|
|
5659
|
-
}
|
|
5660
5906
|
await this.store.observeConnector({
|
|
5661
5907
|
identity: event.current,
|
|
5662
5908
|
health: "healthy",
|
|
5663
|
-
compatibility: "compatible",
|
|
5664
5909
|
protocol: adapter.protocol,
|
|
5665
5910
|
protocolVersion: adapter.protocolVersion,
|
|
5666
5911
|
});
|
|
5912
|
+
this.recordConnectorFreshness(event.current, "healthy");
|
|
5667
5913
|
const refreshedByHandle = new Map(event.routes.map((route) => [route.routeHandle, route.state]));
|
|
5668
5914
|
const pendingReanchors = previousRoutes.flatMap((route) => {
|
|
5669
5915
|
const state = refreshedByHandle.get(route.binding.routeHandle);
|
|
@@ -5715,7 +5961,7 @@ export class GatewayService {
|
|
|
5715
5961
|
route.binding.routeHandle === expected.threadId &&
|
|
5716
5962
|
route.binding.ownerLease === expected.ownerLease &&
|
|
5717
5963
|
route.enabled &&
|
|
5718
|
-
route.
|
|
5964
|
+
route.state !== "stale");
|
|
5719
5965
|
return matches.length === 1;
|
|
5720
5966
|
});
|
|
5721
5967
|
if (committedExactly) {
|
|
@@ -5748,8 +5994,7 @@ export class GatewayService {
|
|
|
5748
5994
|
route.binding.hostId !== event.current.hostId ||
|
|
5749
5995
|
route.binding.endpointGeneration !==
|
|
5750
5996
|
event.current.endpointGeneration ||
|
|
5751
|
-
route.state !== "stale"
|
|
5752
|
-
route.compatibility !== "expired") {
|
|
5997
|
+
route.state !== "stale") {
|
|
5753
5998
|
continue;
|
|
5754
5999
|
}
|
|
5755
6000
|
await this.registerOrRebind(route.alias, route.binding, "endpoint_reobserved", state, route.binding.ownerLease);
|
|
@@ -5764,7 +6009,7 @@ export class GatewayService {
|
|
|
5764
6009
|
event.current.endpointGeneration &&
|
|
5765
6010
|
route.binding.routeHandle === observed.routeHandle &&
|
|
5766
6011
|
route.enabled &&
|
|
5767
|
-
route.
|
|
6012
|
+
route.state !== "stale");
|
|
5768
6013
|
if (matches.length > 1) {
|
|
5769
6014
|
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "A refreshed Codex task has multiple durable owners.");
|
|
5770
6015
|
}
|
|
@@ -5804,7 +6049,7 @@ export class GatewayService {
|
|
|
5804
6049
|
event.current.endpointGeneration &&
|
|
5805
6050
|
route.binding.routeHandle === requiredRoute.threadId &&
|
|
5806
6051
|
route.enabled &&
|
|
5807
|
-
route.
|
|
6052
|
+
route.state !== "stale");
|
|
5808
6053
|
const match = matches[0];
|
|
5809
6054
|
if (matches.length !== 1 || match === undefined) {
|
|
5810
6055
|
throw new BridgeError("CODEX_ENDPOINT_REFRESH_MISMATCH", "The registering Codex task was not durably installed exactly.");
|
|
@@ -5858,7 +6103,7 @@ export class GatewayService {
|
|
|
5858
6103
|
}
|
|
5859
6104
|
for (const route of activatedRoutes) {
|
|
5860
6105
|
requireNativeContinuity(route);
|
|
5861
|
-
await this.
|
|
6106
|
+
await this.setNativeSourceStatus(route.alias, route.state === "idle"
|
|
5862
6107
|
? "idle"
|
|
5863
6108
|
: route.state === "awaiting_approval"
|
|
5864
6109
|
? "waiting"
|
|
@@ -5898,10 +6143,7 @@ export class GatewayService {
|
|
|
5898
6143
|
for (const route of activatedRoutes) {
|
|
5899
6144
|
requireNativeContinuity(route);
|
|
5900
6145
|
}
|
|
5901
|
-
|
|
5902
|
-
// above is complete. This acknowledgement is deliberately the final
|
|
5903
|
-
// fallible activation step.
|
|
5904
|
-
adapter.acceptCompatibilityAttestation(event.attestation);
|
|
6146
|
+
adapter.activateEndpointGeneration(event.current.endpointGeneration);
|
|
5905
6147
|
}
|
|
5906
6148
|
catch (error) {
|
|
5907
6149
|
try {
|
|
@@ -5921,9 +6163,6 @@ export class GatewayService {
|
|
|
5921
6163
|
?.transitionKey === transitionKey) {
|
|
5922
6164
|
this.pendingSelectorEndpointActivations.delete(event.current.hostId);
|
|
5923
6165
|
}
|
|
5924
|
-
this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
|
|
5925
|
-
this.activeCodexIncompatibility = undefined;
|
|
5926
|
-
this.incompatibleProviderSurfaces.delete(this.providerSurfaceKey("codex", event.current.hostId));
|
|
5927
6166
|
return false;
|
|
5928
6167
|
}
|
|
5929
6168
|
async onRouteState(source, event) {
|
|
@@ -5945,7 +6184,6 @@ export class GatewayService {
|
|
|
5945
6184
|
await this.store.observeConnector({
|
|
5946
6185
|
identity: source,
|
|
5947
6186
|
health: event.state === "stale" ? "degraded" : "healthy",
|
|
5948
|
-
compatibility: event.state === "stale" ? "expired" : "compatible",
|
|
5949
6187
|
protocol: adapter.protocol,
|
|
5950
6188
|
protocolVersion: adapter.protocolVersion,
|
|
5951
6189
|
...(event.safeErrorCode === undefined
|
|
@@ -5954,6 +6192,7 @@ export class GatewayService {
|
|
|
5954
6192
|
safeErrorCode: safeCode(event.safeErrorCode, "CONNECTOR_DEGRADED"),
|
|
5955
6193
|
}),
|
|
5956
6194
|
});
|
|
6195
|
+
this.recordConnectorFreshness(source, event.state === "stale" ? "degraded" : "healthy");
|
|
5957
6196
|
if (event.state === "stale") {
|
|
5958
6197
|
if (previousState !== "stale") {
|
|
5959
6198
|
this.addRuntimeAlert(staleSafeCode, "error", { provider: binding.provider, host: binding.hostId, alias });
|
|
@@ -5961,7 +6200,6 @@ export class GatewayService {
|
|
|
5961
6200
|
await this.store.observeRoute({
|
|
5962
6201
|
binding,
|
|
5963
6202
|
state: "stale",
|
|
5964
|
-
compatibility: "expired",
|
|
5965
6203
|
safeErrorCode: staleSafeCode,
|
|
5966
6204
|
});
|
|
5967
6205
|
}
|
|
@@ -5969,7 +6207,6 @@ export class GatewayService {
|
|
|
5969
6207
|
await this.store.observeRoute({
|
|
5970
6208
|
binding,
|
|
5971
6209
|
state: event.state,
|
|
5972
|
-
compatibility: "compatible",
|
|
5973
6210
|
...(event.safeErrorCode === undefined
|
|
5974
6211
|
? {}
|
|
5975
6212
|
: {
|
|
@@ -5979,15 +6216,15 @@ export class GatewayService {
|
|
|
5979
6216
|
}
|
|
5980
6217
|
await this.store.touchProgressWatchesForAlias(alias);
|
|
5981
6218
|
await this.changed();
|
|
5982
|
-
if (binding.provider
|
|
5983
|
-
await this.
|
|
6219
|
+
if (binding.provider !== "claude") {
|
|
6220
|
+
await this.setNativeSourceStatus(alias, event.state === "idle"
|
|
5984
6221
|
? "idle"
|
|
5985
6222
|
: event.state === "awaiting_approval"
|
|
5986
6223
|
? "waiting"
|
|
5987
6224
|
: "busy");
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
6225
|
+
}
|
|
6226
|
+
if (binding.provider === "codex" && previousState === "stale" && event.state !== "stale") {
|
|
6227
|
+
adapter.rearmEndpointRefreshActivation?.(binding.endpointGeneration);
|
|
5991
6228
|
}
|
|
5992
6229
|
if (event.state === "idle" || event.state === "stale") {
|
|
5993
6230
|
const heldTimer = this.heldRedispatchTimers.get(alias);
|
|
@@ -5998,12 +6235,12 @@ export class GatewayService {
|
|
|
5998
6235
|
this.scheduleDispatch(alias);
|
|
5999
6236
|
}
|
|
6000
6237
|
}
|
|
6001
|
-
async
|
|
6238
|
+
async setNativeSourceStatus(alias, status, requireSuccess = false) {
|
|
6002
6239
|
const binding = this.routeBindings.get(alias);
|
|
6003
|
-
if (binding
|
|
6240
|
+
if (binding === undefined || binding.provider === "claude")
|
|
6004
6241
|
return;
|
|
6005
6242
|
const adapter = this.adapter("claude", binding.hostId);
|
|
6006
|
-
const update = adapter.
|
|
6243
|
+
const update = adapter.updateNativeSourcePeerStatus;
|
|
6007
6244
|
if (update === undefined) {
|
|
6008
6245
|
if (requireSuccess) {
|
|
6009
6246
|
throw new BridgeError("CODEX_ENDPOINT_REFRESH_NATIVE_STATUS_FAILED", "The native listener does not support refreshed Codex status activation.", true);
|
|
@@ -6102,30 +6339,6 @@ export class GatewayService {
|
|
|
6102
6339
|
}
|
|
6103
6340
|
async publicSnapshotLocked() {
|
|
6104
6341
|
const base = await this.store.publicSnapshot();
|
|
6105
|
-
const compatibilityChecks = [];
|
|
6106
|
-
for (const { surface } of this.compatibilitySurfaceDefinitions) {
|
|
6107
|
-
if (surface === "codex" && this.activeCodexIncompatibility !== undefined) {
|
|
6108
|
-
compatibilityChecks.push(structuredClone(this.activeCodexIncompatibility));
|
|
6109
|
-
continue;
|
|
6110
|
-
}
|
|
6111
|
-
const currentVersions = [
|
|
6112
|
-
...new Set([...this.adapters, ...this.compatibilityObservers].flatMap((observer) => {
|
|
6113
|
-
const observation = observer.compatibilitySurface?.();
|
|
6114
|
-
return observation?.surface === surface ? [observation.version] : [];
|
|
6115
|
-
})),
|
|
6116
|
-
].sort((left, right) => left.localeCompare(right));
|
|
6117
|
-
const currentAttestations = (await Promise.all(currentVersions.map(async (version) => await this.store.inspectCompatibilityAttestation(surface, version))))
|
|
6118
|
-
.filter((attestation) => attestation !== undefined)
|
|
6119
|
-
.sort((left, right) => {
|
|
6120
|
-
const checkedAt = right.checkedAt.localeCompare(left.checkedAt);
|
|
6121
|
-
if (checkedAt !== 0)
|
|
6122
|
-
return checkedAt;
|
|
6123
|
-
return left.version.localeCompare(right.version);
|
|
6124
|
-
});
|
|
6125
|
-
const selected = currentAttestations[0];
|
|
6126
|
-
if (selected !== undefined)
|
|
6127
|
-
compatibilityChecks.push(selected);
|
|
6128
|
-
}
|
|
6129
6342
|
const peers = this.availablePeers.map((peer) => ({ ...peer }));
|
|
6130
6343
|
if (!arePublicAvailablePeerSnapshots(peers)) {
|
|
6131
6344
|
throw new BridgeError("INVALID_TRANSIENT_INVENTORY", "The transient peer inventory is unsafe.");
|
|
@@ -6154,11 +6367,6 @@ export class GatewayService {
|
|
|
6154
6367
|
return projectGatewayPublicSnapshot({
|
|
6155
6368
|
...base,
|
|
6156
6369
|
connectors: this.connectorsWithRegistry(base.connectors),
|
|
6157
|
-
...(compatibilityChecks.length === 0
|
|
6158
|
-
? {}
|
|
6159
|
-
: {
|
|
6160
|
-
compatibilityChecks: compatibilityChecks.map(projectPublicCompatibilityCheck),
|
|
6161
|
-
}),
|
|
6162
6370
|
availablePeers: peers,
|
|
6163
6371
|
activityEvents: this.activityEvents.map((event) => ({
|
|
6164
6372
|
...event,
|