agent-embassy 1.0.0 → 1.2.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 +44 -0
- package/README.md +50 -5
- package/README.zh-CN.md +32 -5
- package/SECURITY.md +26 -2
- package/assets/live-dashboard/app.css +14 -5
- package/dist/src/gateway/claude-helper-protocol.d.ts +3 -0
- package/dist/src/gateway/claude-helper-protocol.js +15 -1
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -0
- package/dist/src/gateway/claude-helper-supervisor.js +25 -1
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +1 -0
- package/dist/src/gateway/cli-copy.en.js +1 -0
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -0
- package/dist/src/gateway/cli-copy.zh-CN.js +1 -0
- 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 +20 -7
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.js +6 -2
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +13 -0
- package/dist/src/gateway/codex-local-transport.js +21 -2
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/config.js +5 -3
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +11 -1
- package/dist/src/gateway/control.js +48 -14
- 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 +6 -0
- package/dist/src/gateway/dashboard-copy.en.js +11 -5
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +6 -0
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +6 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js +11 -5
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +4 -2
- package/dist/src/gateway/dashboard-model.js +6 -0
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.js +5 -3
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +48 -10
- package/dist/src/gateway/live-dashboard-command.js +10 -0
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +3 -0
- package/dist/src/gateway/live-dashboard-http.js +13 -0
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/live-dashboard-stream.js +26 -25
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +16 -0
- package/dist/src/gateway/provenance-envelope.js +80 -0
- package/dist/src/gateway/provenance-envelope.js.map +1 -0
- package/dist/src/gateway/providers.d.ts +43 -16
- package/dist/src/gateway/providers.js +688 -61
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +2 -0
- package/dist/src/gateway/server.js +9 -3
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +102 -10
- package/dist/src/gateway/service.js +1309 -54
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +39 -3
- package/dist/src/gateway/store.js +528 -26
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +90 -5
- package/dist/src/gateway/types.js +6 -0
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +98 -11
- package/package.json +3 -2
- package/skills/embassy-peer/SKILL.md +15 -1
|
@@ -3,11 +3,12 @@ import path from "node:path";
|
|
|
3
3
|
import { BridgeError } from "../errors.js";
|
|
4
4
|
import { CLAUDE_PEER_COMPATIBILITY, ClaudePeerAdapter, } from "./claude-peer.js";
|
|
5
5
|
import { startClaudeCompatibilityScratch, } from "./claude-compatibility-scratch.js";
|
|
6
|
-
import { compatibilityProbeNames, sharesCompatibilityMajor, } from "./compatibility.js";
|
|
6
|
+
import { certifiedCompatibilityVersions, compatibilityProbeNames, evaluateCompatibilityAttestation, sharesCompatibilityMajor, } from "./compatibility.js";
|
|
7
7
|
import { CODEX_APP_SERVER_WRITABLE_VERSIONS, CodexAppServerConnector, CodexConnectorError, } from "./codex-app-server.js";
|
|
8
8
|
import { LocalCodexTransportError } from "./codex-local-transport.js";
|
|
9
9
|
import { ClaudeNativeHelperSupervisor, } from "./claude-helper-supervisor.js";
|
|
10
10
|
import { isDashboardLocale } from "./locale.js";
|
|
11
|
+
import { composeProvenanceEnvelope } from "./provenance-envelope.js";
|
|
11
12
|
const LOCAL_HOST = "this-mac";
|
|
12
13
|
const NATIVE_CLAUDE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
13
14
|
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
@@ -22,10 +23,45 @@ const DEFAULT_CODEX_CLEANUP_TIMEOUT_MS = 5_000;
|
|
|
22
23
|
const DEFAULT_CODEX_CLEANUP_POLL_MS = 25;
|
|
23
24
|
const DEFAULT_CODEX_RECOVERY_INITIAL_MS = 250;
|
|
24
25
|
const DEFAULT_CODEX_RECOVERY_MAX_MS = 5_000;
|
|
26
|
+
const MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES = 3;
|
|
25
27
|
const COMPATIBILITY_PROBE_THREAD_ID = "00000000-0000-7000-8000-000000000000";
|
|
26
28
|
const COMPATIBILITY_TURN_TIMEOUT_MS = 60_000;
|
|
27
29
|
const CLAUDE_CERTIFICATION_TIMEOUT_MS = 10_000;
|
|
28
30
|
const CLAUDE_CERTIFICATION_DISCOVERY_POLL_MS = 100;
|
|
31
|
+
const CLAUDE_CLEAN_PREWRITE_RETRY_CODES = new Set([
|
|
32
|
+
"CLAUDE_PEER_TARGET_UNKNOWN",
|
|
33
|
+
"CLAUDE_PEER_TARGET_STALE",
|
|
34
|
+
"CLAUDE_PEER_TARGET_CHANGED",
|
|
35
|
+
"CLAUDE_PEER_WORKSPACE_UNATTESTED",
|
|
36
|
+
]);
|
|
37
|
+
function claudeCleanPrewriteResult(error) {
|
|
38
|
+
if (error instanceof BridgeError &&
|
|
39
|
+
error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
|
|
40
|
+
return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof BridgeError &&
|
|
43
|
+
CLAUDE_CLEAN_PREWRITE_RETRY_CODES.has(error.code)) {
|
|
44
|
+
return { state: "deferred", safeErrorCode: error.code };
|
|
45
|
+
}
|
|
46
|
+
const systemCode = typeof error === "object" &&
|
|
47
|
+
error !== null &&
|
|
48
|
+
"code" in error &&
|
|
49
|
+
typeof error.code === "string"
|
|
50
|
+
? error.code
|
|
51
|
+
: undefined;
|
|
52
|
+
return {
|
|
53
|
+
state: "failed",
|
|
54
|
+
safeErrorCode: error instanceof BridgeError
|
|
55
|
+
? error.code
|
|
56
|
+
: systemCode === "ENOENT"
|
|
57
|
+
? "CLAUDE_DISPATCH_PREWRITE_PATH_MISSING"
|
|
58
|
+
: systemCode === "EACCES" || systemCode === "EPERM"
|
|
59
|
+
? "CLAUDE_DISPATCH_PREWRITE_ACCESS_DENIED"
|
|
60
|
+
: systemCode === "ETIMEDOUT"
|
|
61
|
+
? "CLAUDE_DISPATCH_PREWRITE_TIMEOUT"
|
|
62
|
+
: "CLAUDE_DISPATCH_PREWRITE_FAILED",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
29
65
|
function passedProbe(name) {
|
|
30
66
|
return { name, outcome: "pass" };
|
|
31
67
|
}
|
|
@@ -835,7 +871,9 @@ export class LocalClaudeGatewayProvider {
|
|
|
835
871
|
const selectedAlias = this.selected.get(input.binding.routeHandle);
|
|
836
872
|
const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
|
|
837
873
|
if (input.authorization === "selected_route" &&
|
|
838
|
-
(selectedAlias === undefined ||
|
|
874
|
+
(selectedAlias === undefined ||
|
|
875
|
+
selectedAlias !== input.targetAlias ||
|
|
876
|
+
stateRoot === undefined)) {
|
|
839
877
|
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
840
878
|
}
|
|
841
879
|
if (input.authorization === "selected_route") {
|
|
@@ -895,8 +933,20 @@ export class LocalClaudeGatewayProvider {
|
|
|
895
933
|
this.nativeInbound.delete(input.binding.routeHandle);
|
|
896
934
|
return { state: "failed", safeErrorCode: "CLAUDE_NATIVE_REPLY_STALE" };
|
|
897
935
|
}
|
|
936
|
+
if (observedRoute.alias !== input.targetAlias) {
|
|
937
|
+
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
938
|
+
}
|
|
898
939
|
}
|
|
899
|
-
else
|
|
940
|
+
else {
|
|
941
|
+
const selectedAlias = this.selected.get(input.binding.routeHandle);
|
|
942
|
+
const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
|
|
943
|
+
if (selectedAlias === undefined ||
|
|
944
|
+
selectedAlias !== input.targetAlias ||
|
|
945
|
+
stateRoot === undefined) {
|
|
946
|
+
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (activeListener.registration?.alias !== input.sourceAlias) {
|
|
900
950
|
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
901
951
|
}
|
|
902
952
|
if (this.activeNativeCodexListener !== activeListener ||
|
|
@@ -912,15 +962,44 @@ export class LocalClaudeGatewayProvider {
|
|
|
912
962
|
this.providerIdByGatewayId.has(input.messageId)) {
|
|
913
963
|
return { state: "failed", safeErrorCode: "CLAUDE_RECEIPT_CAPACITY" };
|
|
914
964
|
}
|
|
915
|
-
|
|
916
|
-
|
|
965
|
+
let content;
|
|
966
|
+
try {
|
|
967
|
+
content = composeProvenanceEnvelope({
|
|
968
|
+
direction: "claude",
|
|
969
|
+
sourceAlias: input.sourceAlias,
|
|
970
|
+
targetAlias: input.targetAlias,
|
|
971
|
+
conversationId: input.conversationId,
|
|
972
|
+
body: input.text,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
if (error instanceof BridgeError &&
|
|
977
|
+
(error.code === "PROVENANCE_ENVELOPE_INVALID" ||
|
|
978
|
+
error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")) {
|
|
979
|
+
return { state: "failed", safeErrorCode: error.code };
|
|
980
|
+
}
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
917
983
|
if (input.authorization === "selected_route") {
|
|
984
|
+
const stateRoot = this.selectedStateRoots.get(input.binding.routeHandle);
|
|
985
|
+
if (stateRoot === undefined) {
|
|
986
|
+
return { state: "failed", safeErrorCode: "CLAUDE_ROUTE_UNAVAILABLE" };
|
|
987
|
+
}
|
|
918
988
|
this.selectedDispatchEpoch.set(input.binding.routeHandle, (this.selectedDispatchEpoch.get(input.binding.routeHandle) ?? 0) + 1);
|
|
919
|
-
//
|
|
920
|
-
// busy until a later
|
|
989
|
+
// The exact workspace attestation is refreshed before every send. A
|
|
990
|
+
// clean failure keeps the route busy until a later authoritative idle
|
|
991
|
+
// observation wakes the one bounded retry.
|
|
921
992
|
this.selectedObservationDirty.add(input.binding.routeHandle);
|
|
922
993
|
this.emitClaudeRouteObservation(input.binding.routeHandle, "busy");
|
|
994
|
+
try {
|
|
995
|
+
await this.peer.assertTargetWorkspaceDisjoint(input.binding.routeHandle, stateRoot);
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
return claudeCleanPrewriteResult(error);
|
|
999
|
+
}
|
|
923
1000
|
}
|
|
1001
|
+
this.providerIdByGatewayId.set(input.messageId, "");
|
|
1002
|
+
this.pendingTargetByGatewayId.set(input.messageId, input.binding.routeHandle);
|
|
924
1003
|
let providerId;
|
|
925
1004
|
let trackingFailed = false;
|
|
926
1005
|
let terminalDuringSend = false;
|
|
@@ -962,7 +1041,7 @@ export class LocalClaudeGatewayProvider {
|
|
|
962
1041
|
// listener's bounded timeout is authoritative and may settle it later.
|
|
963
1042
|
};
|
|
964
1043
|
try {
|
|
965
|
-
const result = await this.peer.send(input.binding.routeHandle,
|
|
1044
|
+
const result = await this.peer.send(input.binding.routeHandle, content, {
|
|
966
1045
|
listener: activeListener.listener,
|
|
967
1046
|
receiptDeadlineAt: deadline,
|
|
968
1047
|
onTransportStatus,
|
|
@@ -1017,11 +1096,7 @@ export class LocalClaudeGatewayProvider {
|
|
|
1017
1096
|
}
|
|
1018
1097
|
this.providerIdByGatewayId.delete(input.messageId);
|
|
1019
1098
|
this.pendingTargetByGatewayId.delete(input.messageId);
|
|
1020
|
-
|
|
1021
|
-
error.code === "CLAUDE_PEER_MESSAGE_EXPIRED") {
|
|
1022
|
-
return { state: "failed", safeErrorCode: "MESSAGE_EXPIRED" };
|
|
1023
|
-
}
|
|
1024
|
-
return { state: "failed", safeErrorCode: "CLAUDE_DISPATCH_REJECTED" };
|
|
1099
|
+
return claudeCleanPrewriteResult(error);
|
|
1025
1100
|
}
|
|
1026
1101
|
}
|
|
1027
1102
|
async releaseRoute(routeHandle) {
|
|
@@ -1585,13 +1660,12 @@ function validateCodexFactory(factory) {
|
|
|
1585
1660
|
const schema = factory.schemaCompatibility;
|
|
1586
1661
|
const write = factory.writeCompatibility;
|
|
1587
1662
|
const certified = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(factory.appServerVersion);
|
|
1588
|
-
const
|
|
1589
|
-
schema.observedSchemaCandidate === true &&
|
|
1663
|
+
const schemaCandidate = schema.observedSchemaCandidate === true &&
|
|
1590
1664
|
CODEX_APP_SERVER_WRITABLE_VERSIONS.some((version) => sharesCompatibilityMajor(version, factory.appServerVersion));
|
|
1591
1665
|
if (factory.hostId !== LOCAL_HOST ||
|
|
1592
1666
|
factory.protocol !== "codex-app-server" ||
|
|
1593
1667
|
factory.protocolVersion !== factory.appServerVersion ||
|
|
1594
|
-
(!certified && !
|
|
1668
|
+
(!certified && !schemaCandidate) ||
|
|
1595
1669
|
schema.appServerVersion !== factory.appServerVersion ||
|
|
1596
1670
|
schema.endpointGeneration !== factory.endpointGeneration ||
|
|
1597
1671
|
schema.protocol !== "app-server-v2-stable" ||
|
|
@@ -1638,10 +1712,9 @@ function codexRouteSafeCode(observation) {
|
|
|
1638
1712
|
return undefined;
|
|
1639
1713
|
}
|
|
1640
1714
|
export class LocalCodexGatewayProvider {
|
|
1641
|
-
identity;
|
|
1642
|
-
protocol;
|
|
1643
|
-
protocolVersion;
|
|
1644
1715
|
factory;
|
|
1716
|
+
refreshFactory;
|
|
1717
|
+
compatibilityPolicy;
|
|
1645
1718
|
maxCallbacks;
|
|
1646
1719
|
maxRoutes;
|
|
1647
1720
|
maxReplyBytes;
|
|
@@ -1651,32 +1724,41 @@ export class LocalCodexGatewayProvider {
|
|
|
1651
1724
|
recoveryMaxMs;
|
|
1652
1725
|
now;
|
|
1653
1726
|
routes = new Map();
|
|
1727
|
+
/** Exact public alias authority retained across internal route recovery. */
|
|
1728
|
+
routeAliases = new Map();
|
|
1654
1729
|
routeCreations = new Map();
|
|
1655
1730
|
routeReleases = new Map();
|
|
1656
1731
|
routeRecoveryAttempts = new Map();
|
|
1657
1732
|
routeRecoveryTimers = new Map();
|
|
1658
1733
|
routeRecoveries = new Map();
|
|
1659
1734
|
routesRequiringRecovery = new Set();
|
|
1735
|
+
trackedRoutes = new Set();
|
|
1736
|
+
retiredEndpointGeneration;
|
|
1660
1737
|
expectsReply = new Map();
|
|
1661
1738
|
compatibilityCertifyingRoutes = new Set();
|
|
1662
1739
|
compatibilityTurns = new Map();
|
|
1663
1740
|
callbackQueue = [];
|
|
1664
1741
|
callbackScheduled = false;
|
|
1665
1742
|
callbacks;
|
|
1743
|
+
endpointRefresh;
|
|
1744
|
+
endpointRefreshDelivery;
|
|
1745
|
+
endpointActivationRetry;
|
|
1746
|
+
endpointActivationRetryTimer;
|
|
1747
|
+
callbackDrainFrozen = false;
|
|
1666
1748
|
compatibilityAttested;
|
|
1749
|
+
endpointUnavailable = false;
|
|
1750
|
+
pendingEndpointAttestation;
|
|
1751
|
+
pendingEndpointRefreshEvent;
|
|
1667
1752
|
initialized = false;
|
|
1668
1753
|
closing = false;
|
|
1669
1754
|
closed = false;
|
|
1670
1755
|
constructor(options) {
|
|
1671
1756
|
validateCodexFactory(options.factory);
|
|
1672
1757
|
this.factory = options.factory;
|
|
1673
|
-
this.
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
};
|
|
1678
|
-
this.protocol = options.factory.protocol;
|
|
1679
|
-
this.protocolVersion = options.factory.protocolVersion;
|
|
1758
|
+
this.refreshFactory = options.refreshFactory;
|
|
1759
|
+
this.compatibilityPolicy =
|
|
1760
|
+
options.compatibilityPolicy ?? options.factory.compatibilityPolicy;
|
|
1761
|
+
exactLocalHost(options.factory.hostId);
|
|
1680
1762
|
this.compatibilityAttested = CODEX_APP_SERVER_WRITABLE_VERSIONS.includes(options.factory.appServerVersion);
|
|
1681
1763
|
this.maxCallbacks = positiveBounded(options.maxCallbackEvents, MAX_CODEX_CALLBACKS, 4_096);
|
|
1682
1764
|
this.maxRoutes = positiveBounded(options.maxRoutes, MAX_CODEX_ROUTES, 256);
|
|
@@ -1693,30 +1775,60 @@ export class LocalCodexGatewayProvider {
|
|
|
1693
1775
|
}
|
|
1694
1776
|
this.now = options.now ?? (() => new Date());
|
|
1695
1777
|
}
|
|
1778
|
+
get identity() {
|
|
1779
|
+
return {
|
|
1780
|
+
provider: "codex",
|
|
1781
|
+
hostId: exactLocalHost(this.factory.hostId),
|
|
1782
|
+
endpointGeneration: this.factory.endpointGeneration,
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
get protocol() {
|
|
1786
|
+
return this.factory.protocol;
|
|
1787
|
+
}
|
|
1788
|
+
get protocolVersion() {
|
|
1789
|
+
return this.factory.protocolVersion;
|
|
1790
|
+
}
|
|
1696
1791
|
compatibilitySurface() {
|
|
1697
1792
|
return { surface: "codex", version: this.factory.appServerVersion };
|
|
1698
1793
|
}
|
|
1699
1794
|
acceptCompatibilityAttestation(attestation) {
|
|
1795
|
+
const pending = this.pendingEndpointAttestation;
|
|
1700
1796
|
if (attestation.surface !== "codex" ||
|
|
1701
1797
|
attestation.version !== this.factory.appServerVersion ||
|
|
1702
|
-
attestation.tier === "incompatible"
|
|
1798
|
+
attestation.tier === "incompatible" ||
|
|
1799
|
+
(this.endpointUnavailable && pending === undefined) ||
|
|
1800
|
+
(pending !== undefined &&
|
|
1801
|
+
(attestation.checkedAt !== pending.checkedAt ||
|
|
1802
|
+
attestation.tier !== pending.tier ||
|
|
1803
|
+
attestation.safeErrorCode !== pending.safeErrorCode ||
|
|
1804
|
+
attestation.probes.length !== pending.probes.length ||
|
|
1805
|
+
attestation.probes.some((probe, index) => probe.name !== pending.probes[index]?.name ||
|
|
1806
|
+
probe.outcome !== pending.probes[index]?.outcome ||
|
|
1807
|
+
probe.safeErrorCode !== pending.probes[index]?.safeErrorCode)))) {
|
|
1703
1808
|
throw new BridgeError("CODEX_COMPATIBILITY_ATTESTATION_MISMATCH", "The Codex compatibility attestation does not match this endpoint.");
|
|
1704
1809
|
}
|
|
1810
|
+
this.pendingEndpointAttestation = undefined;
|
|
1811
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
1812
|
+
this.clearEndpointActivationRetry();
|
|
1705
1813
|
this.compatibilityAttested = true;
|
|
1814
|
+
this.endpointUnavailable = false;
|
|
1706
1815
|
}
|
|
1707
1816
|
async runCompatibilityProbes() {
|
|
1817
|
+
return await this.runCompatibilityProbesFor(this.factory);
|
|
1818
|
+
}
|
|
1819
|
+
async runCompatibilityProbesFor(factory) {
|
|
1708
1820
|
const [installation, control, initialize, threadList] = compatibilityProbeNames.codex;
|
|
1709
1821
|
let transport;
|
|
1710
1822
|
let connector;
|
|
1711
1823
|
let stage = "transport";
|
|
1712
1824
|
try {
|
|
1713
|
-
transport = await
|
|
1825
|
+
transport = await factory.connectTransport();
|
|
1714
1826
|
stage = "initialize";
|
|
1715
1827
|
connector = await CodexAppServerConnector.connect({
|
|
1716
|
-
compatibility:
|
|
1828
|
+
compatibility: factory.schemaCompatibility,
|
|
1717
1829
|
writesEnabled: false,
|
|
1718
1830
|
route: {
|
|
1719
|
-
endpointGeneration:
|
|
1831
|
+
endpointGeneration: factory.endpointGeneration,
|
|
1720
1832
|
threadId: COMPATIBILITY_PROBE_THREAD_ID,
|
|
1721
1833
|
},
|
|
1722
1834
|
transport,
|
|
@@ -1727,7 +1839,6 @@ export class LocalCodexGatewayProvider {
|
|
|
1727
1839
|
});
|
|
1728
1840
|
stage = "thread_list";
|
|
1729
1841
|
await connector.observeLoadedThread(connector.guard());
|
|
1730
|
-
this.compatibilityAttested = true;
|
|
1731
1842
|
return [
|
|
1732
1843
|
passedProbe(installation),
|
|
1733
1844
|
passedProbe(control),
|
|
@@ -1736,6 +1847,8 @@ export class LocalCodexGatewayProvider {
|
|
|
1736
1847
|
];
|
|
1737
1848
|
}
|
|
1738
1849
|
catch (error) {
|
|
1850
|
+
if (this.isEndpointGenerationChanged(error))
|
|
1851
|
+
throw error;
|
|
1739
1852
|
if (stage === "transport") {
|
|
1740
1853
|
const controlFailure = error instanceof LocalCodexTransportError &&
|
|
1741
1854
|
[
|
|
@@ -1888,7 +2001,9 @@ export class LocalCodexGatewayProvider {
|
|
|
1888
2001
|
: { health: "healthy", compatibility: "compatible" };
|
|
1889
2002
|
}
|
|
1890
2003
|
async selectRoute(input) {
|
|
1891
|
-
this.
|
|
2004
|
+
if (!this.initialized || this.closing || this.closed) {
|
|
2005
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
2006
|
+
}
|
|
1892
2007
|
if (!PUBLIC_ALIAS.test(input.alias) ||
|
|
1893
2008
|
!input.alias.endsWith(`@${this.identity.hostId}`)) {
|
|
1894
2009
|
throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex alias is outside the exact local provider boundary.");
|
|
@@ -1896,25 +2011,116 @@ export class LocalCodexGatewayProvider {
|
|
|
1896
2011
|
if (!OPAQUE_ROUTE.test(input.routeHandle)) {
|
|
1897
2012
|
throw new BridgeError("CODEX_ROUTE_INVALID", "The Codex task identifier is outside the exact provider boundary.");
|
|
1898
2013
|
}
|
|
2014
|
+
const retainedAlias = this.routeAliases.get(input.routeHandle);
|
|
2015
|
+
if (retainedAlias !== undefined && retainedAlias !== input.alias) {
|
|
2016
|
+
throw new BridgeError("CODEX_ALIAS_MISMATCH", "The Codex route is already bound to another exact public alias.");
|
|
2017
|
+
}
|
|
1899
2018
|
this.cancelRouteRecovery(input.routeHandle);
|
|
1900
|
-
|
|
2019
|
+
let refreshResult;
|
|
2020
|
+
let selectorRefreshReserved = false;
|
|
2021
|
+
let route;
|
|
2022
|
+
const stageRefreshedRoute = async (result) => {
|
|
2023
|
+
if (result === undefined ||
|
|
2024
|
+
result.event.outcome !== "compatible") {
|
|
2025
|
+
throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex endpoint replacement was not compatibility-attested.");
|
|
2026
|
+
}
|
|
2027
|
+
refreshResult = result;
|
|
2028
|
+
selectorRefreshReserved = this.reserveSelectorRefresh(result);
|
|
2029
|
+
if (!selectorRefreshReserved) {
|
|
2030
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
|
|
2031
|
+
}
|
|
2032
|
+
try {
|
|
2033
|
+
return await this.ensureRoute(input.routeHandle, false, true);
|
|
2034
|
+
}
|
|
2035
|
+
catch (selectionError) {
|
|
2036
|
+
this.emitSelectorRefreshFallback(result, input.routeHandle);
|
|
2037
|
+
throw selectionError;
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
if (this.endpointRefresh !== undefined) {
|
|
2041
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
2042
|
+
}
|
|
2043
|
+
else if (this.endpointUnavailable &&
|
|
2044
|
+
this.pendingEndpointAttestation === undefined) {
|
|
2045
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
2046
|
+
}
|
|
2047
|
+
else {
|
|
2048
|
+
this.assertReady();
|
|
2049
|
+
try {
|
|
2050
|
+
route = await this.ensureRoute(input.routeHandle);
|
|
2051
|
+
}
|
|
2052
|
+
catch (error) {
|
|
2053
|
+
const refreshPending = error instanceof BridgeError &&
|
|
2054
|
+
error.code === "CODEX_ENDPOINT_REFRESH_PENDING";
|
|
2055
|
+
if (!this.isEndpointGenerationChanged(error) && !refreshPending) {
|
|
2056
|
+
throw error;
|
|
2057
|
+
}
|
|
2058
|
+
if (this.isEndpointGenerationChanged(error)) {
|
|
2059
|
+
this.endpointUnavailable = true;
|
|
2060
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
2061
|
+
}
|
|
2062
|
+
else if (this.endpointRefresh !== undefined) {
|
|
2063
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
2064
|
+
}
|
|
2065
|
+
else {
|
|
2066
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (refreshResult === undefined &&
|
|
2070
|
+
(route.endpointGeneration !== this.factory.endpointGeneration ||
|
|
2071
|
+
this.endpointUnavailable ||
|
|
2072
|
+
this.endpointRefresh !== undefined)) {
|
|
2073
|
+
if (this.endpointRefresh === undefined) {
|
|
2074
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The Codex endpoint replacement is awaiting controller activation.", true);
|
|
2075
|
+
}
|
|
2076
|
+
route = await stageRefreshedRoute(await this.refreshEndpoint("selector"));
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
1901
2079
|
const observation = route.connector.observation();
|
|
1902
2080
|
const state = codexRouteState(observation);
|
|
1903
2081
|
if (state === "stale") {
|
|
1904
|
-
|
|
2082
|
+
if (refreshResult !== undefined && selectorRefreshReserved) {
|
|
2083
|
+
// Publish the transition before any asynchronous connector cleanup so
|
|
2084
|
+
// the controller can accept or durably reject the exact replacement
|
|
2085
|
+
// generation. A route which closed after refresh staging is not valid
|
|
2086
|
+
// re-anchoring evidence and must be removed from both the callback and
|
|
2087
|
+
// its retained retry authority.
|
|
2088
|
+
this.emitSelectorRefreshFallback(refreshResult, input.routeHandle);
|
|
2089
|
+
}
|
|
2090
|
+
this.routesRequiringRecovery.add(input.routeHandle);
|
|
2091
|
+
await this.releaseRouteInternal(input.routeHandle);
|
|
2092
|
+
if (this.trackedRoutes.has(input.routeHandle)) {
|
|
2093
|
+
this.scheduleRouteRecovery(input.routeHandle);
|
|
2094
|
+
}
|
|
1905
2095
|
throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task connection closed during route selection.");
|
|
1906
2096
|
}
|
|
1907
|
-
this.
|
|
2097
|
+
this.trackedRoutes.add(input.routeHandle);
|
|
2098
|
+
this.routeAliases.set(input.routeHandle, input.alias);
|
|
2099
|
+
if (refreshResult === undefined) {
|
|
2100
|
+
this.queueRouteObservation(route, observation);
|
|
2101
|
+
}
|
|
2102
|
+
const endpointRefresh = refreshResult === undefined || !selectorRefreshReserved
|
|
2103
|
+
? undefined
|
|
2104
|
+
: refreshResult.event;
|
|
2105
|
+
if (endpointRefresh !== undefined) {
|
|
2106
|
+
this.armEndpointActivationRetry(endpointRefresh);
|
|
2107
|
+
}
|
|
1908
2108
|
return {
|
|
1909
2109
|
routeHandle: input.routeHandle,
|
|
1910
2110
|
state,
|
|
2111
|
+
...(endpointRefresh === undefined ? {} : { endpointRefresh }),
|
|
1911
2112
|
};
|
|
1912
2113
|
}
|
|
1913
2114
|
observeRouteSuccessionBarrier(routeHandle) {
|
|
1914
|
-
this.
|
|
2115
|
+
if (!this.initialized || this.closing || this.closed) {
|
|
2116
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
2117
|
+
}
|
|
1915
2118
|
const route = this.routes.get(routeHandle);
|
|
1916
2119
|
const observation = route?.connector.observation();
|
|
1917
|
-
const routeCreationInFlight = this.routeCreations.has(routeHandle)
|
|
2120
|
+
const routeCreationInFlight = this.routeCreations.has(routeHandle) ||
|
|
2121
|
+
this.routeRecoveries.has(routeHandle) ||
|
|
2122
|
+
(this.endpointRefresh !== undefined && this.trackedRoutes.has(routeHandle)) ||
|
|
2123
|
+
this.pendingEndpointActivationClaims(routeHandle);
|
|
1918
2124
|
const routeReleaseInFlight = this.routeReleases.has(routeHandle);
|
|
1919
2125
|
const pendingReplyCorrelations = this.expectsReply.size;
|
|
1920
2126
|
const pendingCallbacks = Math.max(this.callbackQueue.length, this.callbackScheduled ? 1 : 0);
|
|
@@ -1951,6 +2157,8 @@ export class LocalCodexGatewayProvider {
|
|
|
1951
2157
|
if (!this.initialized ||
|
|
1952
2158
|
this.closing ||
|
|
1953
2159
|
this.closed ||
|
|
2160
|
+
this.endpointUnavailable ||
|
|
2161
|
+
this.endpointRefresh !== undefined ||
|
|
1954
2162
|
!this.compatibilityAttested ||
|
|
1955
2163
|
input.authorization !== "selected_route" ||
|
|
1956
2164
|
!sameEndpoint(input.binding, this.identity)) {
|
|
@@ -1958,7 +2166,10 @@ export class LocalCodexGatewayProvider {
|
|
|
1958
2166
|
}
|
|
1959
2167
|
const route = this.routes.get(input.binding.routeHandle);
|
|
1960
2168
|
if (route === undefined ||
|
|
1961
|
-
this.
|
|
2169
|
+
this.routeAliases.get(input.binding.routeHandle) !== input.targetAlias ||
|
|
2170
|
+
this.routeReleases.has(input.binding.routeHandle) ||
|
|
2171
|
+
route.endpointGeneration !== input.binding.endpointGeneration ||
|
|
2172
|
+
route.endpointGeneration !== this.factory.endpointGeneration) {
|
|
1962
2173
|
return { state: "failed", safeErrorCode: "CODEX_ROUTE_UNAVAILABLE" };
|
|
1963
2174
|
}
|
|
1964
2175
|
if (this.compatibilityCertifyingRoutes.has(input.binding.routeHandle)) {
|
|
@@ -1978,13 +2189,31 @@ export class LocalCodexGatewayProvider {
|
|
|
1978
2189
|
if (this.expectsReply.has(input.messageId)) {
|
|
1979
2190
|
return { state: "failed", safeErrorCode: "CODEX_MESSAGE_DUPLICATE" };
|
|
1980
2191
|
}
|
|
2192
|
+
let content;
|
|
2193
|
+
try {
|
|
2194
|
+
content = composeProvenanceEnvelope({
|
|
2195
|
+
direction: "codex",
|
|
2196
|
+
sourceAlias: input.sourceAlias,
|
|
2197
|
+
targetAlias: input.targetAlias,
|
|
2198
|
+
conversationId: input.conversationId,
|
|
2199
|
+
body: input.text,
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
catch (error) {
|
|
2203
|
+
const safeErrorCode = error instanceof BridgeError &&
|
|
2204
|
+
(error.code === "PROVENANCE_ENVELOPE_INVALID" ||
|
|
2205
|
+
error.code === "PROVENANCE_ENVELOPE_TOO_LARGE")
|
|
2206
|
+
? error.code
|
|
2207
|
+
: "PROVENANCE_ENVELOPE_INVALID";
|
|
2208
|
+
return { state: "failed", safeErrorCode };
|
|
2209
|
+
}
|
|
1981
2210
|
this.expectsReply.set(input.messageId, input.expectsReply);
|
|
1982
2211
|
try {
|
|
1983
2212
|
const disposition = await route.connector.submitMessage(guard, {
|
|
1984
2213
|
deadlineAt: input.deadlineAt,
|
|
1985
2214
|
messageId: input.messageId,
|
|
1986
2215
|
...(input.steer === true ? { steer: true } : {}),
|
|
1987
|
-
text:
|
|
2216
|
+
text: content,
|
|
1988
2217
|
});
|
|
1989
2218
|
if (disposition.disposition === "deferred") {
|
|
1990
2219
|
this.expectsReply.delete(input.messageId);
|
|
@@ -2019,11 +2248,22 @@ export class LocalCodexGatewayProvider {
|
|
|
2019
2248
|
}
|
|
2020
2249
|
}
|
|
2021
2250
|
async releaseRoute(routeHandle) {
|
|
2251
|
+
if (this.pendingEndpointActivationClaims(routeHandle)) {
|
|
2252
|
+
throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route still belongs to an unaccepted endpoint transition.", true);
|
|
2253
|
+
}
|
|
2254
|
+
this.trackedRoutes.delete(routeHandle);
|
|
2022
2255
|
this.cancelRouteRecovery(routeHandle);
|
|
2023
2256
|
const recovery = this.routeRecoveries.get(routeHandle);
|
|
2024
2257
|
if (recovery !== undefined)
|
|
2025
2258
|
await recovery;
|
|
2259
|
+
const endpointRefresh = this.endpointRefresh;
|
|
2260
|
+
if (endpointRefresh !== undefined)
|
|
2261
|
+
await endpointRefresh;
|
|
2262
|
+
if (this.pendingEndpointActivationClaims(routeHandle)) {
|
|
2263
|
+
throw new BridgeError("CODEX_ENDPOINT_ACTIVATION_PENDING", "The exact Codex route acquired endpoint-transition authority during release.", true);
|
|
2264
|
+
}
|
|
2026
2265
|
await this.releaseRouteInternal(routeHandle);
|
|
2266
|
+
this.routeAliases.delete(routeHandle);
|
|
2027
2267
|
}
|
|
2028
2268
|
async releaseRouteInternal(routeHandle) {
|
|
2029
2269
|
const pending = this.routeReleases.get(routeHandle);
|
|
@@ -2053,6 +2293,12 @@ export class LocalCodexGatewayProvider {
|
|
|
2053
2293
|
clearTimeout(timer);
|
|
2054
2294
|
this.routeRecoveryTimers.clear();
|
|
2055
2295
|
this.routesRequiringRecovery.clear();
|
|
2296
|
+
this.clearEndpointActivationRetry();
|
|
2297
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
2298
|
+
this.pendingEndpointAttestation = undefined;
|
|
2299
|
+
const refreshResult = this.endpointRefresh === undefined
|
|
2300
|
+
? []
|
|
2301
|
+
: await Promise.allSettled([this.endpointRefresh]);
|
|
2056
2302
|
const entries = [...this.routes.entries()];
|
|
2057
2303
|
const routeResults = await Promise.allSettled(entries.map(async ([routeHandle]) => this.releaseRoute(routeHandle)));
|
|
2058
2304
|
await Promise.allSettled([
|
|
@@ -2060,7 +2306,8 @@ export class LocalCodexGatewayProvider {
|
|
|
2060
2306
|
...this.routeCreations.values(),
|
|
2061
2307
|
]);
|
|
2062
2308
|
this.drainCallbackQueue();
|
|
2063
|
-
if (
|
|
2309
|
+
if (refreshResult.some((result) => result.status === "rejected") ||
|
|
2310
|
+
routeResults.some((result) => result.status === "rejected")) {
|
|
2064
2311
|
this.closing = false;
|
|
2065
2312
|
throw new BridgeError("CODEX_PROVIDER_CLEANUP_FAILED", "An exact owned Codex turn did not confirm termination.");
|
|
2066
2313
|
}
|
|
@@ -2085,6 +2332,10 @@ export class LocalCodexGatewayProvider {
|
|
|
2085
2332
|
}
|
|
2086
2333
|
this.compatibilityTurns.clear();
|
|
2087
2334
|
this.compatibilityCertifyingRoutes.clear();
|
|
2335
|
+
this.pendingEndpointAttestation = undefined;
|
|
2336
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
2337
|
+
this.trackedRoutes.clear();
|
|
2338
|
+
this.routeAliases.clear();
|
|
2088
2339
|
this.expectsReply.clear();
|
|
2089
2340
|
this.callbacks = undefined;
|
|
2090
2341
|
}
|
|
@@ -2092,16 +2343,25 @@ export class LocalCodexGatewayProvider {
|
|
|
2092
2343
|
if (!this.initialized ||
|
|
2093
2344
|
this.closing ||
|
|
2094
2345
|
this.closed ||
|
|
2346
|
+
this.endpointUnavailable ||
|
|
2347
|
+
this.endpointRefresh !== undefined ||
|
|
2095
2348
|
!this.compatibilityAttested) {
|
|
2096
2349
|
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
2097
2350
|
}
|
|
2098
2351
|
}
|
|
2099
|
-
async ensureRoute(threadId) {
|
|
2100
|
-
|
|
2352
|
+
async ensureRoute(threadId, queueInitialObservation = true, allowPendingEndpointAttestation = false) {
|
|
2353
|
+
if (allowPendingEndpointAttestation) {
|
|
2354
|
+
if (!this.initialized || this.closing || this.closed) {
|
|
2355
|
+
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider is unavailable.", true);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
this.assertReady();
|
|
2360
|
+
}
|
|
2101
2361
|
const pendingRelease = this.routeReleases.get(threadId);
|
|
2102
2362
|
if (pendingRelease !== undefined) {
|
|
2103
2363
|
await pendingRelease;
|
|
2104
|
-
return await this.ensureRoute(threadId);
|
|
2364
|
+
return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
|
|
2105
2365
|
}
|
|
2106
2366
|
const existing = this.routes.get(threadId);
|
|
2107
2367
|
if (existing !== undefined) {
|
|
@@ -2113,7 +2373,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2113
2373
|
// a fresh one. The identity-checked release lets automatic recovery and
|
|
2114
2374
|
// an explicit selector join the same cleanup safely.
|
|
2115
2375
|
await this.releaseRouteInternal(threadId);
|
|
2116
|
-
return await this.ensureRoute(threadId);
|
|
2376
|
+
return await this.ensureRoute(threadId, queueInitialObservation, allowPendingEndpointAttestation);
|
|
2117
2377
|
}
|
|
2118
2378
|
const pending = this.routeCreations.get(threadId);
|
|
2119
2379
|
if (pending !== undefined)
|
|
@@ -2121,42 +2381,51 @@ export class LocalCodexGatewayProvider {
|
|
|
2121
2381
|
if (this.routes.size + this.routeCreations.size >= this.maxRoutes) {
|
|
2122
2382
|
throw new BridgeError("CODEX_ROUTE_CAPACITY", "The bounded local Codex route table is full.", true);
|
|
2123
2383
|
}
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
const route = await creation;
|
|
2384
|
+
const admittedFactory = this.factory;
|
|
2385
|
+
const creation = (async () => {
|
|
2386
|
+
const route = await this.createRoute(threadId, admittedFactory, queueInitialObservation);
|
|
2128
2387
|
if (this.closing || this.closed) {
|
|
2129
2388
|
await this.closeRoute(route);
|
|
2130
2389
|
throw new BridgeError("CODEX_PROVIDER_UNAVAILABLE", "The local Codex provider closed during route creation.");
|
|
2131
2390
|
}
|
|
2391
|
+
if (!allowPendingEndpointAttestation &&
|
|
2392
|
+
(this.endpointUnavailable ||
|
|
2393
|
+
this.endpointRefresh !== undefined ||
|
|
2394
|
+
admittedFactory !== this.factory ||
|
|
2395
|
+
route.endpointGeneration !== this.factory.endpointGeneration)) {
|
|
2396
|
+
await this.closeRoute(route);
|
|
2397
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The Codex endpoint moved during exact route creation.", true);
|
|
2398
|
+
}
|
|
2132
2399
|
this.routes.set(threadId, route);
|
|
2133
2400
|
return route;
|
|
2401
|
+
})();
|
|
2402
|
+
this.routeCreations.set(threadId, creation);
|
|
2403
|
+
try {
|
|
2404
|
+
return await creation;
|
|
2134
2405
|
}
|
|
2135
2406
|
finally {
|
|
2136
2407
|
this.routeCreations.delete(threadId);
|
|
2137
2408
|
}
|
|
2138
2409
|
}
|
|
2139
|
-
async createRoute(threadId) {
|
|
2410
|
+
async createRoute(threadId, factory, queueInitialObservation) {
|
|
2140
2411
|
let transport;
|
|
2141
2412
|
let connector;
|
|
2142
2413
|
const generation = Symbol(threadId);
|
|
2143
2414
|
try {
|
|
2144
|
-
transport = await
|
|
2145
|
-
const writesEnabled =
|
|
2146
|
-
this.factory.writeCompatibility !== null;
|
|
2415
|
+
transport = await factory.connectTransport();
|
|
2416
|
+
const writesEnabled = factory.writableReady && factory.writeCompatibility !== null;
|
|
2147
2417
|
connector = await CodexAppServerConnector.connect({
|
|
2148
|
-
compatibility:
|
|
2149
|
-
this.factory.schemaCompatibility,
|
|
2418
|
+
compatibility: factory.writeCompatibility ?? factory.schemaCompatibility,
|
|
2150
2419
|
writesEnabled,
|
|
2151
2420
|
route: {
|
|
2152
|
-
endpointGeneration:
|
|
2421
|
+
endpointGeneration: factory.endpointGeneration,
|
|
2153
2422
|
threadId,
|
|
2154
2423
|
},
|
|
2155
2424
|
transport,
|
|
2156
2425
|
maxReplyBytes: this.maxReplyBytes,
|
|
2157
2426
|
now: this.now,
|
|
2158
2427
|
onEvent: (event) => this.onConnectorEvent(threadId, generation, event),
|
|
2159
|
-
onTurnResult: (result) => this.onTurnResult(result),
|
|
2428
|
+
onTurnResult: (result) => this.onTurnResult(factory.endpointGeneration, result),
|
|
2160
2429
|
});
|
|
2161
2430
|
const loaded = await connector.observeLoadedThread(connector.guard());
|
|
2162
2431
|
if (!loaded.selectedThreadLoaded) {
|
|
@@ -2165,11 +2434,14 @@ export class LocalCodexGatewayProvider {
|
|
|
2165
2434
|
await connector.resumeThread(connector.guard());
|
|
2166
2435
|
const route = {
|
|
2167
2436
|
connector,
|
|
2437
|
+
endpointGeneration: factory.endpointGeneration,
|
|
2168
2438
|
generation,
|
|
2169
2439
|
threadId,
|
|
2170
2440
|
transport,
|
|
2171
2441
|
};
|
|
2172
|
-
|
|
2442
|
+
if (queueInitialObservation) {
|
|
2443
|
+
this.queueRouteObservation(route, connector.observation());
|
|
2444
|
+
}
|
|
2173
2445
|
return route;
|
|
2174
2446
|
}
|
|
2175
2447
|
catch (error) {
|
|
@@ -2179,6 +2451,8 @@ export class LocalCodexGatewayProvider {
|
|
|
2179
2451
|
else if (transport !== undefined) {
|
|
2180
2452
|
await transport.close().catch(() => undefined);
|
|
2181
2453
|
}
|
|
2454
|
+
if (this.isEndpointGenerationChanged(error))
|
|
2455
|
+
throw error;
|
|
2182
2456
|
if (error instanceof BridgeError)
|
|
2183
2457
|
throw error;
|
|
2184
2458
|
if (error instanceof CodexConnectorError) {
|
|
@@ -2236,6 +2510,19 @@ export class LocalCodexGatewayProvider {
|
|
|
2236
2510
|
return;
|
|
2237
2511
|
}
|
|
2238
2512
|
try {
|
|
2513
|
+
if (this.endpointUnavailable) {
|
|
2514
|
+
if (this.pendingEndpointAttestation !== undefined) {
|
|
2515
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is awaiting controller activation.", true);
|
|
2516
|
+
}
|
|
2517
|
+
const refreshed = await this.refreshEndpoint("callback");
|
|
2518
|
+
if (refreshed?.event.outcome === "compatible" &&
|
|
2519
|
+
refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
|
|
2520
|
+
this.routesRequiringRecovery.delete(threadId);
|
|
2521
|
+
this.routeRecoveryAttempts.delete(threadId);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_PENDING", "The replacement Codex endpoint is not ready.", true);
|
|
2525
|
+
}
|
|
2239
2526
|
const current = this.routes.get(threadId);
|
|
2240
2527
|
if (current !== undefined &&
|
|
2241
2528
|
codexRouteState(current.connector.observation()) === "stale") {
|
|
@@ -2255,7 +2542,17 @@ export class LocalCodexGatewayProvider {
|
|
|
2255
2542
|
this.routeRecoveryAttempts.delete(threadId);
|
|
2256
2543
|
this.queueRouteObservation(replacement, replacement.connector.observation());
|
|
2257
2544
|
}
|
|
2258
|
-
catch {
|
|
2545
|
+
catch (error) {
|
|
2546
|
+
if (this.isEndpointGenerationChanged(error)) {
|
|
2547
|
+
this.endpointUnavailable = true;
|
|
2548
|
+
const refreshed = await this.refreshEndpoint("callback").catch(() => undefined);
|
|
2549
|
+
if (refreshed?.event.outcome === "compatible" &&
|
|
2550
|
+
refreshed.event.routes.some((route) => route.routeHandle === threadId)) {
|
|
2551
|
+
this.routesRequiringRecovery.delete(threadId);
|
|
2552
|
+
this.routeRecoveryAttempts.delete(threadId);
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2259
2556
|
this.routeRecoveryAttempts.set(threadId, (this.routeRecoveryAttempts.get(threadId) ?? 0) + 1);
|
|
2260
2557
|
}
|
|
2261
2558
|
}
|
|
@@ -2267,7 +2564,323 @@ export class LocalCodexGatewayProvider {
|
|
|
2267
2564
|
clearTimeout(timer);
|
|
2268
2565
|
this.routeRecoveryTimers.delete(threadId);
|
|
2269
2566
|
}
|
|
2270
|
-
|
|
2567
|
+
isEndpointGenerationChanged(error) {
|
|
2568
|
+
return (error instanceof LocalCodexTransportError &&
|
|
2569
|
+
error.code === "ENDPOINT_GENERATION_CHANGED");
|
|
2570
|
+
}
|
|
2571
|
+
pendingEndpointActivationClaims(routeHandle) {
|
|
2572
|
+
const event = this.pendingEndpointRefreshEvent;
|
|
2573
|
+
return (event !== undefined &&
|
|
2574
|
+
event.current.endpointGeneration === this.factory.endpointGeneration &&
|
|
2575
|
+
event.routes.some((route) => route.routeHandle === routeHandle));
|
|
2576
|
+
}
|
|
2577
|
+
async refreshEndpoint(delivery) {
|
|
2578
|
+
if (this.refreshFactory === undefined || this.closing || this.closed) {
|
|
2579
|
+
return undefined;
|
|
2580
|
+
}
|
|
2581
|
+
const existing = this.endpointRefresh;
|
|
2582
|
+
if (existing !== undefined) {
|
|
2583
|
+
// A selector already running inside the controller lock takes ownership
|
|
2584
|
+
// of the transition result. This avoids waiting on a callback that needs
|
|
2585
|
+
// that same lock while still ensuring exactly one activation path.
|
|
2586
|
+
if (delivery === "selector")
|
|
2587
|
+
this.endpointRefreshDelivery = "selector";
|
|
2588
|
+
return await existing;
|
|
2589
|
+
}
|
|
2590
|
+
this.endpointRefreshDelivery = delivery;
|
|
2591
|
+
const refresh = this.performEndpointRefresh();
|
|
2592
|
+
this.endpointRefresh = refresh;
|
|
2593
|
+
try {
|
|
2594
|
+
return await refresh;
|
|
2595
|
+
}
|
|
2596
|
+
finally {
|
|
2597
|
+
if (this.endpointRefresh === refresh) {
|
|
2598
|
+
this.endpointRefresh = undefined;
|
|
2599
|
+
this.endpointRefreshDelivery = undefined;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
async performEndpointRefresh() {
|
|
2604
|
+
const refreshFactory = this.refreshFactory;
|
|
2605
|
+
if (refreshFactory === undefined)
|
|
2606
|
+
return undefined;
|
|
2607
|
+
const previousFactory = this.factory;
|
|
2608
|
+
const previous = this.identity;
|
|
2609
|
+
let candidate;
|
|
2610
|
+
try {
|
|
2611
|
+
const resolved = await this.resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous);
|
|
2612
|
+
candidate = resolved.factory;
|
|
2613
|
+
const { attestation, identity: candidateIdentity } = resolved;
|
|
2614
|
+
await this.retireEndpointGeneration(previousFactory, previous.endpointGeneration);
|
|
2615
|
+
if (attestation.tier === "incompatible") {
|
|
2616
|
+
await candidate.close();
|
|
2617
|
+
candidate = undefined;
|
|
2618
|
+
this.compatibilityAttested = false;
|
|
2619
|
+
this.pendingEndpointAttestation = undefined;
|
|
2620
|
+
this.pendingEndpointRefreshEvent = undefined;
|
|
2621
|
+
for (const routeHandle of this.trackedRoutes) {
|
|
2622
|
+
this.routesRequiringRecovery.add(routeHandle);
|
|
2623
|
+
}
|
|
2624
|
+
if (this.closing || this.closed)
|
|
2625
|
+
return undefined;
|
|
2626
|
+
const result = {
|
|
2627
|
+
event: {
|
|
2628
|
+
outcome: "incompatible",
|
|
2629
|
+
previous,
|
|
2630
|
+
candidate: candidateIdentity,
|
|
2631
|
+
attestation,
|
|
2632
|
+
},
|
|
2633
|
+
delivery: "callback",
|
|
2634
|
+
selectorClaimed: true,
|
|
2635
|
+
};
|
|
2636
|
+
this.emitEndpointRefresh(result.event);
|
|
2637
|
+
for (const routeHandle of this.routesRequiringRecovery) {
|
|
2638
|
+
if (this.trackedRoutes.has(routeHandle)) {
|
|
2639
|
+
this.scheduleRouteRecovery(routeHandle);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
return result;
|
|
2643
|
+
}
|
|
2644
|
+
if (this.closing || this.closed) {
|
|
2645
|
+
await candidate.close();
|
|
2646
|
+
candidate = undefined;
|
|
2647
|
+
return undefined;
|
|
2648
|
+
}
|
|
2649
|
+
this.factory = candidate;
|
|
2650
|
+
candidate = undefined;
|
|
2651
|
+
this.retiredEndpointGeneration = undefined;
|
|
2652
|
+
this.compatibilityAttested = false;
|
|
2653
|
+
this.endpointUnavailable = true;
|
|
2654
|
+
this.pendingEndpointAttestation = attestation;
|
|
2655
|
+
const refreshedRoutes = [];
|
|
2656
|
+
for (const routeHandle of [...this.trackedRoutes]) {
|
|
2657
|
+
if (this.closing || this.closed)
|
|
2658
|
+
break;
|
|
2659
|
+
try {
|
|
2660
|
+
const route = await this.createRoute(routeHandle, this.factory, false);
|
|
2661
|
+
if (this.closing || this.closed) {
|
|
2662
|
+
await this.closeRoute(route);
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
const state = codexRouteState(route.connector.observation());
|
|
2666
|
+
if (state === "stale") {
|
|
2667
|
+
await this.closeRoute(route);
|
|
2668
|
+
throw new BridgeError("CODEX_ROUTE_SETUP_REJECTED", "The exact Codex task was stale on the replacement endpoint.");
|
|
2669
|
+
}
|
|
2670
|
+
this.routes.set(routeHandle, route);
|
|
2671
|
+
this.routesRequiringRecovery.delete(routeHandle);
|
|
2672
|
+
this.routeRecoveryAttempts.delete(routeHandle);
|
|
2673
|
+
refreshedRoutes.push({ routeHandle, state });
|
|
2674
|
+
}
|
|
2675
|
+
catch {
|
|
2676
|
+
this.routesRequiringRecovery.add(routeHandle);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
if (this.closing || this.closed)
|
|
2680
|
+
return undefined;
|
|
2681
|
+
const result = {
|
|
2682
|
+
event: {
|
|
2683
|
+
outcome: "compatible",
|
|
2684
|
+
previous,
|
|
2685
|
+
current: this.identity,
|
|
2686
|
+
attestation,
|
|
2687
|
+
routes: refreshedRoutes,
|
|
2688
|
+
},
|
|
2689
|
+
delivery: this.endpointRefreshDelivery ?? "callback",
|
|
2690
|
+
selectorClaimed: false,
|
|
2691
|
+
};
|
|
2692
|
+
if (result.event.outcome === "compatible") {
|
|
2693
|
+
this.pendingEndpointRefreshEvent = result.event;
|
|
2694
|
+
}
|
|
2695
|
+
if (result.delivery === "callback") {
|
|
2696
|
+
result.selectorClaimed = true;
|
|
2697
|
+
this.publishEndpointRefresh(result.event);
|
|
2698
|
+
}
|
|
2699
|
+
for (const routeHandle of this.routesRequiringRecovery) {
|
|
2700
|
+
if (this.trackedRoutes.has(routeHandle)) {
|
|
2701
|
+
this.scheduleRouteRecovery(routeHandle);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return result;
|
|
2705
|
+
}
|
|
2706
|
+
catch (error) {
|
|
2707
|
+
if (candidate !== undefined && candidate !== previousFactory) {
|
|
2708
|
+
await candidate.close().catch(() => undefined);
|
|
2709
|
+
}
|
|
2710
|
+
throw error;
|
|
2711
|
+
}
|
|
2712
|
+
finally {
|
|
2713
|
+
if (this.callbackDrainFrozen) {
|
|
2714
|
+
this.callbackDrainFrozen = false;
|
|
2715
|
+
this.drainCallbackQueue();
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
async resolveEndpointRefreshCandidate(refreshFactory, previousFactory, previous) {
|
|
2720
|
+
for (let attempt = 0; attempt < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES; attempt += 1) {
|
|
2721
|
+
let candidate;
|
|
2722
|
+
try {
|
|
2723
|
+
candidate = await refreshFactory();
|
|
2724
|
+
validateCodexFactory(candidate);
|
|
2725
|
+
if (candidate.hostId !== previous.hostId ||
|
|
2726
|
+
candidate.protocol !== previousFactory.protocol ||
|
|
2727
|
+
candidate.compatibilityPolicy !== this.compatibilityPolicy) {
|
|
2728
|
+
throw new BridgeError("CODEX_FACTORY_ATTESTATION_INVALID", "The replacement Codex factory moved outside its pinned boundary.");
|
|
2729
|
+
}
|
|
2730
|
+
if (candidate === previousFactory ||
|
|
2731
|
+
candidate.endpointGeneration === previous.endpointGeneration) {
|
|
2732
|
+
if (candidate !== previousFactory)
|
|
2733
|
+
await candidate.close();
|
|
2734
|
+
candidate = undefined;
|
|
2735
|
+
if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
|
|
2736
|
+
continue;
|
|
2737
|
+
}
|
|
2738
|
+
throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
|
|
2739
|
+
}
|
|
2740
|
+
// Endpoint generations never inherit cached compatibility. Every
|
|
2741
|
+
// candidate independently proves all four read-only wire probes.
|
|
2742
|
+
const probes = await this.runCompatibilityProbesFor(candidate);
|
|
2743
|
+
const attestation = evaluateCompatibilityAttestation({
|
|
2744
|
+
surface: "codex",
|
|
2745
|
+
version: candidate.appServerVersion,
|
|
2746
|
+
checkedAt: this.now().toISOString(),
|
|
2747
|
+
policy: this.compatibilityPolicy,
|
|
2748
|
+
certifiedVersions: certifiedCompatibilityVersions.codex,
|
|
2749
|
+
probes,
|
|
2750
|
+
});
|
|
2751
|
+
return {
|
|
2752
|
+
factory: candidate,
|
|
2753
|
+
identity: {
|
|
2754
|
+
provider: "codex",
|
|
2755
|
+
hostId: previous.hostId,
|
|
2756
|
+
endpointGeneration: candidate.endpointGeneration,
|
|
2757
|
+
},
|
|
2758
|
+
attestation,
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
catch (error) {
|
|
2762
|
+
if (candidate !== undefined && candidate !== previousFactory) {
|
|
2763
|
+
await candidate.close();
|
|
2764
|
+
}
|
|
2765
|
+
if (this.isEndpointGenerationChanged(error)) {
|
|
2766
|
+
if (attempt + 1 < MAX_CODEX_ENDPOINT_REFRESH_CANDIDATES) {
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
|
|
2770
|
+
}
|
|
2771
|
+
throw error;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
throw new BridgeError("CODEX_ENDPOINT_GENERATION_CHURN", "The Codex endpoint did not stabilize within its bounded refresh window.", true);
|
|
2775
|
+
}
|
|
2776
|
+
async retireEndpointGeneration(factory, endpointGeneration) {
|
|
2777
|
+
this.callbackDrainFrozen = true;
|
|
2778
|
+
this.drainCallbackQueue(true);
|
|
2779
|
+
// Fence every operation admitted on the previous immutable factory.
|
|
2780
|
+
// Ordinary route creation is blocked by endpointRefresh; joining these
|
|
2781
|
+
// exact promises ensures none can be inserted after the close sweep.
|
|
2782
|
+
await Promise.allSettled([...this.routeCreations.values()]);
|
|
2783
|
+
await Promise.allSettled([...this.routeReleases.values()]);
|
|
2784
|
+
this.drainCallbackQueue(true);
|
|
2785
|
+
for (const [routeHandle, route] of [...this.routes]) {
|
|
2786
|
+
if (route.endpointGeneration !== endpointGeneration)
|
|
2787
|
+
continue;
|
|
2788
|
+
await this.closeRoute(route);
|
|
2789
|
+
if (this.routes.get(routeHandle) === route) {
|
|
2790
|
+
this.routes.delete(routeHandle);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
this.drainCallbackQueue(true);
|
|
2794
|
+
await factory.close();
|
|
2795
|
+
this.drainCallbackQueue(true);
|
|
2796
|
+
this.retiredEndpointGeneration = endpointGeneration;
|
|
2797
|
+
}
|
|
2798
|
+
reserveSelectorRefresh(result) {
|
|
2799
|
+
if (result.delivery !== "selector" || result.selectorClaimed) {
|
|
2800
|
+
return false;
|
|
2801
|
+
}
|
|
2802
|
+
result.selectorClaimed = true;
|
|
2803
|
+
return true;
|
|
2804
|
+
}
|
|
2805
|
+
emitSelectorRefreshFallback(result, staleRouteHandle) {
|
|
2806
|
+
if (result.delivery !== "selector" || !result.selectorClaimed)
|
|
2807
|
+
return;
|
|
2808
|
+
let event = result.event;
|
|
2809
|
+
if (staleRouteHandle !== undefined &&
|
|
2810
|
+
event.outcome === "compatible" &&
|
|
2811
|
+
event.routes.some((route) => route.routeHandle === staleRouteHandle)) {
|
|
2812
|
+
event = {
|
|
2813
|
+
...event,
|
|
2814
|
+
routes: event.routes.filter((route) => route.routeHandle !== staleRouteHandle),
|
|
2815
|
+
};
|
|
2816
|
+
result.event = event;
|
|
2817
|
+
this.pendingEndpointRefreshEvent = event;
|
|
2818
|
+
}
|
|
2819
|
+
this.publishEndpointRefresh(event);
|
|
2820
|
+
}
|
|
2821
|
+
publishEndpointRefresh(event) {
|
|
2822
|
+
this.emitEndpointRefresh(event);
|
|
2823
|
+
if (event.outcome === "compatible") {
|
|
2824
|
+
this.armEndpointActivationRetry(event);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
armEndpointActivationRetry(event) {
|
|
2828
|
+
this.clearEndpointActivationRetry();
|
|
2829
|
+
this.endpointActivationRetry = { event, attempt: 0 };
|
|
2830
|
+
this.scheduleEndpointActivationRetry();
|
|
2831
|
+
}
|
|
2832
|
+
scheduleEndpointActivationRetry() {
|
|
2833
|
+
const pending = this.endpointActivationRetry;
|
|
2834
|
+
if (pending === undefined ||
|
|
2835
|
+
this.closing ||
|
|
2836
|
+
this.closed ||
|
|
2837
|
+
pending.attempt >= 3 ||
|
|
2838
|
+
(pending.event.outcome === "compatible" &&
|
|
2839
|
+
this.pendingEndpointAttestation === undefined)) {
|
|
2840
|
+
this.clearEndpointActivationRetry();
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
const delay = Math.max(DEFAULT_CODEX_RECOVERY_INITIAL_MS, Math.min(this.recoveryMaxMs, this.recoveryInitialMs * 2 ** pending.attempt));
|
|
2844
|
+
const timer = setTimeout(() => {
|
|
2845
|
+
if (this.endpointActivationRetryTimer !== timer)
|
|
2846
|
+
return;
|
|
2847
|
+
this.endpointActivationRetryTimer = undefined;
|
|
2848
|
+
const current = this.endpointActivationRetry;
|
|
2849
|
+
if (current === undefined || current.event !== pending.event)
|
|
2850
|
+
return;
|
|
2851
|
+
if (current.event.outcome === "compatible" &&
|
|
2852
|
+
this.pendingEndpointAttestation === undefined) {
|
|
2853
|
+
this.clearEndpointActivationRetry();
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
this.emitEndpointRefresh(current.event);
|
|
2857
|
+
if (this.endpointActivationRetry === undefined ||
|
|
2858
|
+
this.endpointActivationRetry.event !== current.event) {
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
this.endpointActivationRetry = {
|
|
2862
|
+
event: current.event,
|
|
2863
|
+
attempt: current.attempt + 1,
|
|
2864
|
+
};
|
|
2865
|
+
this.scheduleEndpointActivationRetry();
|
|
2866
|
+
}, delay);
|
|
2867
|
+
timer.unref();
|
|
2868
|
+
this.endpointActivationRetryTimer = timer;
|
|
2869
|
+
}
|
|
2870
|
+
clearEndpointActivationRetry() {
|
|
2871
|
+
if (this.endpointActivationRetryTimer !== undefined) {
|
|
2872
|
+
clearTimeout(this.endpointActivationRetryTimer);
|
|
2873
|
+
}
|
|
2874
|
+
this.endpointActivationRetryTimer = undefined;
|
|
2875
|
+
this.endpointActivationRetry = undefined;
|
|
2876
|
+
}
|
|
2877
|
+
emitEndpointRefresh(event) {
|
|
2878
|
+
const callbacks = this.callbacks;
|
|
2879
|
+
if (callbacks?.onEndpointRefresh === undefined)
|
|
2880
|
+
return;
|
|
2881
|
+
invokeCallback(() => callbacks.onEndpointRefresh?.(event));
|
|
2882
|
+
}
|
|
2883
|
+
onTurnResult(endpointGeneration, result) {
|
|
2271
2884
|
const certification = this.compatibilityTurns.get(result.messageId);
|
|
2272
2885
|
if (certification !== undefined) {
|
|
2273
2886
|
clearTimeout(certification.timer);
|
|
@@ -2306,6 +2919,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2306
2919
|
: "CODEX_DELIVERY_AMBIGUOUS";
|
|
2307
2920
|
this.enqueueCodexCallback({
|
|
2308
2921
|
type: "delivery",
|
|
2922
|
+
endpointGeneration,
|
|
2309
2923
|
value: {
|
|
2310
2924
|
messageId: result.messageId,
|
|
2311
2925
|
state,
|
|
@@ -2317,6 +2931,7 @@ export class LocalCodexGatewayProvider {
|
|
|
2317
2931
|
queueRouteObservation(route, observation) {
|
|
2318
2932
|
this.enqueueCodexCallback({
|
|
2319
2933
|
type: "route",
|
|
2934
|
+
endpointGeneration: route.endpointGeneration,
|
|
2320
2935
|
generation: route.generation,
|
|
2321
2936
|
routeHandle: route.threadId,
|
|
2322
2937
|
state: codexRouteState(observation),
|
|
@@ -2340,6 +2955,8 @@ export class LocalCodexGatewayProvider {
|
|
|
2340
2955
|
}
|
|
2341
2956
|
}
|
|
2342
2957
|
this.callbackQueue.push(event);
|
|
2958
|
+
if (this.callbackDrainFrozen)
|
|
2959
|
+
return;
|
|
2343
2960
|
if (this.callbackScheduled)
|
|
2344
2961
|
return;
|
|
2345
2962
|
this.callbackScheduled = true;
|
|
@@ -2353,6 +2970,10 @@ export class LocalCodexGatewayProvider {
|
|
|
2353
2970
|
const callbacks = this.callbacks;
|
|
2354
2971
|
if (event === undefined || callbacks === undefined)
|
|
2355
2972
|
return;
|
|
2973
|
+
if (event.endpointGeneration !== this.factory.endpointGeneration ||
|
|
2974
|
+
this.retiredEndpointGeneration === event.endpointGeneration) {
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2356
2977
|
if (event.type === "delivery") {
|
|
2357
2978
|
invokeCallback(() => callbacks.onDelivery(event.value));
|
|
2358
2979
|
}
|
|
@@ -2361,7 +2982,11 @@ export class LocalCodexGatewayProvider {
|
|
|
2361
2982
|
return;
|
|
2362
2983
|
}
|
|
2363
2984
|
invokeCallback(() => callbacks.onRouteState({
|
|
2364
|
-
endpoint: callbackEndpoint(
|
|
2985
|
+
endpoint: callbackEndpoint({
|
|
2986
|
+
provider: "codex",
|
|
2987
|
+
hostId: this.identity.hostId,
|
|
2988
|
+
endpointGeneration: event.endpointGeneration,
|
|
2989
|
+
}, event.routeHandle),
|
|
2365
2990
|
state: event.state,
|
|
2366
2991
|
...(event.safeErrorCode === undefined
|
|
2367
2992
|
? {}
|
|
@@ -2369,7 +2994,9 @@ export class LocalCodexGatewayProvider {
|
|
|
2369
2994
|
}));
|
|
2370
2995
|
}
|
|
2371
2996
|
}
|
|
2372
|
-
drainCallbackQueue() {
|
|
2997
|
+
drainCallbackQueue(force = false) {
|
|
2998
|
+
if (this.callbackDrainFrozen && !force)
|
|
2999
|
+
return;
|
|
2373
3000
|
while (this.callbackQueue.length > 0)
|
|
2374
3001
|
this.drainOneCallback();
|
|
2375
3002
|
}
|