agent-embassy 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/CONTRIBUTING.md +69 -6
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/SECURITY.md +152 -27
- package/dist/src/gateway/claude-peer.d.ts +7 -4
- package/dist/src/gateway/claude-peer.js +80 -36
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +4 -0
- package/dist/src/gateway/claude-runtime.js +50 -39
- 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 +1 -1
- package/dist/src/gateway/cli-copy.en.js +1 -1
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +1 -1
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +1 -1
- package/dist/src/gateway/cli.js +6 -7
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +44 -2
- package/dist/src/gateway/codex-app-server.js +536 -2
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +10 -6
- package/dist/src/gateway/codex-local-transport.js +101 -56
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +42 -3
- package/dist/src/gateway/compatibility.js +190 -37
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/control.js +28 -30
- 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 +42 -17
- package/dist/src/gateway/dashboard-copy.en.js +56 -31
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +42 -17
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +42 -17
- package/dist/src/gateway/dashboard-copy.zh-CN.js +56 -31
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +199 -11
- package/dist/src/gateway/dashboard-model.js +478 -75
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.d.ts +3 -0
- package/dist/src/gateway/dashboard.js +62 -137
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/deepseek-detect.d.ts +13 -0
- package/dist/src/gateway/deepseek-detect.js +125 -0
- package/dist/src/gateway/deepseek-detect.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +158 -256
- package/dist/src/gateway/live-dashboard-assets.js +2 -1
- package/dist/src/gateway/live-dashboard-assets.js.map +1 -1
- package/dist/src/gateway/live-dashboard-stream.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-stream.js +2 -2
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +50 -64
- package/dist/src/gateway/progress-watch-machine.js +42 -133
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +2 -0
- package/dist/src/gateway/provenance-envelope.js +13 -3
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +80 -16
- package/dist/src/gateway/providers.js +782 -115
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +10 -5
- package/dist/src/gateway/server.js +100 -13
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +46 -3
- package/dist/src/gateway/service.js +363 -101
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +23 -28
- package/dist/src/gateway/store.js +479 -467
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +40 -12
- package/dist/src/gateway/types.js +145 -4
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +8 -8
- package/docs/CONFIGURATION.zh-CN.md +8 -8
- package/docs/DASHBOARD.md +35 -6
- package/docs/DASHBOARD.zh-CN.md +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +173 -61
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { BridgeError } from "../errors.js";
|
|
3
3
|
import { KeyedMutex } from "../mutex.js";
|
|
4
|
-
import { certifiedCompatibilityVersions,
|
|
4
|
+
import { certifiedCompatibilityVersions, compatibilitySurfaceDefinitions, evaluateCompatibilityAttestation, isCompatibilityAttestation, } from "./compatibility.js";
|
|
5
5
|
import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, } from "./control.js";
|
|
6
6
|
import { publishGatewayDashboard } from "./dashboard.js";
|
|
7
7
|
import { createDeliveryMachine, isTerminalDeliveryMachine, projectDelivery, projectDeliveryWakeups, transitionDelivery, } from "./delivery-machine.js";
|
|
@@ -9,7 +9,7 @@ import { createCodexRegistrationSuccession, transitionCodexRegistrationSuccessio
|
|
|
9
9
|
import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
|
|
10
10
|
import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
|
|
11
11
|
import { GatewayStore, } from "./store.js";
|
|
12
|
-
import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, arePublicAvailablePeerSnapshots, projectGatewayPublicSnapshot, } from "./types.js";
|
|
12
|
+
import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, arePublicAvailablePeerSnapshots, projectPublicCompatibilityCheck, projectGatewayPublicSnapshot, } from "./types.js";
|
|
13
13
|
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
14
14
|
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
15
|
const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
@@ -22,10 +22,37 @@ const NATIVE_HELD_THRESHOLD_MS = 1_000;
|
|
|
22
22
|
const RUNTIME_ACTIVITY_CAPACITY = 256;
|
|
23
23
|
const DELIVERY_SETTLEMENT_RETRY_MS = 250;
|
|
24
24
|
const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
|
|
25
|
+
const BOOT_REACTIVATION_TRUST_FAILURE_CODES = new Set([
|
|
26
|
+
"HOME_INVALID",
|
|
27
|
+
"MANAGED_CODEX_INVALID",
|
|
28
|
+
"LOCAL_APP_SERVER_ENDPOINT_UNSAFE",
|
|
29
|
+
"APP_SERVER_VERSION_MISMATCH",
|
|
30
|
+
"ENDPOINT_GENERATION_CHANGED",
|
|
31
|
+
"CODEX_ENDPOINT_GENERATION_CHURN",
|
|
32
|
+
"CODEX_FACTORY_ATTESTATION_INVALID",
|
|
33
|
+
"CLEANUP_FAILED",
|
|
34
|
+
"FACTORY_CLOSED",
|
|
35
|
+
"INVALID_CONFIGURATION",
|
|
36
|
+
"CLAUDE_PEER_CALLBACK_UNSAFE",
|
|
37
|
+
"CODEX_ROUTE_CLEANUP_FAILED",
|
|
38
|
+
"CODEX_PROVIDER_CLEANUP_FAILED",
|
|
39
|
+
"CODEX_PROVIDER_CLEANUP_AMBIGUOUS",
|
|
40
|
+
"CODEX_PROVIDER_CLEANUP_TIMEOUT",
|
|
41
|
+
]);
|
|
42
|
+
function boundedErrorCode(error) {
|
|
43
|
+
if (typeof error !== "object" ||
|
|
44
|
+
error === null ||
|
|
45
|
+
!("code" in error) ||
|
|
46
|
+
typeof error.code !== "string" ||
|
|
47
|
+
!SAFE_CODE.test(error.code)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return error.code;
|
|
51
|
+
}
|
|
25
52
|
function progressWatchActivity(text, conversationId, actorAlias, explicitIdleMinutes) {
|
|
26
53
|
const prefixedOpen = text.startsWith("TRACK:");
|
|
27
54
|
const completionSignal = text.startsWith("DONE:");
|
|
28
|
-
if (completionSignal &&
|
|
55
|
+
if (completionSignal && explicitIdleMinutes !== undefined) {
|
|
29
56
|
throw new BridgeError("PROGRESS_WATCH_SIGNAL_CONFLICT", "A single message cannot both open and complete a progress watch.");
|
|
30
57
|
}
|
|
31
58
|
const openIdleMs = explicitIdleMinutes === undefined
|
|
@@ -122,6 +149,8 @@ export class GatewayService {
|
|
|
122
149
|
config;
|
|
123
150
|
store;
|
|
124
151
|
adapters;
|
|
152
|
+
compatibilityObservers;
|
|
153
|
+
compatibilitySurfaceDefinitions;
|
|
125
154
|
publishDashboard;
|
|
126
155
|
now;
|
|
127
156
|
timers;
|
|
@@ -133,6 +162,8 @@ export class GatewayService {
|
|
|
133
162
|
bindingAliases = new Map();
|
|
134
163
|
candidates = new Map();
|
|
135
164
|
availablePeers = [];
|
|
165
|
+
registryEvidenceByConnector = new Map();
|
|
166
|
+
registryAdaptersWithParseableRecords = new Set();
|
|
136
167
|
conversations = new Map();
|
|
137
168
|
messageContexts = new Map();
|
|
138
169
|
providerTurnContinuations = new Map();
|
|
@@ -145,6 +176,8 @@ export class GatewayService {
|
|
|
145
176
|
deliveryTokens = new Map();
|
|
146
177
|
runtimeAlerts = [];
|
|
147
178
|
activityEvents = [];
|
|
179
|
+
/** Startup probe failures that must preflight cross-provider mutations. */
|
|
180
|
+
incompatibleProviderSurfaces = new Map();
|
|
148
181
|
activeCodexIncompatibility;
|
|
149
182
|
activitySequence = 0;
|
|
150
183
|
detachedReceiptWrites = new Set();
|
|
@@ -214,6 +247,9 @@ export class GatewayService {
|
|
|
214
247
|
constructor(options) {
|
|
215
248
|
this.config = options.config;
|
|
216
249
|
this.adapters = [...(options.adapters ?? [])];
|
|
250
|
+
this.compatibilityObservers = [...(options.compatibilityObservers ?? [])];
|
|
251
|
+
this.compatibilitySurfaceDefinitions =
|
|
252
|
+
options.compatibilitySurfaceDefinitions ?? compatibilitySurfaceDefinitions;
|
|
217
253
|
this.publishDashboard = options.publishDashboard ?? publishGatewayDashboard;
|
|
218
254
|
this.now = options.now ?? (() => new Date());
|
|
219
255
|
this.store =
|
|
@@ -260,6 +296,12 @@ export class GatewayService {
|
|
|
260
296
|
throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
|
|
261
297
|
}
|
|
262
298
|
seen.add(key);
|
|
299
|
+
}
|
|
300
|
+
if (this.adapters.length > 0) {
|
|
301
|
+
await this.runAutomaticCompatibilityProbesLocked();
|
|
302
|
+
assertStartActive();
|
|
303
|
+
}
|
|
304
|
+
for (const adapter of this.adapters) {
|
|
263
305
|
const observation = await adapter.initialize(this.callbacksFor(adapter));
|
|
264
306
|
assertStartActive();
|
|
265
307
|
await this.store.observeConnector({
|
|
@@ -274,15 +316,6 @@ export class GatewayService {
|
|
|
274
316
|
});
|
|
275
317
|
assertStartActive();
|
|
276
318
|
}
|
|
277
|
-
if (this.adapters.length > 0 &&
|
|
278
|
-
this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
|
|
279
|
-
adapter.runCompatibilityProbes !== undefined)) {
|
|
280
|
-
const attestations = await this.runAutomaticCompatibilityProbesLocked();
|
|
281
|
-
const incompatible = attestations.find((surface) => surface.tier === "incompatible");
|
|
282
|
-
if (incompatible !== undefined) {
|
|
283
|
-
throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
319
|
await this.recoverCodexSuccessionAfterRestartLocked();
|
|
287
320
|
bootReactivatedIdleAliases =
|
|
288
321
|
await this.reactivateRetainedCodexRoutesAfterRestartLocked();
|
|
@@ -338,7 +371,7 @@ export class GatewayService {
|
|
|
338
371
|
try {
|
|
339
372
|
await control.close();
|
|
340
373
|
}
|
|
341
|
-
catch {
|
|
374
|
+
catch (error) {
|
|
342
375
|
closeFailed = true;
|
|
343
376
|
}
|
|
344
377
|
}
|
|
@@ -404,7 +437,7 @@ export class GatewayService {
|
|
|
404
437
|
await this.drainDetachedReceiptWritesLocked();
|
|
405
438
|
await this.publish();
|
|
406
439
|
}
|
|
407
|
-
catch {
|
|
440
|
+
catch (error) {
|
|
408
441
|
closeFailed = true;
|
|
409
442
|
}
|
|
410
443
|
const adapterResults = await Promise.allSettled(this.adapters.map(async (adapter) => adapter.close()));
|
|
@@ -427,6 +460,9 @@ export class GatewayService {
|
|
|
427
460
|
this.deliveryTrackers.clear();
|
|
428
461
|
this.deliveryTokens.clear();
|
|
429
462
|
this.runtimeAlerts.length = 0;
|
|
463
|
+
this.incompatibleProviderSurfaces.clear();
|
|
464
|
+
this.registryEvidenceByConnector.clear();
|
|
465
|
+
this.registryAdaptersWithParseableRecords.clear();
|
|
430
466
|
this.detachedReceiptWrites.clear();
|
|
431
467
|
this.nativeIngressByConversation.clear();
|
|
432
468
|
this.callbackQueue.length = 0;
|
|
@@ -511,10 +547,7 @@ export class GatewayService {
|
|
|
511
547
|
observeSnapshot: async () => await this.observeSnapshot(),
|
|
512
548
|
deliveryStatus: async (params) => await this.deliveryStatus(params.token),
|
|
513
549
|
untrack: async (params) => await this.exclusiveDecision(async () => {
|
|
514
|
-
const settled = await this.store.
|
|
515
|
-
conversationId: params.conversationId,
|
|
516
|
-
outcome: "done",
|
|
517
|
-
});
|
|
550
|
+
const settled = await this.store.endProgressWatch(params.conversationId);
|
|
518
551
|
if (!settled) {
|
|
519
552
|
throw new BridgeError("PROGRESS_WATCH_NOT_FOUND", "No active progress watch matches that conversation token.");
|
|
520
553
|
}
|
|
@@ -540,25 +573,58 @@ export class GatewayService {
|
|
|
540
573
|
return (await this.observeSnapshot()).snapshot;
|
|
541
574
|
}
|
|
542
575
|
async runAutomaticCompatibilityProbesLocked() {
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
adapter.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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(),
|
|
550
592
|
}));
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "
|
|
554
|
-
}
|
|
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
|
+
};
|
|
555
606
|
const attestations = [];
|
|
556
|
-
for (const
|
|
607
|
+
for (const definition of this.compatibilitySurfaceDefinitions) {
|
|
608
|
+
const { surface } = definition;
|
|
557
609
|
const entry = bySurface.get(surface);
|
|
558
610
|
if (entry === undefined) {
|
|
611
|
+
if (!definition.required)
|
|
612
|
+
continue;
|
|
559
613
|
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
|
|
560
614
|
}
|
|
561
|
-
const probes =
|
|
615
|
+
const probes = entry.adapter === undefined
|
|
616
|
+
? await entry.observer.runCompatibilityProbes()
|
|
617
|
+
: await entry.adapter.runCompatibilityProbes(probeContext);
|
|
618
|
+
if (entry.adapter !== undefined) {
|
|
619
|
+
const writeProbe = entry.adapter.latestWriteCompatibilityProbeObservation?.(entry.adapter.identity.endpointGeneration);
|
|
620
|
+
if (writeProbe?.outcome === "fail") {
|
|
621
|
+
this.addRuntimeAlert(writeProbe.safeErrorCode, writeProbe.safeErrorCode === "CODEX_WRITE_PROBE_TOOL_ACTIVITY_OBSERVED" ||
|
|
622
|
+
writeProbe.safeErrorCode === "CODEX_WRITE_PROBE_CLEANUP_UNCONFIRMED"
|
|
623
|
+
? "error"
|
|
624
|
+
: "warning", { provider: "codex", host: entry.adapter.identity.hostId });
|
|
625
|
+
}
|
|
626
|
+
this.captureRegistryObservation(entry.adapter, entry.adapter.latestRegistryObservation?.());
|
|
627
|
+
}
|
|
562
628
|
const attestation = evaluateCompatibilityAttestation({
|
|
563
629
|
surface,
|
|
564
630
|
version: entry.observation.version,
|
|
@@ -567,13 +633,84 @@ export class GatewayService {
|
|
|
567
633
|
probes,
|
|
568
634
|
});
|
|
569
635
|
await this.store.recordCompatibilityAttestation(attestation);
|
|
570
|
-
|
|
571
|
-
|
|
636
|
+
entry.observer.acceptCompatibilityAttestation?.(attestation);
|
|
637
|
+
if (attestation.tier === "incompatible" && entry.adapter !== undefined) {
|
|
638
|
+
const code = attestation.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED";
|
|
639
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey(entry.adapter.identity.provider, entry.adapter.identity.hostId), code);
|
|
640
|
+
if (surface === "codex") {
|
|
641
|
+
this.activeCodexIncompatibility = structuredClone(attestation);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
else if (surface === "codex" &&
|
|
645
|
+
attestation.tier === "schema_attested" &&
|
|
646
|
+
entry.adapter !== undefined) {
|
|
647
|
+
// A live but untested App Server schema is useful for observation only.
|
|
648
|
+
// Fence retained boot reactivation before it can trigger endpoint
|
|
649
|
+
// refresh churn; an explicit registration may still re-probe a later,
|
|
650
|
+
// certified generation through the provider's bounded refresh path.
|
|
651
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", entry.adapter.identity.hostId), "CODEX_MONITOR_ONLY");
|
|
572
652
|
}
|
|
573
653
|
attestations.push(attestation);
|
|
574
654
|
}
|
|
575
655
|
return attestations;
|
|
576
656
|
}
|
|
657
|
+
captureRegistryObservation(adapter, observation) {
|
|
658
|
+
if (adapter.identity.provider !== "claude" ||
|
|
659
|
+
observation === undefined ||
|
|
660
|
+
!Number.isSafeInteger(observation.entriesScanned) ||
|
|
661
|
+
observation.entriesScanned < 0 ||
|
|
662
|
+
!Number.isSafeInteger(observation.parseableRecords) ||
|
|
663
|
+
observation.parseableRecords < 0 ||
|
|
664
|
+
observation.parseableRecords > observation.entriesScanned ||
|
|
665
|
+
!Array.isArray(observation.rejected)) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const counts = new Map();
|
|
669
|
+
for (const row of observation.rejected) {
|
|
670
|
+
if (!row ||
|
|
671
|
+
typeof row !== "object" ||
|
|
672
|
+
typeof row.safeErrorCode !== "string" ||
|
|
673
|
+
!SAFE_CODE.test(row.safeErrorCode) ||
|
|
674
|
+
!Number.isSafeInteger(row.count) ||
|
|
675
|
+
row.count <= 0) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const next = (counts.get(row.safeErrorCode) ?? 0) + row.count;
|
|
679
|
+
counts.set(row.safeErrorCode, Math.min(Number.MAX_SAFE_INTEGER, next));
|
|
680
|
+
}
|
|
681
|
+
const ordered = [...counts]
|
|
682
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
683
|
+
.map(([safeErrorCode, count]) => ({ safeErrorCode, count }));
|
|
684
|
+
const rejected = ordered.slice(0, gatewayPublicSnapshotLimits.registryRejectionCodes);
|
|
685
|
+
if (observation.parseableRecords > 0) {
|
|
686
|
+
this.registryAdaptersWithParseableRecords.add(adapter);
|
|
687
|
+
}
|
|
688
|
+
const hasObservedParseableRecord = this.registryAdaptersWithParseableRecords.has(adapter);
|
|
689
|
+
const row = {
|
|
690
|
+
entriesScanned: observation.entriesScanned,
|
|
691
|
+
parseableRecords: observation.parseableRecords,
|
|
692
|
+
parseableRecordSeenSinceBoot: hasObservedParseableRecord,
|
|
693
|
+
rejected,
|
|
694
|
+
rejectedCodesOmitted: Math.max(0, ordered.length - rejected.length),
|
|
695
|
+
};
|
|
696
|
+
this.registryEvidenceByConnector.set(`${adapter.identity.provider}\0${adapter.identity.hostId}`, row);
|
|
697
|
+
}
|
|
698
|
+
connectorsWithRegistry(connectors) {
|
|
699
|
+
return connectors.map((connector) => {
|
|
700
|
+
const registry = this.registryEvidenceByConnector.get(`${connector.provider}\0${connector.host}`);
|
|
701
|
+
return {
|
|
702
|
+
...connector,
|
|
703
|
+
...(connector.provider !== "claude" || registry === undefined
|
|
704
|
+
? {}
|
|
705
|
+
: {
|
|
706
|
+
registry: {
|
|
707
|
+
...registry,
|
|
708
|
+
rejected: registry.rejected.map((row) => ({ ...row })),
|
|
709
|
+
},
|
|
710
|
+
}),
|
|
711
|
+
};
|
|
712
|
+
});
|
|
713
|
+
}
|
|
577
714
|
async observeSnapshot() {
|
|
578
715
|
return await this.mutex.run("service", async () => {
|
|
579
716
|
const changed = await this.processLifecycleLocked();
|
|
@@ -921,17 +1058,31 @@ export class GatewayService {
|
|
|
921
1058
|
await this.publish();
|
|
922
1059
|
}
|
|
923
1060
|
this.pruneTransient();
|
|
1061
|
+
const deferredRouteObservations = [];
|
|
924
1062
|
while (this.hasPendingCallbackWork()) {
|
|
925
1063
|
// Preserve the complete ordinary queue order. In particular, every
|
|
926
1064
|
// delivery observation already retained before endpoint replacement is
|
|
927
1065
|
// reduced before the lifecycle transition can settle its generation.
|
|
928
1066
|
const ordinaryBatch = this.callbackQueue.splice(0, this.callbackQueue.length);
|
|
1067
|
+
const pendingRefresh = this.endpointRefreshCallback?.value;
|
|
929
1068
|
for (const event of ordinaryBatch) {
|
|
930
1069
|
try {
|
|
931
1070
|
if (event.type === "delivery") {
|
|
932
1071
|
await this.onDelivery(event.source, event.value, event.receivedAt);
|
|
933
1072
|
}
|
|
934
1073
|
else if (event.type === "route") {
|
|
1074
|
+
if (pendingRefresh?.outcome === "compatible" &&
|
|
1075
|
+
event.source.provider === "codex" &&
|
|
1076
|
+
event.source.hostId === pendingRefresh.current.hostId &&
|
|
1077
|
+
event.source.endpointGeneration ===
|
|
1078
|
+
pendingRefresh.current.endpointGeneration &&
|
|
1079
|
+
pendingRefresh.routes.some((route) => route.routeHandle === event.value.routeHandle)) {
|
|
1080
|
+
// The provider can observe G2 after staging it but before this
|
|
1081
|
+
// worker installs G2's durable binding. Preserve that fresher
|
|
1082
|
+
// state until the exact transition evidence has been reduced.
|
|
1083
|
+
deferredRouteObservations.push(event);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
935
1086
|
await this.onRouteState(event.source, event.value);
|
|
936
1087
|
}
|
|
937
1088
|
else if (event.type === "claude_reply") {
|
|
@@ -994,6 +1145,14 @@ export class GatewayService {
|
|
|
994
1145
|
this.endpointRefreshCallback = requestedRetry;
|
|
995
1146
|
}
|
|
996
1147
|
}
|
|
1148
|
+
for (const event of deferredRouteObservations) {
|
|
1149
|
+
try {
|
|
1150
|
+
await this.onRouteState(event.source, event.value);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
this.dashboardHealthy = false;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
997
1156
|
}
|
|
998
1157
|
/**
|
|
999
1158
|
* A selector can synchronously admit callbacks while its promise is still
|
|
@@ -1149,6 +1308,23 @@ export class GatewayService {
|
|
|
1149
1308
|
throw new BridgeError("PROVIDER_UNAVAILABLE", "The selected provider is unavailable.", true);
|
|
1150
1309
|
return adapter;
|
|
1151
1310
|
}
|
|
1311
|
+
providerSurfaceKey(provider, host) {
|
|
1312
|
+
return `${provider}@${host}`;
|
|
1313
|
+
}
|
|
1314
|
+
incompatibleProviderCode(provider, host) {
|
|
1315
|
+
return this.incompatibleProviderSurfaces.get(this.providerSurfaceKey(provider, host));
|
|
1316
|
+
}
|
|
1317
|
+
/** Reject before either half of a cross-provider mutation can acquire state. */
|
|
1318
|
+
assertCrossProviderMutationCompatible(host, options = { allowCodexRefresh: false }) {
|
|
1319
|
+
const claudeCode = this.incompatibleProviderCode("claude", host);
|
|
1320
|
+
if (claudeCode !== undefined) {
|
|
1321
|
+
throw new BridgeError(claudeCode, "The Claude provider is observation-only until Embassy restarts with a compatible runtime.");
|
|
1322
|
+
}
|
|
1323
|
+
const codexCode = this.incompatibleProviderCode("codex", host);
|
|
1324
|
+
if (codexCode !== undefined && !options.allowCodexRefresh) {
|
|
1325
|
+
throw new BridgeError(codexCode, "The Codex provider is observation-only until a compatible endpoint is re-observed.");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1152
1328
|
async assertClaudeWorkspaceDisjoint(adapter, routeHandle) {
|
|
1153
1329
|
if (adapter.identity.provider !== "claude") {
|
|
1154
1330
|
throw new BridgeError("CLAUDE_PROVIDER_MISMATCH", "The selected workspace guard does not belong to Claude.");
|
|
@@ -1219,6 +1395,13 @@ export class GatewayService {
|
|
|
1219
1395
|
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1220
1396
|
continue;
|
|
1221
1397
|
}
|
|
1398
|
+
if (this.incompatibleProviderCode("codex", route.binding.hostId) !==
|
|
1399
|
+
undefined ||
|
|
1400
|
+
this.incompatibleProviderCode("claude", route.binding.hostId) !==
|
|
1401
|
+
undefined) {
|
|
1402
|
+
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1222
1405
|
let routeSelected = false;
|
|
1223
1406
|
let advertiseAttempted = false;
|
|
1224
1407
|
let storeReactivated = false;
|
|
@@ -1308,7 +1491,7 @@ export class GatewayService {
|
|
|
1308
1491
|
if (selected.state === "idle")
|
|
1309
1492
|
idleAliases.push(route.alias);
|
|
1310
1493
|
}
|
|
1311
|
-
catch {
|
|
1494
|
+
catch (error) {
|
|
1312
1495
|
let cleanupFailed = false;
|
|
1313
1496
|
if (advertiseAttempted) {
|
|
1314
1497
|
await claudeAdapter
|
|
@@ -1327,6 +1510,11 @@ export class GatewayService {
|
|
|
1327
1510
|
if (cleanupFailed || storeReactivated || deferredEndpointRefresh) {
|
|
1328
1511
|
throw new BridgeError("CODEX_BOOT_REACTIVATION_CLEANUP_FAILED", "A Codex boot reactivation crossed its durable boundary without exact live authority.");
|
|
1329
1512
|
}
|
|
1513
|
+
const failureCode = boundedErrorCode(error);
|
|
1514
|
+
if (failureCode !== undefined &&
|
|
1515
|
+
BOOT_REACTIVATION_TRUST_FAILURE_CODES.has(failureCode)) {
|
|
1516
|
+
throw error;
|
|
1517
|
+
}
|
|
1330
1518
|
this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
|
|
1331
1519
|
}
|
|
1332
1520
|
}
|
|
@@ -2119,6 +2307,9 @@ export class GatewayService {
|
|
|
2119
2307
|
if (!params.alias.startsWith("codex-")) {
|
|
2120
2308
|
throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
|
|
2121
2309
|
}
|
|
2310
|
+
this.assertCrossProviderMutationCompatible(params.hostId, {
|
|
2311
|
+
allowCodexRefresh: params.succeedsAlias === undefined,
|
|
2312
|
+
});
|
|
2122
2313
|
if (this.codexSuccessionPoisoned.has(params.alias) ||
|
|
2123
2314
|
this.codexOrphanCleanupPending.has(params.alias) ||
|
|
2124
2315
|
(params.succeedsAlias !== undefined &&
|
|
@@ -2338,7 +2529,7 @@ export class GatewayService {
|
|
|
2338
2529
|
unwrittenOutcome: "cancelled",
|
|
2339
2530
|
safeErrorCode: "ROUTE_UNREGISTERED",
|
|
2340
2531
|
});
|
|
2341
|
-
const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements);
|
|
2532
|
+
const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements, "operator");
|
|
2342
2533
|
for (const settlement of settlements) {
|
|
2343
2534
|
await this.applyTerminalSettlementLocked(settlement);
|
|
2344
2535
|
}
|
|
@@ -2573,6 +2764,7 @@ export class GatewayService {
|
|
|
2573
2764
|
const base = await this.store.publicSnapshot();
|
|
2574
2765
|
const snapshot = projectGatewayPublicSnapshot({
|
|
2575
2766
|
...base,
|
|
2767
|
+
connectors: this.connectorsWithRegistry(base.connectors),
|
|
2576
2768
|
availablePeers: this.availablePeers.map((peer) => ({ ...peer })),
|
|
2577
2769
|
});
|
|
2578
2770
|
return JSON.stringify({
|
|
@@ -2607,6 +2799,7 @@ export class GatewayService {
|
|
|
2607
2799
|
// authority. The provider still revalidates the exact target before
|
|
2608
2800
|
// any write.
|
|
2609
2801
|
}
|
|
2802
|
+
this.captureRegistryObservation(adapter, discovered.registry ?? adapter.latestRegistryObservation?.());
|
|
2610
2803
|
const grouped = new Map();
|
|
2611
2804
|
const byHandle = new Map();
|
|
2612
2805
|
for (const peer of discovered.peers) {
|
|
@@ -3094,6 +3287,32 @@ export class GatewayService {
|
|
|
3094
3287
|
await this.selectAndPairClaude(params.alias, await this.inferCodexAlias(params.codexThreadId));
|
|
3095
3288
|
}
|
|
3096
3289
|
async selectAndPairClaude(selector, codexAlias) {
|
|
3290
|
+
const codexBinding = this.routeBindings.get(codexAlias);
|
|
3291
|
+
const retainedCodexRoute = codexBinding?.provider === "codex"
|
|
3292
|
+
? undefined
|
|
3293
|
+
: await this.store.inspectPrivateRoute(codexAlias);
|
|
3294
|
+
const codexHost = codexBinding?.provider === "codex"
|
|
3295
|
+
? codexBinding.hostId
|
|
3296
|
+
: retainedCodexRoute?.binding.provider === "codex"
|
|
3297
|
+
? retainedCodexRoute.binding.hostId
|
|
3298
|
+
: undefined;
|
|
3299
|
+
if (codexHost !== undefined) {
|
|
3300
|
+
this.assertCrossProviderMutationCompatible(codexHost);
|
|
3301
|
+
}
|
|
3302
|
+
const codexAdapter = codexBinding?.provider === "codex"
|
|
3303
|
+
? this.adapters.find((adapter) => adapter.identity.provider === "codex" &&
|
|
3304
|
+
adapter.identity.hostId === codexBinding.hostId)
|
|
3305
|
+
: undefined;
|
|
3306
|
+
const codexState = this.routeStates.get(codexAlias);
|
|
3307
|
+
if (codexBinding?.provider !== "codex" ||
|
|
3308
|
+
codexAdapter === undefined ||
|
|
3309
|
+
codexAdapter.identity.endpointGeneration !==
|
|
3310
|
+
codexBinding.endpointGeneration ||
|
|
3311
|
+
(codexState !== "idle" &&
|
|
3312
|
+
codexState !== "busy" &&
|
|
3313
|
+
codexState !== "awaiting_approval")) {
|
|
3314
|
+
throw new BridgeError("CODEX_ROUTE_UNAVAILABLE", "Claude selection requires an exact positively observed live Codex route.");
|
|
3315
|
+
}
|
|
3097
3316
|
const discoveryChanged = await this.refreshClaudeDiscovery();
|
|
3098
3317
|
const candidate = this.claudeCandidate(selector);
|
|
3099
3318
|
if (candidate === undefined)
|
|
@@ -3201,6 +3420,16 @@ export class GatewayService {
|
|
|
3201
3420
|
this.routeStates.set(alias, state);
|
|
3202
3421
|
this.bindingAliases.set(bindingKey(binding), alias);
|
|
3203
3422
|
}
|
|
3423
|
+
reanchorQueuedMessageContexts(alias, previous, current) {
|
|
3424
|
+
const previousKey = bindingKey(previous);
|
|
3425
|
+
const currentKey = bindingKey(current);
|
|
3426
|
+
for (const context of this.messageContexts.values()) {
|
|
3427
|
+
if (context.targetAlias === alias &&
|
|
3428
|
+
context.targetBindingKey === previousKey) {
|
|
3429
|
+
context.targetBindingKey = currentKey;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3204
3433
|
forgetBinding(alias) {
|
|
3205
3434
|
const heldTimer = this.heldRedispatchTimers.get(alias);
|
|
3206
3435
|
if (heldTimer !== undefined) {
|
|
@@ -3924,14 +4153,7 @@ export class GatewayService {
|
|
|
3924
4153
|
return false;
|
|
3925
4154
|
}
|
|
3926
4155
|
const actions = await this.store.advanceDueProgressWatches();
|
|
3927
|
-
let changed = actions.length > 0;
|
|
3928
4156
|
for (const action of actions) {
|
|
3929
|
-
if (action.type === "settled") {
|
|
3930
|
-
this.addRuntimeAlert(action.outcome === "unresponsive"
|
|
3931
|
-
? "PROGRESS_WATCH_UNRESPONSIVE"
|
|
3932
|
-
: "PROGRESS_WATCH_ENDPOINT_RETIRED", action.outcome === "unresponsive" ? "error" : "warning", { alias: action.ownerAlias });
|
|
3933
|
-
continue;
|
|
3934
|
-
}
|
|
3935
4157
|
const text = `[Embassy automated liveness check — ${action.conversationId} / ` +
|
|
3936
4158
|
`Embassy 自动活跃检查 — ${action.conversationId}]\n` +
|
|
3937
4159
|
"Reply with the result or a status. / 请回复结果或状态。";
|
|
@@ -3945,20 +4167,13 @@ export class GatewayService {
|
|
|
3945
4167
|
nudgeNumber: action.nudgeNumber,
|
|
3946
4168
|
},
|
|
3947
4169
|
});
|
|
3948
|
-
this.addRuntimeAlert("PROGRESS_WATCH_NUDGE", "warning", {
|
|
3949
|
-
alias: action.ownerAlias,
|
|
3950
|
-
});
|
|
3951
4170
|
}
|
|
3952
4171
|
catch {
|
|
3953
|
-
await this.store.deferProgressWatchNudge(action.conversationId);
|
|
3954
|
-
this.addRuntimeAlert("PROGRESS_WATCH_NUDGE_DEFERRED", "warning", {
|
|
3955
|
-
alias: action.ownerAlias,
|
|
3956
|
-
});
|
|
3957
|
-
changed = true;
|
|
4172
|
+
await this.store.deferProgressWatchNudge(action.conversationId, action.nudgeNumber);
|
|
3958
4173
|
}
|
|
3959
4174
|
}
|
|
3960
4175
|
await this.refreshNextProgressWatchWakeLocked();
|
|
3961
|
-
return
|
|
4176
|
+
return actions.length > 0;
|
|
3962
4177
|
}
|
|
3963
4178
|
async processLifecycleLocked() {
|
|
3964
4179
|
const now = this.now().getTime();
|
|
@@ -4253,6 +4468,9 @@ export class GatewayService {
|
|
|
4253
4468
|
const conversationId = options.existingConversationId ?? createGatewayConversationId();
|
|
4254
4469
|
const conversation = this.conversations.get(conversationId) ?? {
|
|
4255
4470
|
id: conversationId,
|
|
4471
|
+
...(options.existingConversationId === undefined
|
|
4472
|
+
? {}
|
|
4473
|
+
: { dedupeScope: createGatewayConversationId() }),
|
|
4256
4474
|
sourceAlias,
|
|
4257
4475
|
targetAlias,
|
|
4258
4476
|
expectsReply,
|
|
@@ -4269,7 +4487,7 @@ export class GatewayService {
|
|
|
4269
4487
|
sourceAlias,
|
|
4270
4488
|
targetAlias,
|
|
4271
4489
|
body: text,
|
|
4272
|
-
dedupeKey: `${conversationId}:${sequence}`,
|
|
4490
|
+
dedupeKey: `${conversation.dedupeScope ?? conversationId}:${sequence}`,
|
|
4273
4491
|
conversationIdSuffix: conversationId.slice(-8),
|
|
4274
4492
|
deadlineAt,
|
|
4275
4493
|
...(options.steer === true ? { steer: true } : {}),
|
|
@@ -4348,7 +4566,7 @@ export class GatewayService {
|
|
|
4348
4566
|
binding: capability.binding,
|
|
4349
4567
|
},
|
|
4350
4568
|
body: text,
|
|
4351
|
-
dedupeKey: `${conversation.id}:${sequence}`,
|
|
4569
|
+
dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
|
|
4352
4570
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
4353
4571
|
deadlineAt,
|
|
4354
4572
|
...(conversation.pair === true ? { pair: true } : {}),
|
|
@@ -4410,7 +4628,7 @@ export class GatewayService {
|
|
|
4410
4628
|
},
|
|
4411
4629
|
targetAlias: conversation.sourceAlias,
|
|
4412
4630
|
body: text,
|
|
4413
|
-
dedupeKey: `${conversation.id}:${sequence}`,
|
|
4631
|
+
dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
|
|
4414
4632
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
4415
4633
|
deadlineAt,
|
|
4416
4634
|
authorizedPairTeardownReply: true,
|
|
@@ -4525,14 +4743,17 @@ export class GatewayService {
|
|
|
4525
4743
|
await this.publishNativeHeldLocked(afterCallbacks);
|
|
4526
4744
|
}
|
|
4527
4745
|
}
|
|
4528
|
-
restoreQueuedMessageContextLocked(item, binding) {
|
|
4746
|
+
restoreQueuedMessageContextLocked(item, binding, canonicalConversationId) {
|
|
4529
4747
|
this.pruneTransient();
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
const conversation = {
|
|
4748
|
+
const conversationId = canonicalConversationId ??
|
|
4749
|
+
(item.conversationIdSuffix === undefined
|
|
4750
|
+
? createGatewayConversationId()
|
|
4751
|
+
: `${createGatewayConversationId().slice(0, -8)}${item.conversationIdSuffix}`);
|
|
4752
|
+
const conversation = this.conversations.get(conversationId) ?? {
|
|
4535
4753
|
id: conversationId,
|
|
4754
|
+
...(canonicalConversationId === undefined
|
|
4755
|
+
? {}
|
|
4756
|
+
: { dedupeScope: createGatewayConversationId() }),
|
|
4536
4757
|
sourceAlias: item.sourceAlias,
|
|
4537
4758
|
targetAlias: item.targetAlias,
|
|
4538
4759
|
expectsReply: false,
|
|
@@ -4548,7 +4769,6 @@ export class GatewayService {
|
|
|
4548
4769
|
authorization: "selected_route",
|
|
4549
4770
|
targetAlias: item.targetAlias,
|
|
4550
4771
|
deadlineAt: item.deadlineAt,
|
|
4551
|
-
recovered: true,
|
|
4552
4772
|
};
|
|
4553
4773
|
this.conversations.set(conversationId, conversation);
|
|
4554
4774
|
this.messageContexts.set(item.messageId, context);
|
|
@@ -4589,6 +4809,7 @@ export class GatewayService {
|
|
|
4589
4809
|
return;
|
|
4590
4810
|
const selectedBinding = this.routeBindings.get(targetAlias);
|
|
4591
4811
|
let context = this.messageContexts.get(item.messageId);
|
|
4812
|
+
let progressWatchActive;
|
|
4592
4813
|
if (context === undefined && selectedBinding === undefined) {
|
|
4593
4814
|
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4594
4815
|
if (result.status === "settled") {
|
|
@@ -4598,7 +4819,15 @@ export class GatewayService {
|
|
|
4598
4819
|
return;
|
|
4599
4820
|
}
|
|
4600
4821
|
if (context === undefined && selectedBinding !== undefined) {
|
|
4601
|
-
|
|
4822
|
+
const resolution = item.pair === true && item.conversationIdSuffix !== undefined
|
|
4823
|
+
? await this.store.resolveProgressWatchDispatch({
|
|
4824
|
+
recoveredConversationIdSuffix: item.conversationIdSuffix,
|
|
4825
|
+
sourceAlias: item.sourceAlias,
|
|
4826
|
+
targetAlias: item.targetAlias,
|
|
4827
|
+
})
|
|
4828
|
+
: undefined;
|
|
4829
|
+
context = this.restoreQueuedMessageContextLocked(item, selectedBinding, resolution?.conversationId);
|
|
4830
|
+
progressWatchActive = resolution?.markerActive === true;
|
|
4602
4831
|
}
|
|
4603
4832
|
if (context === undefined) {
|
|
4604
4833
|
const result = await this.store.settleMessage({
|
|
@@ -4634,22 +4863,11 @@ export class GatewayService {
|
|
|
4634
4863
|
inspection?.state === "stale" ||
|
|
4635
4864
|
inspection?.safeErrorCode === "CODEX_ROUTE_STALE");
|
|
4636
4865
|
if (codexRouteStale) {
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
await this.applyTerminalSettlementLocked(result.settlement);
|
|
4641
|
-
}
|
|
4642
|
-
await this.changed();
|
|
4643
|
-
return;
|
|
4866
|
+
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4867
|
+
if (result.status === "settled") {
|
|
4868
|
+
await this.applyTerminalSettlementLocked(result.settlement);
|
|
4644
4869
|
}
|
|
4645
|
-
await this.advanceDeliveryLocked(item.messageId, {
|
|
4646
|
-
type: "route_terminated",
|
|
4647
|
-
at: this.now().getTime(),
|
|
4648
|
-
unwrittenOutcome: "failed",
|
|
4649
|
-
safeErrorCode: "CODEX_ROUTE_STALE",
|
|
4650
|
-
});
|
|
4651
4870
|
await this.changed();
|
|
4652
|
-
this.scheduleDispatch(targetAlias);
|
|
4653
4871
|
return;
|
|
4654
4872
|
}
|
|
4655
4873
|
const storedDispatchable = binding.provider === "claude" ||
|
|
@@ -4741,23 +4959,16 @@ export class GatewayService {
|
|
|
4741
4959
|
this.scheduleLifecycleWakeLocked();
|
|
4742
4960
|
return;
|
|
4743
4961
|
}
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
ownerAlias: item.sourceAlias,
|
|
4754
|
-
workerAlias: item.targetAlias,
|
|
4755
|
-
});
|
|
4756
|
-
progressWatches = await this.store.inspectProgressWatches();
|
|
4962
|
+
if (progressWatchActive === undefined) {
|
|
4963
|
+
progressWatchActive =
|
|
4964
|
+
item.pair === true
|
|
4965
|
+
? (await this.store.resolveProgressWatchDispatch({
|
|
4966
|
+
conversationId: context.conversationId,
|
|
4967
|
+
sourceAlias: item.sourceAlias,
|
|
4968
|
+
targetAlias: item.targetAlias,
|
|
4969
|
+
})).markerActive
|
|
4970
|
+
: false;
|
|
4757
4971
|
}
|
|
4758
|
-
const progressWatchActive = progressWatches.some((watch) => watch.capability === "conversation" &&
|
|
4759
|
-
watch.workerAlias === item.targetAlias &&
|
|
4760
|
-
watch.conversationId === context.conversationId);
|
|
4761
4972
|
let result;
|
|
4762
4973
|
try {
|
|
4763
4974
|
result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
|
|
@@ -4771,6 +4982,9 @@ export class GatewayService {
|
|
|
4771
4982
|
expectsReply: context.expectsReply,
|
|
4772
4983
|
deadlineAt: item.deadlineAt,
|
|
4773
4984
|
...(item.steer === true ? { steer: true } : {}),
|
|
4985
|
+
...(item.steer === true && item.queuedAhead !== undefined
|
|
4986
|
+
? { queuedAhead: item.queuedAhead }
|
|
4987
|
+
: {}),
|
|
4774
4988
|
...(progressWatchActive
|
|
4775
4989
|
? { progressWatchActive: true }
|
|
4776
4990
|
: {}),
|
|
@@ -5304,6 +5518,9 @@ export class GatewayService {
|
|
|
5304
5518
|
return activationDeferred;
|
|
5305
5519
|
}
|
|
5306
5520
|
finally {
|
|
5521
|
+
if (!succeeded && event.outcome === "compatible") {
|
|
5522
|
+
adapter.releaseEndpointRefreshSelectorClaim?.(event.current.endpointGeneration);
|
|
5523
|
+
}
|
|
5307
5524
|
if (ownsActiveTransition &&
|
|
5308
5525
|
this.activeEndpointRefreshCallbackKey === transitionKey) {
|
|
5309
5526
|
this.activeEndpointRefreshCallback = undefined;
|
|
@@ -5404,11 +5621,37 @@ export class GatewayService {
|
|
|
5404
5621
|
if (event.outcome === "incompatible") {
|
|
5405
5622
|
await this.requireEndpointRefreshUnconflicted();
|
|
5406
5623
|
this.activeCodexIncompatibility = structuredClone(event.attestation);
|
|
5624
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.previous.hostId), event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE");
|
|
5407
5625
|
this.addRuntimeAlert(event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE", "error", { provider: "codex", host: event.previous.hostId });
|
|
5408
5626
|
this.handledIncompatibleEndpointRefreshes.set(event.previous.hostId, transitionKey);
|
|
5409
5627
|
await this.changed();
|
|
5410
5628
|
return false;
|
|
5411
5629
|
}
|
|
5630
|
+
if (event.attestation.tier === "schema_attested") {
|
|
5631
|
+
// A passing untested-version schema is useful observation, but it is not
|
|
5632
|
+
// durable route or write authority. Settle the provider's pending probe
|
|
5633
|
+
// so a later generation can be examined, while leaving every G1 route
|
|
5634
|
+
// stale and refusing selector/registration activation.
|
|
5635
|
+
await this.requireEndpointRefreshUnconflicted();
|
|
5636
|
+
await this.store.observeConnector({
|
|
5637
|
+
identity: event.current,
|
|
5638
|
+
health: "degraded",
|
|
5639
|
+
compatibility: "unknown",
|
|
5640
|
+
protocol: adapter.protocol,
|
|
5641
|
+
protocolVersion: adapter.protocolVersion,
|
|
5642
|
+
safeErrorCode: "CODEX_MONITOR_ONLY",
|
|
5643
|
+
});
|
|
5644
|
+
adapter.acceptCompatibilityAttestation(event.attestation);
|
|
5645
|
+
this.activeCodexIncompatibility = undefined;
|
|
5646
|
+
this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.current.hostId), "CODEX_MONITOR_ONLY");
|
|
5647
|
+
this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
|
|
5648
|
+
await this.changed();
|
|
5649
|
+
if (options.deferActivation === true ||
|
|
5650
|
+
options.requiredRoute !== undefined) {
|
|
5651
|
+
throw new BridgeError("CODEX_MONITOR_ONLY", "This Codex App Server passed read-only schema probes but cannot authorize a task route or message write.");
|
|
5652
|
+
}
|
|
5653
|
+
return false;
|
|
5654
|
+
}
|
|
5412
5655
|
await this.store.observeConnector({
|
|
5413
5656
|
identity: event.current,
|
|
5414
5657
|
health: "healthy",
|
|
@@ -5430,6 +5673,18 @@ export class GatewayService {
|
|
|
5430
5673
|
},
|
|
5431
5674
|
];
|
|
5432
5675
|
});
|
|
5676
|
+
const reanchorPendingMessageContexts = () => {
|
|
5677
|
+
for (const route of pendingReanchors) {
|
|
5678
|
+
const previous = previousRoutes.find((candidate) => candidate.alias === route.alias)?.binding;
|
|
5679
|
+
if (previous === undefined)
|
|
5680
|
+
continue;
|
|
5681
|
+
this.reanchorQueuedMessageContexts(route.alias, previous, {
|
|
5682
|
+
...event.current,
|
|
5683
|
+
routeHandle: route.threadId,
|
|
5684
|
+
ownerLease: route.ownerLease,
|
|
5685
|
+
});
|
|
5686
|
+
}
|
|
5687
|
+
};
|
|
5433
5688
|
if (pendingReanchors.length > 0) {
|
|
5434
5689
|
try {
|
|
5435
5690
|
await this.store.reanchorCodexRoutes({
|
|
@@ -5437,6 +5692,7 @@ export class GatewayService {
|
|
|
5437
5692
|
newEndpoint: event.current,
|
|
5438
5693
|
routes: pendingReanchors,
|
|
5439
5694
|
});
|
|
5695
|
+
reanchorPendingMessageContexts();
|
|
5440
5696
|
this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
|
|
5441
5697
|
}
|
|
5442
5698
|
catch (error) {
|
|
@@ -5458,6 +5714,7 @@ export class GatewayService {
|
|
|
5458
5714
|
return matches.length === 1;
|
|
5459
5715
|
});
|
|
5460
5716
|
if (committedExactly) {
|
|
5717
|
+
reanchorPendingMessageContexts();
|
|
5461
5718
|
this.durablyAppliedEndpointRefreshes.set(event.current.hostId, transitionKey);
|
|
5462
5719
|
try {
|
|
5463
5720
|
await this.store.markConnectorOffline(event.current, "CODEX_ENDPOINT_ACTIVATION_FAILED", [], { preserveQueued: true });
|
|
@@ -5639,11 +5896,7 @@ export class GatewayService {
|
|
|
5639
5896
|
// Provider writes remain closed until every durable and native transition
|
|
5640
5897
|
// above is complete. This acknowledgement is deliberately the final
|
|
5641
5898
|
// fallible activation step.
|
|
5642
|
-
|
|
5643
|
-
if (accept === undefined) {
|
|
5644
|
-
throw new BridgeError("CODEX_ENDPOINT_REFRESH_ACTIVATION_UNAVAILABLE", "The Codex provider cannot acknowledge the validated endpoint transition.");
|
|
5645
|
-
}
|
|
5646
|
-
accept.call(adapter, event.attestation);
|
|
5899
|
+
adapter.acceptCompatibilityAttestation(event.attestation);
|
|
5647
5900
|
}
|
|
5648
5901
|
catch (error) {
|
|
5649
5902
|
try {
|
|
@@ -5665,6 +5918,7 @@ export class GatewayService {
|
|
|
5665
5918
|
}
|
|
5666
5919
|
this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
|
|
5667
5920
|
this.activeCodexIncompatibility = undefined;
|
|
5921
|
+
this.incompatibleProviderSurfaces.delete(this.providerSurfaceKey("codex", event.current.hostId));
|
|
5668
5922
|
return false;
|
|
5669
5923
|
}
|
|
5670
5924
|
async onRouteState(source, event) {
|
|
@@ -5726,6 +5980,9 @@ export class GatewayService {
|
|
|
5726
5980
|
: event.state === "awaiting_approval"
|
|
5727
5981
|
? "waiting"
|
|
5728
5982
|
: "busy");
|
|
5983
|
+
if (previousState === "stale" && event.state !== "stale") {
|
|
5984
|
+
adapter.rearmEndpointRefreshActivation?.(binding.endpointGeneration);
|
|
5985
|
+
}
|
|
5729
5986
|
}
|
|
5730
5987
|
if (event.state === "idle" || event.state === "stale") {
|
|
5731
5988
|
const heldTimer = this.heldRedispatchTimers.get(alias);
|
|
@@ -5841,14 +6098,14 @@ export class GatewayService {
|
|
|
5841
6098
|
async publicSnapshotLocked() {
|
|
5842
6099
|
const base = await this.store.publicSnapshot();
|
|
5843
6100
|
const compatibilityChecks = [];
|
|
5844
|
-
for (const surface of
|
|
6101
|
+
for (const { surface } of this.compatibilitySurfaceDefinitions) {
|
|
5845
6102
|
if (surface === "codex" && this.activeCodexIncompatibility !== undefined) {
|
|
5846
6103
|
compatibilityChecks.push(structuredClone(this.activeCodexIncompatibility));
|
|
5847
6104
|
continue;
|
|
5848
6105
|
}
|
|
5849
6106
|
const currentVersions = [
|
|
5850
|
-
...new Set(this.adapters.flatMap((
|
|
5851
|
-
const observation =
|
|
6107
|
+
...new Set([...this.adapters, ...this.compatibilityObservers].flatMap((observer) => {
|
|
6108
|
+
const observation = observer.compatibilitySurface?.();
|
|
5852
6109
|
return observation?.surface === surface ? [observation.version] : [];
|
|
5853
6110
|
})),
|
|
5854
6111
|
].sort((left, right) => left.localeCompare(right));
|
|
@@ -5891,7 +6148,12 @@ export class GatewayService {
|
|
|
5891
6148
|
});
|
|
5892
6149
|
return projectGatewayPublicSnapshot({
|
|
5893
6150
|
...base,
|
|
5894
|
-
|
|
6151
|
+
connectors: this.connectorsWithRegistry(base.connectors),
|
|
6152
|
+
...(compatibilityChecks.length === 0
|
|
6153
|
+
? {}
|
|
6154
|
+
: {
|
|
6155
|
+
compatibilityChecks: compatibilityChecks.map(projectPublicCompatibilityCheck),
|
|
6156
|
+
}),
|
|
5895
6157
|
availablePeers: peers,
|
|
5896
6158
|
activityEvents: this.activityEvents.map((event) => ({
|
|
5897
6159
|
...event,
|