agent-embassy 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/CONTRIBUTING.md +25 -7
- package/README.md +66 -13
- package/README.zh-CN.md +41 -13
- package/SECURITY.md +80 -42
- package/assets/live-dashboard/app.css +14 -5
- package/dist/src/gateway/claude-helper-protocol.d.ts +3 -0
- package/dist/src/gateway/claude-helper-protocol.js +15 -1
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -0
- package/dist/src/gateway/claude-helper-supervisor.js +24 -1
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +0 -2
- package/dist/src/gateway/claude-runtime.js +6 -17
- package/dist/src/gateway/claude-runtime.js.map +1 -1
- package/dist/src/gateway/cli-copy.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +4 -1
- package/dist/src/gateway/cli-copy.en.js +6 -3
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +2 -0
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +4 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +6 -3
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +2 -5
- package/dist/src/gateway/cli.js +49 -73
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +1 -7
- package/dist/src/gateway/codex-app-server.js +10 -53
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +10 -3
- package/dist/src/gateway/codex-local-transport.js +25 -9
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +0 -27
- package/dist/src/gateway/compatibility.js +1 -101
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/config.d.ts +0 -3
- package/dist/src/gateway/config.js +4 -12
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +11 -20
- package/dist/src/gateway/control.js +50 -47
- package/dist/src/gateway/control.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.d.ts +1 -1
- package/dist/src/gateway/dashboard-copy.en.d.ts +12 -7
- package/dist/src/gateway/dashboard-copy.en.js +26 -21
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +12 -7
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +12 -7
- package/dist/src/gateway/dashboard-copy.zh-CN.js +26 -21
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +7 -9
- package/dist/src/gateway/dashboard-model.js +47 -16
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.js +10 -10
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +65 -55
- package/dist/src/gateway/live-dashboard-command.d.ts +5 -1
- package/dist/src/gateway/live-dashboard-command.js +18 -6
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +3 -4
- package/dist/src/gateway/live-dashboard-http.js +32 -93
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/live-dashboard-protocol.d.ts +1 -5
- package/dist/src/gateway/live-dashboard-protocol.js +0 -42
- package/dist/src/gateway/live-dashboard-protocol.js.map +1 -1
- package/dist/src/gateway/live-dashboard-server.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-server.js +18 -4
- package/dist/src/gateway/live-dashboard-server.js.map +1 -1
- package/dist/src/gateway/live-dashboard-stream.js +26 -25
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
- package/dist/src/gateway/live-dashboard.d.ts +3 -6
- package/dist/src/gateway/live-dashboard.js +6 -42
- package/dist/src/gateway/live-dashboard.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +16 -0
- package/dist/src/gateway/provenance-envelope.js +80 -0
- package/dist/src/gateway/provenance-envelope.js.map +1 -0
- package/dist/src/gateway/providers.d.ts +40 -33
- package/dist/src/gateway/providers.js +683 -302
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +2 -0
- package/dist/src/gateway/server.js +8 -5
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +103 -23
- package/dist/src/gateway/service.js +1346 -272
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +39 -3
- package/dist/src/gateway/store.js +604 -44
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +90 -9
- package/dist/src/gateway/types.js +6 -0
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +155 -56
- package/package.json +3 -2
- package/skills/embassy-peer/SKILL.md +36 -7
- package/dist/src/gateway/claude-compatibility-scratch.d.ts +0 -18
- package/dist/src/gateway/claude-compatibility-scratch.js +0 -158
- package/dist/src/gateway/claude-compatibility-scratch.js.map +0 -1
- package/dist/src/gateway/live-dashboard-bootstrap.d.ts +0 -36
- package/dist/src/gateway/live-dashboard-bootstrap.js +0 -176
- package/dist/src/gateway/live-dashboard-bootstrap.js.map +0 -1
- package/docs/DESIGN.md +0 -134
|
@@ -8,7 +8,7 @@ import { KeyedMutex } from "../mutex.js";
|
|
|
8
8
|
import { isCodexRegistrationGeneration } from "./codex-registration-generation.js";
|
|
9
9
|
import { compatibilityCacheKey, isCompatibilityAttestation, } from "./compatibility.js";
|
|
10
10
|
import { PROGRESS_WATCH_DEFAULT_CAPACITY, PROGRESS_WATCH_MAX_IDLE_MS, PROGRESS_WATCH_MIN_IDLE_MS, createProgressWatchMachine, progressWatchCapabilities, progressWatchJournalKinds, progressWatchPhases, transitionProgressWatch, } from "./progress-watch-machine.js";
|
|
11
|
-
import { compatibilityStates, connectorHealthStates, deliveryStates, gatewayProviders, messageDirections, projectGatewayPublicSnapshot, routeRegistrationModes, routeStates, } from "./types.js";
|
|
11
|
+
import { CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY, CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY, compatibilityStates, connectorHealthStates, deliveryStates, gatewayProviders, messageDirections, projectGatewayPublicSnapshot, routeRegistrationModes, routeStates, } from "./types.js";
|
|
12
12
|
const STATE_MARKER = ".agent-embassy-state";
|
|
13
13
|
const STATE_MARKER_CONTENT = "agent-embassy-state-v1\n";
|
|
14
14
|
const STATE_FILE = "gateway-state.json";
|
|
@@ -16,6 +16,7 @@ const CONTROLLER_LOCK = ".gateway-controller.lock";
|
|
|
16
16
|
const MAX_MARKER_FILE_BYTES = 128;
|
|
17
17
|
const MAX_LOCK_FILE_BYTES = 4 * 1024;
|
|
18
18
|
export const GATEWAY_MAX_STATE_FILE_BYTES = 8 * 1024 * 1024;
|
|
19
|
+
const DEFAULT_RETAINED_BODY_BYTES = 1 * 1024 * 1024;
|
|
19
20
|
const COMPATIBILITY_ATTESTATION_CAPACITY = 16;
|
|
20
21
|
const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
21
22
|
const HOST_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
@@ -153,6 +154,49 @@ function isPrivateRouteBinding(value) {
|
|
|
153
154
|
isPrivateToken(value.routeHandle) &&
|
|
154
155
|
isPrivateToken(value.ownerLease));
|
|
155
156
|
}
|
|
157
|
+
function isCodexEndpointRefreshJournalEvent(value) {
|
|
158
|
+
return (isObject(value) &&
|
|
159
|
+
hasOnlyKeys(value, [
|
|
160
|
+
"sequence",
|
|
161
|
+
"timestamp",
|
|
162
|
+
"alias",
|
|
163
|
+
"hostId",
|
|
164
|
+
"threadId",
|
|
165
|
+
"oldEndpointGeneration",
|
|
166
|
+
"newEndpointGeneration",
|
|
167
|
+
], ["reason"]) &&
|
|
168
|
+
isPositiveInteger(value.sequence) &&
|
|
169
|
+
isIsoTimestamp(value.timestamp) &&
|
|
170
|
+
typeof value.alias === "string" &&
|
|
171
|
+
ALIAS_PATTERN.test(value.alias) &&
|
|
172
|
+
value.alias.startsWith("codex-") &&
|
|
173
|
+
typeof value.hostId === "string" &&
|
|
174
|
+
HOST_PATTERN.test(value.hostId) &&
|
|
175
|
+
value.alias.endsWith(`@${value.hostId}`) &&
|
|
176
|
+
isPrivateToken(value.threadId) &&
|
|
177
|
+
isPrivateToken(value.oldEndpointGeneration) &&
|
|
178
|
+
isPrivateToken(value.newEndpointGeneration) &&
|
|
179
|
+
(value.reason === undefined
|
|
180
|
+
? value.oldEndpointGeneration !== value.newEndpointGeneration
|
|
181
|
+
: value.reason === "boot_reactivation"));
|
|
182
|
+
}
|
|
183
|
+
function isCodexOrphanRemovalJournalEvent(value) {
|
|
184
|
+
return (isObject(value) &&
|
|
185
|
+
hasOnlyKeys(value, [
|
|
186
|
+
"sequence",
|
|
187
|
+
"timestamp",
|
|
188
|
+
"alias",
|
|
189
|
+
"hostId",
|
|
190
|
+
]) &&
|
|
191
|
+
isPositiveInteger(value.sequence) &&
|
|
192
|
+
isIsoTimestamp(value.timestamp) &&
|
|
193
|
+
typeof value.alias === "string" &&
|
|
194
|
+
ALIAS_PATTERN.test(value.alias) &&
|
|
195
|
+
value.alias.startsWith("codex-") &&
|
|
196
|
+
typeof value.hostId === "string" &&
|
|
197
|
+
HOST_PATTERN.test(value.hostId) &&
|
|
198
|
+
value.alias.endsWith(`@${value.hostId}`));
|
|
199
|
+
}
|
|
156
200
|
function isRouteCounters(value) {
|
|
157
201
|
if (!isObject(value) ||
|
|
158
202
|
!hasOnlyKeys(value, [
|
|
@@ -348,8 +392,7 @@ function isQueuedMetadata(value) {
|
|
|
348
392
|
"enqueuedAt",
|
|
349
393
|
"deadlineAt",
|
|
350
394
|
"bytes",
|
|
351
|
-
|
|
352
|
-
], ["conversationIdSuffix", "pair", "steer"])) {
|
|
395
|
+
], ["conversationIdSuffix", "body", "pair", "transientTarget", "steer"])) {
|
|
353
396
|
return false;
|
|
354
397
|
}
|
|
355
398
|
return (typeof value.messageId === "string" &&
|
|
@@ -368,8 +411,13 @@ function isQueuedMetadata(value) {
|
|
|
368
411
|
isIsoTimestamp(value.enqueuedAt) &&
|
|
369
412
|
isIsoTimestamp(value.deadlineAt) &&
|
|
370
413
|
isPositiveInteger(value.bytes) &&
|
|
371
|
-
|
|
414
|
+
(value.body === undefined ||
|
|
415
|
+
(typeof value.body === "string" &&
|
|
416
|
+
value.body.length > 0 &&
|
|
417
|
+
!value.body.includes("\u0000") &&
|
|
418
|
+
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
372
419
|
(value.pair === undefined || value.pair === true) &&
|
|
420
|
+
(value.transientTarget === undefined || value.transientTarget === true) &&
|
|
373
421
|
(value.steer === undefined || value.steer === true));
|
|
374
422
|
}
|
|
375
423
|
function isInFlightMetadata(value) {
|
|
@@ -389,8 +437,7 @@ function isEvent(value) {
|
|
|
389
437
|
"targetAlias",
|
|
390
438
|
"state",
|
|
391
439
|
"bytes",
|
|
392
|
-
|
|
393
|
-
], ["conversationIdSuffix", "latencyMs", "safeErrorCode", "steer"])) {
|
|
440
|
+
], ["conversationIdSuffix", "body", "latencyMs", "safeErrorCode", "steer"])) {
|
|
394
441
|
return false;
|
|
395
442
|
}
|
|
396
443
|
return (isPositiveInteger(value.sequence) &&
|
|
@@ -409,7 +456,11 @@ function isEvent(value) {
|
|
|
409
456
|
typeof value.state === "string" &&
|
|
410
457
|
DELIVERY_STATES.has(value.state) &&
|
|
411
458
|
isPositiveInteger(value.bytes) &&
|
|
412
|
-
|
|
459
|
+
(value.body === undefined ||
|
|
460
|
+
(typeof value.body === "string" &&
|
|
461
|
+
value.body.length > 0 &&
|
|
462
|
+
!value.body.includes("\u0000") &&
|
|
463
|
+
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
413
464
|
(value.latencyMs === undefined || isNonNegativeInteger(value.latencyMs)) &&
|
|
414
465
|
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
|
|
415
466
|
(value.steer === undefined || value.steer === true));
|
|
@@ -627,6 +678,115 @@ function migratePreCompatibilityAttestations(value) {
|
|
|
627
678
|
}
|
|
628
679
|
return { ...value, compatibilityAttestations: [] };
|
|
629
680
|
}
|
|
681
|
+
const PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS = [
|
|
682
|
+
...PRE_COMPATIBILITY_ATTESTATION_STATE_KEYS,
|
|
683
|
+
"compatibilityAttestations",
|
|
684
|
+
];
|
|
685
|
+
/** Add an empty bounded private endpoint-refresh journal to exact legacy v1. */
|
|
686
|
+
function migratePreCodexEndpointRefreshJournal(value) {
|
|
687
|
+
if (!isObject(value) ||
|
|
688
|
+
value.schemaVersion !== 1 ||
|
|
689
|
+
!hasOnlyKeys(value, PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS)) {
|
|
690
|
+
return value;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
...value,
|
|
694
|
+
codexEndpointRefreshSequence: 0,
|
|
695
|
+
codexEndpointRefreshEvents: [],
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const PRE_CODEX_ORPHAN_REMOVAL_STATE_KEYS = [
|
|
699
|
+
...PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS,
|
|
700
|
+
"codexEndpointRefreshSequence",
|
|
701
|
+
"codexEndpointRefreshEvents",
|
|
702
|
+
];
|
|
703
|
+
/** Add an empty private orphan-removal journal to exact preceding v1 state. */
|
|
704
|
+
function migratePreCodexOrphanRemovalJournal(value) {
|
|
705
|
+
if (!isObject(value) ||
|
|
706
|
+
value.schemaVersion !== 1 ||
|
|
707
|
+
!hasOnlyKeys(value, PRE_CODEX_ORPHAN_REMOVAL_STATE_KEYS)) {
|
|
708
|
+
return value;
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
...value,
|
|
712
|
+
codexOrphanRemovalSequence: 0,
|
|
713
|
+
codexOrphanRemovalEvents: [],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* v1.2.0 persisted hop counters on queue, in-flight, and event rows. Hop
|
|
718
|
+
* policy no longer exists; accept only the old bounded shape and discard the
|
|
719
|
+
* obsolete field before strict current-schema validation and persistence.
|
|
720
|
+
*/
|
|
721
|
+
function migrateLegacyHopCounts(value) {
|
|
722
|
+
if (!isObject(value) ||
|
|
723
|
+
value.schemaVersion !== 1 ||
|
|
724
|
+
!Array.isArray(value.queue) ||
|
|
725
|
+
!Array.isArray(value.inFlight) ||
|
|
726
|
+
!Array.isArray(value.events)) {
|
|
727
|
+
return value;
|
|
728
|
+
}
|
|
729
|
+
const strip = (entry) => {
|
|
730
|
+
if (!isObject(entry) || !Object.hasOwn(entry, "hopCount"))
|
|
731
|
+
return entry;
|
|
732
|
+
if (!isNonNegativeInteger(entry.hopCount) || entry.hopCount > 64) {
|
|
733
|
+
return entry;
|
|
734
|
+
}
|
|
735
|
+
const { hopCount: _legacyHopCount, ...current } = entry;
|
|
736
|
+
return current;
|
|
737
|
+
};
|
|
738
|
+
return {
|
|
739
|
+
...value,
|
|
740
|
+
queue: value.queue.map(strip),
|
|
741
|
+
inFlight: value.inFlight.map(strip),
|
|
742
|
+
events: value.events.map(strip),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function isLegacyCompatibilityCertification(value) {
|
|
746
|
+
if (!isObject(value))
|
|
747
|
+
return false;
|
|
748
|
+
const required = ["depth", "outcome", "certifiedAt"];
|
|
749
|
+
if (!hasOnlyKeys(value, required, ["safeErrorCode"]))
|
|
750
|
+
return false;
|
|
751
|
+
if ((value.depth !== "wire" &&
|
|
752
|
+
value.depth !== "thread_ops" &&
|
|
753
|
+
value.depth !== "turn") ||
|
|
754
|
+
(value.outcome !== "pass" && value.outcome !== "fail") ||
|
|
755
|
+
!isIsoTimestamp(value.certifiedAt) ||
|
|
756
|
+
new Date(Date.parse(value.certifiedAt)).toISOString() !==
|
|
757
|
+
value.certifiedAt ||
|
|
758
|
+
(value.safeErrorCode !== undefined && !isSafeCode(value.safeErrorCode))) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return (value.outcome === "pass") === (value.safeErrorCode === undefined);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* v1.1-v1.2 compatibility rows could contain manual live-certification
|
|
765
|
+
* evidence. The ceremony is gone: accept only its exact old shape, discard
|
|
766
|
+
* it, and retain the automatic bounded probe evidence.
|
|
767
|
+
*/
|
|
768
|
+
function migrateLegacyCompatibilityCertifications(value) {
|
|
769
|
+
if (!isObject(value) ||
|
|
770
|
+
value.schemaVersion !== 1 ||
|
|
771
|
+
!Array.isArray(value.compatibilityAttestations)) {
|
|
772
|
+
return value;
|
|
773
|
+
}
|
|
774
|
+
let changed = false;
|
|
775
|
+
const compatibilityAttestations = value.compatibilityAttestations.map((entry) => {
|
|
776
|
+
if (!isObject(entry) || !Object.hasOwn(entry, "certification")) {
|
|
777
|
+
return entry;
|
|
778
|
+
}
|
|
779
|
+
if (!isLegacyCompatibilityCertification(entry.certification)) {
|
|
780
|
+
return entry;
|
|
781
|
+
}
|
|
782
|
+
const { certification: _legacyCertification, ...current } = entry;
|
|
783
|
+
if (!isCompatibilityAttestation(current))
|
|
784
|
+
return entry;
|
|
785
|
+
changed = true;
|
|
786
|
+
return current;
|
|
787
|
+
});
|
|
788
|
+
return changed ? { ...value, compatibilityAttestations } : value;
|
|
789
|
+
}
|
|
630
790
|
function isPersistedState(value) {
|
|
631
791
|
if (!isObject(value) ||
|
|
632
792
|
!hasOnlyKeys(value, [
|
|
@@ -647,6 +807,10 @@ function isPersistedState(value) {
|
|
|
647
807
|
"progressWatches",
|
|
648
808
|
"progressWatchEvents",
|
|
649
809
|
"compatibilityAttestations",
|
|
810
|
+
"codexEndpointRefreshSequence",
|
|
811
|
+
"codexEndpointRefreshEvents",
|
|
812
|
+
"codexOrphanRemovalSequence",
|
|
813
|
+
"codexOrphanRemovalEvents",
|
|
650
814
|
"codexSuccession",
|
|
651
815
|
]) ||
|
|
652
816
|
value.schemaVersion !== 1 ||
|
|
@@ -654,6 +818,8 @@ function isPersistedState(value) {
|
|
|
654
818
|
!isIsoTimestamp(value.updatedAt) ||
|
|
655
819
|
!isNonNegativeInteger(value.eventSequence) ||
|
|
656
820
|
!isNonNegativeInteger(value.watchSequence) ||
|
|
821
|
+
!isNonNegativeInteger(value.codexEndpointRefreshSequence) ||
|
|
822
|
+
!isNonNegativeInteger(value.codexOrphanRemovalSequence) ||
|
|
657
823
|
!Array.isArray(value.routes) ||
|
|
658
824
|
!value.routes.every(isRouteRecord) ||
|
|
659
825
|
!Array.isArray(value.pairs) ||
|
|
@@ -668,6 +834,14 @@ function isPersistedState(value) {
|
|
|
668
834
|
value.compatibilityAttestations.length >
|
|
669
835
|
COMPATIBILITY_ATTESTATION_CAPACITY ||
|
|
670
836
|
!value.compatibilityAttestations.every(isCompatibilityAttestation) ||
|
|
837
|
+
!Array.isArray(value.codexEndpointRefreshEvents) ||
|
|
838
|
+
value.codexEndpointRefreshEvents.length >
|
|
839
|
+
CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY ||
|
|
840
|
+
!value.codexEndpointRefreshEvents.every(isCodexEndpointRefreshJournalEvent) ||
|
|
841
|
+
!Array.isArray(value.codexOrphanRemovalEvents) ||
|
|
842
|
+
value.codexOrphanRemovalEvents.length >
|
|
843
|
+
CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY ||
|
|
844
|
+
!value.codexOrphanRemovalEvents.every(isCodexOrphanRemovalJournalEvent) ||
|
|
671
845
|
!Array.isArray(value.connectors) ||
|
|
672
846
|
!value.connectors.every(isConnectorRecord) ||
|
|
673
847
|
!Array.isArray(value.queue) ||
|
|
@@ -770,6 +944,23 @@ function isPersistedState(value) {
|
|
|
770
944
|
};
|
|
771
945
|
const sequencesStrictlyIncrease = candidate.events.every((event, index) => index === 0 ||
|
|
772
946
|
event.sequence > (candidate.events[index - 1]?.sequence ?? 0));
|
|
947
|
+
const endpointRefreshSequenceConsistent = candidate.codexEndpointRefreshEvents.length === 0
|
|
948
|
+
? candidate.codexEndpointRefreshSequence === 0
|
|
949
|
+
: candidate.codexEndpointRefreshEvents.at(-1)?.sequence ===
|
|
950
|
+
candidate.codexEndpointRefreshSequence &&
|
|
951
|
+
candidate.codexEndpointRefreshEvents.every((event, index) => index === 0 ||
|
|
952
|
+
event.sequence ===
|
|
953
|
+
(candidate.codexEndpointRefreshEvents[index - 1]?.sequence ??
|
|
954
|
+
0) +
|
|
955
|
+
1);
|
|
956
|
+
const orphanRemovalSequenceConsistent = candidate.codexOrphanRemovalEvents.length === 0
|
|
957
|
+
? candidate.codexOrphanRemovalSequence === 0
|
|
958
|
+
: candidate.codexOrphanRemovalEvents.at(-1)?.sequence ===
|
|
959
|
+
candidate.codexOrphanRemovalSequence &&
|
|
960
|
+
candidate.codexOrphanRemovalEvents.every((event, index) => index === 0 ||
|
|
961
|
+
event.sequence ===
|
|
962
|
+
(candidate.codexOrphanRemovalEvents[index - 1]?.sequence ?? 0) +
|
|
963
|
+
1);
|
|
773
964
|
return (aliases.size === candidate.routes.length &&
|
|
774
965
|
new Set(pairKeys).size === pairKeys.length &&
|
|
775
966
|
pairsValid &&
|
|
@@ -784,6 +975,8 @@ function isPersistedState(value) {
|
|
|
784
975
|
candidate.accounting.queuedBytes === expectedQueuedBytes &&
|
|
785
976
|
sequencesStrictlyIncrease &&
|
|
786
977
|
candidate.events.every((event) => event.sequence <= candidate.eventSequence) &&
|
|
978
|
+
endpointRefreshSequenceConsistent &&
|
|
979
|
+
orphanRemovalSequenceConsistent &&
|
|
787
980
|
candidate.routes.every((route) => route.alias.endsWith(`@${route.binding.hostId}`) &&
|
|
788
981
|
((route.binding.provider === "codex" &&
|
|
789
982
|
route.registrationMode === "explicit_opt_in") ||
|
|
@@ -1105,6 +1298,10 @@ export class GatewayStore {
|
|
|
1105
1298
|
progressWatches: [],
|
|
1106
1299
|
progressWatchEvents: [],
|
|
1107
1300
|
compatibilityAttestations: [],
|
|
1301
|
+
codexEndpointRefreshSequence: 0,
|
|
1302
|
+
codexEndpointRefreshEvents: [],
|
|
1303
|
+
codexOrphanRemovalSequence: 0,
|
|
1304
|
+
codexOrphanRemovalEvents: [],
|
|
1108
1305
|
codexSuccession: null,
|
|
1109
1306
|
};
|
|
1110
1307
|
if (this.state.pairs.length > this.config.limits.maxPairs) {
|
|
@@ -1183,7 +1380,7 @@ export class GatewayStore {
|
|
|
1183
1380
|
}
|
|
1184
1381
|
});
|
|
1185
1382
|
}
|
|
1186
|
-
async markConnectorOffline(identity, safeErrorCode = "CONNECTOR_OFFLINE", inFlightSettlements = []) {
|
|
1383
|
+
async markConnectorOffline(identity, safeErrorCode = "CONNECTOR_OFFLINE", inFlightSettlements = [], options = {}) {
|
|
1187
1384
|
return await this.mutate(async (state, now) => {
|
|
1188
1385
|
this.assertAllowedIdentity(identity);
|
|
1189
1386
|
if (!SAFE_CODE_PATTERN.test(safeErrorCode)) {
|
|
@@ -1211,7 +1408,7 @@ export class GatewayStore {
|
|
|
1211
1408
|
route.updatedAt = now.toISOString();
|
|
1212
1409
|
route.safeErrorCode = safeErrorCode;
|
|
1213
1410
|
}
|
|
1214
|
-
return this.terminateAffectedMessages(state, affectedAliases, now, safeErrorCode, settlementPlan);
|
|
1411
|
+
return this.terminateAffectedMessages(state, affectedAliases, now, safeErrorCode, settlementPlan, options.preserveQueued === true);
|
|
1215
1412
|
});
|
|
1216
1413
|
}
|
|
1217
1414
|
async registerRoute(input) {
|
|
@@ -1261,7 +1458,7 @@ export class GatewayStore {
|
|
|
1261
1458
|
registeredAt: now.toISOString(),
|
|
1262
1459
|
updatedAt: now.toISOString(),
|
|
1263
1460
|
lastSeenAt: now.toISOString(),
|
|
1264
|
-
queueDepth:
|
|
1461
|
+
queueDepth: state.queue.filter((item) => item.targetAlias === input.alias).length,
|
|
1265
1462
|
counters: emptyCounters(),
|
|
1266
1463
|
});
|
|
1267
1464
|
});
|
|
@@ -1372,7 +1569,7 @@ export class GatewayStore {
|
|
|
1372
1569
|
registeredAt: now.toISOString(),
|
|
1373
1570
|
updatedAt: now.toISOString(),
|
|
1374
1571
|
lastSeenAt: now.toISOString(),
|
|
1375
|
-
queueDepth:
|
|
1572
|
+
queueDepth: state.queue.filter((item) => item.targetAlias === replacement.alias).length,
|
|
1376
1573
|
counters: emptyCounters(),
|
|
1377
1574
|
});
|
|
1378
1575
|
}
|
|
@@ -1449,7 +1646,9 @@ export class GatewayStore {
|
|
|
1449
1646
|
if (!ALIAS_PATTERN.test(input.alias) ||
|
|
1450
1647
|
!ALIAS_PATTERN.test(newAlias) ||
|
|
1451
1648
|
!isPrivateRouteBinding(input.newBinding) ||
|
|
1452
|
-
!newAlias.endsWith(`@${input.newBinding.hostId}`)
|
|
1649
|
+
!newAlias.endsWith(`@${input.newBinding.hostId}`) ||
|
|
1650
|
+
(input.journalReason !== undefined &&
|
|
1651
|
+
input.journalReason !== "boot_reactivation")) {
|
|
1453
1652
|
throw new BridgeError("INVALID_ROUTE_REBIND", "The stale-route rebind request is malformed.");
|
|
1454
1653
|
}
|
|
1455
1654
|
this.assertAllowedIdentity(endpointOf(input.newBinding));
|
|
@@ -1457,10 +1656,8 @@ export class GatewayStore {
|
|
|
1457
1656
|
if (!route.enabled ||
|
|
1458
1657
|
route.state !== "stale" ||
|
|
1459
1658
|
route.compatibility !== "expired" ||
|
|
1460
|
-
route.queueDepth !== 0 ||
|
|
1461
|
-
state.queue.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias) ||
|
|
1462
1659
|
state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
|
|
1463
|
-
throw new BridgeError("ROUTE_REBIND_NOT_SAFE", "Only an enabled, expired
|
|
1660
|
+
throw new BridgeError("ROUTE_REBIND_NOT_SAFE", "Only an enabled, expired stale route without in-flight work can be rebound.");
|
|
1464
1661
|
}
|
|
1465
1662
|
if (route.binding.provider !== input.newBinding.provider ||
|
|
1466
1663
|
route.binding.hostId !== input.newBinding.hostId) {
|
|
@@ -1476,6 +1673,10 @@ export class GatewayStore {
|
|
|
1476
1673
|
(!claudeReasonAllowed || !sameLogicalRoute))) {
|
|
1477
1674
|
throw new BridgeError("ROUTE_RESELECTION_REQUIRED", "A stale route may rebind only the same provider-native logical identity and ownership lease.");
|
|
1478
1675
|
}
|
|
1676
|
+
if (input.journalReason === "boot_reactivation" &&
|
|
1677
|
+
route.binding.provider !== "codex") {
|
|
1678
|
+
throw new BridgeError("INVALID_ROUTE_REBIND", "Only an exact retained Codex route may record boot reactivation.");
|
|
1679
|
+
}
|
|
1479
1680
|
const connector = state.connectors.find((candidate) => sameEndpoint(candidate, input.newBinding));
|
|
1480
1681
|
if (!connector ||
|
|
1481
1682
|
!["healthy", "degraded"].includes(connector.health) ||
|
|
@@ -1488,7 +1689,12 @@ export class GatewayStore {
|
|
|
1488
1689
|
candidate.binding.ownerLease === input.newBinding.ownerLease))) {
|
|
1489
1690
|
throw new BridgeError("ROUTE_BINDING_COLLISION", "The replacement private binding already belongs to another alias.");
|
|
1490
1691
|
}
|
|
1692
|
+
if (input.journalReason === "boot_reactivation" &&
|
|
1693
|
+
state.codexEndpointRefreshSequence >= Number.MAX_SAFE_INTEGER) {
|
|
1694
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_SEQUENCE_EXHAUSTED", "The bounded Codex endpoint-refresh sequence is exhausted.");
|
|
1695
|
+
}
|
|
1491
1696
|
const previousAlias = route.alias;
|
|
1697
|
+
const oldEndpointGeneration = route.binding.endpointGeneration;
|
|
1492
1698
|
route.binding = { ...input.newBinding };
|
|
1493
1699
|
route.alias = newAlias;
|
|
1494
1700
|
route.state = input.state ?? "idle";
|
|
@@ -1497,9 +1703,8 @@ export class GatewayStore {
|
|
|
1497
1703
|
route.lastSeenAt = now.toISOString();
|
|
1498
1704
|
delete route.safeErrorCode;
|
|
1499
1705
|
if (previousAlias !== newAlias) {
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
// future caller uses the same primitive after another invalidation.
|
|
1706
|
+
// Retained queued mail follows only the exact re-observed logical
|
|
1707
|
+
// route. In-flight work remains forbidden above.
|
|
1503
1708
|
for (const item of [...state.queue, ...state.inFlight]) {
|
|
1504
1709
|
if (item.sourceAlias === previousAlias)
|
|
1505
1710
|
item.sourceAlias = newAlias;
|
|
@@ -1517,6 +1722,164 @@ export class GatewayStore {
|
|
|
1517
1722
|
renameRateBucket(state, previousAlias, newAlias);
|
|
1518
1723
|
renamePairAlias(state, previousAlias, newAlias, now);
|
|
1519
1724
|
}
|
|
1725
|
+
if (input.journalReason === "boot_reactivation") {
|
|
1726
|
+
this.appendCodexEndpointRefreshEvent(state, {
|
|
1727
|
+
timestamp: now.toISOString(),
|
|
1728
|
+
alias: route.alias,
|
|
1729
|
+
hostId: route.binding.hostId,
|
|
1730
|
+
threadId: route.binding.routeHandle,
|
|
1731
|
+
oldEndpointGeneration,
|
|
1732
|
+
newEndpointGeneration: route.binding.endpointGeneration,
|
|
1733
|
+
reason: "boot_reactivation",
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Atomically re-anchor only the exact Codex tasks proved present on a newly
|
|
1740
|
+
* compatible App Server generation. Omitted stale tasks stay untouched.
|
|
1741
|
+
*/
|
|
1742
|
+
async reanchorCodexRoutes(input) {
|
|
1743
|
+
return await this.mutate(async (state, now) => {
|
|
1744
|
+
if (!isObject(input) ||
|
|
1745
|
+
!hasOnlyKeys(input, ["oldEndpoint", "newEndpoint", "routes"]) ||
|
|
1746
|
+
!isPrivateEndpointIdentity(input.oldEndpoint) ||
|
|
1747
|
+
!isPrivateEndpointIdentity(input.newEndpoint) ||
|
|
1748
|
+
input.oldEndpoint.provider !== "codex" ||
|
|
1749
|
+
input.newEndpoint.provider !== "codex" ||
|
|
1750
|
+
input.oldEndpoint.hostId !== input.newEndpoint.hostId ||
|
|
1751
|
+
input.oldEndpoint.endpointGeneration ===
|
|
1752
|
+
input.newEndpoint.endpointGeneration ||
|
|
1753
|
+
!Array.isArray(input.routes) ||
|
|
1754
|
+
input.routes.length > this.config.limits.maxRoutes) {
|
|
1755
|
+
throw new BridgeError("INVALID_CODEX_ENDPOINT_REFRESH", "Codex endpoint refresh requires two distinct exact generations on one allowlisted host and a bounded route subset.");
|
|
1756
|
+
}
|
|
1757
|
+
this.assertAllowedIdentity(input.oldEndpoint);
|
|
1758
|
+
this.assertAllowedIdentity(input.newEndpoint);
|
|
1759
|
+
const newConnector = state.connectors.find((candidate) => sameEndpoint(candidate, input.newEndpoint));
|
|
1760
|
+
if (newConnector === undefined ||
|
|
1761
|
+
!["healthy", "degraded"].includes(newConnector.health) ||
|
|
1762
|
+
newConnector.compatibility !== "compatible") {
|
|
1763
|
+
throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The replacement Codex endpoint generation must be positively observed and compatible.");
|
|
1764
|
+
}
|
|
1765
|
+
const aliases = new Set();
|
|
1766
|
+
const threadIds = new Set();
|
|
1767
|
+
const ownerLeases = new Set();
|
|
1768
|
+
const replacements = [];
|
|
1769
|
+
for (const candidate of input.routes) {
|
|
1770
|
+
if (!isObject(candidate) ||
|
|
1771
|
+
!hasOnlyKeys(candidate, ["alias", "threadId", "ownerLease"], ["state"]) ||
|
|
1772
|
+
typeof candidate.alias !== "string" ||
|
|
1773
|
+
!ALIAS_PATTERN.test(candidate.alias) ||
|
|
1774
|
+
!candidate.alias.startsWith("codex-") ||
|
|
1775
|
+
!candidate.alias.endsWith(`@${input.oldEndpoint.hostId}`) ||
|
|
1776
|
+
!isPrivateToken(candidate.threadId) ||
|
|
1777
|
+
!isPrivateToken(candidate.ownerLease) ||
|
|
1778
|
+
(candidate.state !== undefined &&
|
|
1779
|
+
candidate.state !== "idle" &&
|
|
1780
|
+
candidate.state !== "busy" &&
|
|
1781
|
+
candidate.state !== "awaiting_approval")) {
|
|
1782
|
+
throw new BridgeError("INVALID_CODEX_ENDPOINT_REFRESH", "A Codex endpoint refresh route proof is malformed.");
|
|
1783
|
+
}
|
|
1784
|
+
if (aliases.has(candidate.alias) ||
|
|
1785
|
+
threadIds.has(candidate.threadId) ||
|
|
1786
|
+
ownerLeases.has(candidate.ownerLease)) {
|
|
1787
|
+
throw new BridgeError("AMBIGUOUS_CODEX_ENDPOINT_REFRESH", "A Codex endpoint refresh cannot contain duplicate alias, task, or lease claims.");
|
|
1788
|
+
}
|
|
1789
|
+
aliases.add(candidate.alias);
|
|
1790
|
+
threadIds.add(candidate.threadId);
|
|
1791
|
+
ownerLeases.add(candidate.ownerLease);
|
|
1792
|
+
const route = state.routes.find((stored) => stored.alias === candidate.alias);
|
|
1793
|
+
if (route === undefined ||
|
|
1794
|
+
route.binding.provider !== "codex" ||
|
|
1795
|
+
route.registrationMode !== "explicit_opt_in" ||
|
|
1796
|
+
!route.enabled ||
|
|
1797
|
+
route.state !== "stale" ||
|
|
1798
|
+
route.compatibility !== "expired" ||
|
|
1799
|
+
!sameEndpoint(route.binding, input.oldEndpoint) ||
|
|
1800
|
+
route.binding.routeHandle !== candidate.threadId ||
|
|
1801
|
+
route.binding.ownerLease !== candidate.ownerLease ||
|
|
1802
|
+
state.inFlight.some((item) => item.sourceAlias === route.alias ||
|
|
1803
|
+
item.targetAlias === route.alias)) {
|
|
1804
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_NOT_SAFE", "Only an exact enabled, expired stale Codex task without in-flight work may be refreshed.");
|
|
1805
|
+
}
|
|
1806
|
+
const newBinding = {
|
|
1807
|
+
...route.binding,
|
|
1808
|
+
endpointGeneration: input.newEndpoint.endpointGeneration,
|
|
1809
|
+
};
|
|
1810
|
+
if (state.routes.some((stored) => stored !== route &&
|
|
1811
|
+
(sameRouteTarget(stored.binding, newBinding) ||
|
|
1812
|
+
stored.binding.ownerLease === newBinding.ownerLease))) {
|
|
1813
|
+
throw new BridgeError("ROUTE_BINDING_COLLISION", "A refreshed Codex task or ownership lease is already claimed by another route.");
|
|
1814
|
+
}
|
|
1815
|
+
replacements.push({ route, state: candidate.state ?? "idle" });
|
|
1816
|
+
}
|
|
1817
|
+
if (state.codexEndpointRefreshSequence >
|
|
1818
|
+
Number.MAX_SAFE_INTEGER - replacements.length) {
|
|
1819
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_SEQUENCE_EXHAUSTED", "The bounded Codex endpoint-refresh sequence is exhausted.");
|
|
1820
|
+
}
|
|
1821
|
+
for (const replacement of replacements) {
|
|
1822
|
+
const oldEndpointGeneration = replacement.route.binding.endpointGeneration;
|
|
1823
|
+
replacement.route.binding = {
|
|
1824
|
+
...replacement.route.binding,
|
|
1825
|
+
endpointGeneration: input.newEndpoint.endpointGeneration,
|
|
1826
|
+
};
|
|
1827
|
+
replacement.route.state = replacement.state;
|
|
1828
|
+
replacement.route.compatibility = "compatible";
|
|
1829
|
+
replacement.route.updatedAt = now.toISOString();
|
|
1830
|
+
replacement.route.lastSeenAt = now.toISOString();
|
|
1831
|
+
delete replacement.route.safeErrorCode;
|
|
1832
|
+
this.appendCodexEndpointRefreshEvent(state, {
|
|
1833
|
+
timestamp: now.toISOString(),
|
|
1834
|
+
alias: replacement.route.alias,
|
|
1835
|
+
hostId: replacement.route.binding.hostId,
|
|
1836
|
+
threadId: replacement.route.binding.routeHandle,
|
|
1837
|
+
oldEndpointGeneration,
|
|
1838
|
+
newEndpointGeneration: input.newEndpoint.endpointGeneration,
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
return { reboundAliases: replacements.map(({ route }) => route.alias) };
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Dashboard recovery primitive for one abandoned Codex registration. The
|
|
1846
|
+
* alias is only authority to request evaluation; durable state must prove
|
|
1847
|
+
* the task is empty, stale, expired, and on a dead or superseded generation.
|
|
1848
|
+
*/
|
|
1849
|
+
async removeStaleCodexOrphan(input) {
|
|
1850
|
+
return await this.mutate(async (state, now) => {
|
|
1851
|
+
if (!isObject(input) ||
|
|
1852
|
+
!hasOnlyKeys(input, ["alias"]) ||
|
|
1853
|
+
typeof input.alias !== "string") {
|
|
1854
|
+
throw new BridgeError("INVALID_CODEX_ORPHAN_RECOVERY", "Codex orphan recovery requires one exact Codex alias.");
|
|
1855
|
+
}
|
|
1856
|
+
const route = this.requireStaleCodexOrphan(state, input.alias);
|
|
1857
|
+
const aliases = new Set([route.alias]);
|
|
1858
|
+
const removedPairs = state.pairs
|
|
1859
|
+
.filter((pair) => pair.claudeAlias === route.alias || pair.codexAlias === route.alias)
|
|
1860
|
+
.map((pair) => ({
|
|
1861
|
+
claudeAlias: pair.claudeAlias,
|
|
1862
|
+
codexAlias: pair.codexAlias,
|
|
1863
|
+
}));
|
|
1864
|
+
if (state.codexOrphanRemovalSequence >= Number.MAX_SAFE_INTEGER) {
|
|
1865
|
+
throw new BridgeError("CODEX_ORPHAN_REMOVAL_SEQUENCE_EXHAUSTED", "The bounded Codex orphan-removal sequence is exhausted.");
|
|
1866
|
+
}
|
|
1867
|
+
this.settleProgressWatchesForAliases(state, aliases, now);
|
|
1868
|
+
state.routes = state.routes.filter((candidate) => candidate !== route);
|
|
1869
|
+
removePairsForAliases(state, aliases);
|
|
1870
|
+
state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== route.alias);
|
|
1871
|
+
state.dedupe = state.dedupe.filter((record) => record.sourceAlias !== route.alias &&
|
|
1872
|
+
record.targetAlias !== route.alias);
|
|
1873
|
+
this.appendCodexOrphanRemovalEvent(state, {
|
|
1874
|
+
timestamp: now.toISOString(),
|
|
1875
|
+
alias: route.alias,
|
|
1876
|
+
hostId: route.binding.hostId,
|
|
1877
|
+
});
|
|
1878
|
+
return {
|
|
1879
|
+
alias: route.alias,
|
|
1880
|
+
binding: { ...route.binding },
|
|
1881
|
+
removedPairs,
|
|
1882
|
+
};
|
|
1520
1883
|
});
|
|
1521
1884
|
}
|
|
1522
1885
|
async disableRoute(alias, ownerLease, inFlightSettlements = []) {
|
|
@@ -1651,6 +2014,64 @@ export class GatewayStore {
|
|
|
1651
2014
|
};
|
|
1652
2015
|
});
|
|
1653
2016
|
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Read-only preflight for service-side provider cleanup. Removal repeats the
|
|
2019
|
+
* identical proof atomically so no intervening state change can authorize it.
|
|
2020
|
+
*/
|
|
2021
|
+
async inspectStaleCodexOrphan(alias) {
|
|
2022
|
+
return this.mutex.run("gateway", async () => {
|
|
2023
|
+
const route = this.requireStaleCodexOrphan(this.requireState(), alias);
|
|
2024
|
+
return { ...route.binding };
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Capture the exact pre-mutation authority needed to reconcile a possible
|
|
2029
|
+
* post-rename orphan-removal commit. This value remains controller-private.
|
|
2030
|
+
*/
|
|
2031
|
+
async inspectStaleCodexOrphanRemovalAuthority(alias) {
|
|
2032
|
+
return this.mutex.run("gateway", async () => {
|
|
2033
|
+
const state = this.requireState();
|
|
2034
|
+
const route = this.requireStaleCodexOrphan(state, alias);
|
|
2035
|
+
return {
|
|
2036
|
+
binding: { ...route.binding },
|
|
2037
|
+
previousSequence: state.codexOrphanRemovalSequence,
|
|
2038
|
+
};
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Fail-closed reconciliation for one exact removal whose durable rename may
|
|
2043
|
+
* have committed before a directory-sync error. The latest private journal
|
|
2044
|
+
* row, monotonic predecessor, alias absence, and native authority absence
|
|
2045
|
+
* must all corroborate the same mutation.
|
|
2046
|
+
*/
|
|
2047
|
+
async wasStaleCodexOrphanRemovalCommitted(input) {
|
|
2048
|
+
return this.mutex.run("gateway", async () => {
|
|
2049
|
+
if (!isObject(input) ||
|
|
2050
|
+
!hasOnlyKeys(input, ["alias", "binding", "previousSequence"]) ||
|
|
2051
|
+
typeof input.alias !== "string" ||
|
|
2052
|
+
!ALIAS_PATTERN.test(input.alias) ||
|
|
2053
|
+
!input.alias.startsWith("codex-") ||
|
|
2054
|
+
!isPrivateRouteBinding(input.binding) ||
|
|
2055
|
+
input.binding.provider !== "codex" ||
|
|
2056
|
+
!input.alias.endsWith(`@${input.binding.hostId}`) ||
|
|
2057
|
+
!isNonNegativeInteger(input.previousSequence) ||
|
|
2058
|
+
input.previousSequence >= Number.MAX_SAFE_INTEGER) {
|
|
2059
|
+
throw new BridgeError("INVALID_CODEX_ORPHAN_COMMIT_PROOF", "Codex orphan-removal reconciliation requires one exact private pre-mutation authority.");
|
|
2060
|
+
}
|
|
2061
|
+
this.assertAllowedIdentity(endpointOf(input.binding));
|
|
2062
|
+
const state = this.requireState();
|
|
2063
|
+
const expectedSequence = input.previousSequence + 1;
|
|
2064
|
+
const latest = state.codexOrphanRemovalEvents.at(-1);
|
|
2065
|
+
const routeOrAuthorityPresent = state.routes.some((route) => route.alias === input.alias ||
|
|
2066
|
+
sameRouteTarget(route.binding, input.binding) ||
|
|
2067
|
+
route.binding.ownerLease === input.binding.ownerLease);
|
|
2068
|
+
return (!routeOrAuthorityPresent &&
|
|
2069
|
+
state.codexOrphanRemovalSequence === expectedSequence &&
|
|
2070
|
+
latest?.sequence === expectedSequence &&
|
|
2071
|
+
latest.alias === input.alias &&
|
|
2072
|
+
latest.hostId === input.binding.hostId);
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
1654
2075
|
/**
|
|
1655
2076
|
* Controller-internal inventory used to preserve exact Codex registration
|
|
1656
2077
|
* identities across restart. Native task handles never cross the control
|
|
@@ -2336,7 +2757,9 @@ export class GatewayStore {
|
|
|
2336
2757
|
targetAlias: input.target.alias,
|
|
2337
2758
|
direction: "codex_to_claude",
|
|
2338
2759
|
sourceRoute: source,
|
|
2339
|
-
...(input.pair === true
|
|
2760
|
+
...(input.pair === true
|
|
2761
|
+
? { pair: true }
|
|
2762
|
+
: { transientTarget: true }),
|
|
2340
2763
|
});
|
|
2341
2764
|
});
|
|
2342
2765
|
}
|
|
@@ -2363,7 +2786,7 @@ export class GatewayStore {
|
|
|
2363
2786
|
const target = state.routes.find((route) => route.alias === metadata.targetAlias);
|
|
2364
2787
|
if (target)
|
|
2365
2788
|
target.queueDepth = Math.max(0, target.queueDepth - 1);
|
|
2366
|
-
const body = this.transientBodies.get(metadata.messageId);
|
|
2789
|
+
const body = this.transientBodies.get(metadata.messageId) ?? metadata.body;
|
|
2367
2790
|
this.transientBodies.delete(metadata.messageId);
|
|
2368
2791
|
if (body === undefined) {
|
|
2369
2792
|
this.finishMetadata(state, metadata, "abandoned", now, "TRANSIENT_BODY_UNAVAILABLE");
|
|
@@ -2381,13 +2804,17 @@ export class GatewayStore {
|
|
|
2381
2804
|
targetAlias: metadata.targetAlias,
|
|
2382
2805
|
state: "dispatching",
|
|
2383
2806
|
bytes: metadata.bytes,
|
|
2384
|
-
|
|
2807
|
+
body,
|
|
2385
2808
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2386
2809
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
2387
2810
|
});
|
|
2388
2811
|
return { ...metadata, body };
|
|
2389
2812
|
});
|
|
2390
2813
|
}
|
|
2814
|
+
/** Controller-private shutdown inventory; full IDs never cross control output. */
|
|
2815
|
+
async inspectQueuedMessageIds() {
|
|
2816
|
+
return this.mutex.run("gateway", async () => this.requireState().queue.map((item) => item.messageId));
|
|
2817
|
+
}
|
|
2391
2818
|
/**
|
|
2392
2819
|
* Atomically terminalizes every message whose delivery deadline is due.
|
|
2393
2820
|
* Returned settlements are emitted only by the mutation that removed the
|
|
@@ -2462,9 +2889,13 @@ export class GatewayStore {
|
|
|
2462
2889
|
};
|
|
2463
2890
|
});
|
|
2464
2891
|
}
|
|
2465
|
-
async requeueInFlightMessage(messageId, body) {
|
|
2892
|
+
async requeueInFlightMessage(messageId, body, safeErrorCode) {
|
|
2466
2893
|
return this.mutate(async (state, now) => {
|
|
2467
|
-
if (!MESSAGE_ID_PATTERN.test(messageId) ||
|
|
2894
|
+
if (!MESSAGE_ID_PATTERN.test(messageId) ||
|
|
2895
|
+
typeof body !== "string" ||
|
|
2896
|
+
body.length === 0 ||
|
|
2897
|
+
body.includes("\u0000") ||
|
|
2898
|
+
(safeErrorCode !== undefined && !SAFE_CODE_PATTERN.test(safeErrorCode))) {
|
|
2468
2899
|
throw new BridgeError("INVALID_GATEWAY_MESSAGE", "Only an exact in-flight message can be returned to the queue.");
|
|
2469
2900
|
}
|
|
2470
2901
|
const index = state.inFlight.findIndex((item) => item.messageId === messageId);
|
|
@@ -2472,6 +2903,9 @@ export class GatewayStore {
|
|
|
2472
2903
|
if (metadata === undefined || index < 0) {
|
|
2473
2904
|
return { status: "not_in_flight" };
|
|
2474
2905
|
}
|
|
2906
|
+
if (Buffer.byteLength(body, "utf8") !== metadata.bytes) {
|
|
2907
|
+
throw new BridgeError("INVALID_GATEWAY_MESSAGE", "A requeued message body must exactly match its admitted byte count.");
|
|
2908
|
+
}
|
|
2475
2909
|
state.inFlight.splice(index, 1);
|
|
2476
2910
|
if (Date.parse(metadata.deadlineAt) <= now.getTime()) {
|
|
2477
2911
|
return {
|
|
@@ -2480,6 +2914,7 @@ export class GatewayStore {
|
|
|
2480
2914
|
};
|
|
2481
2915
|
}
|
|
2482
2916
|
const { dispatchedAt: _dispatchedAt, ...queuedMetadata } = metadata;
|
|
2917
|
+
queuedMetadata.body = body;
|
|
2483
2918
|
state.queue.unshift(queuedMetadata);
|
|
2484
2919
|
this.transientBodies.set(messageId, body);
|
|
2485
2920
|
state.accounting.queuedBytes += metadata.bytes;
|
|
@@ -2497,8 +2932,9 @@ export class GatewayStore {
|
|
|
2497
2932
|
targetAlias: metadata.targetAlias,
|
|
2498
2933
|
state: "held",
|
|
2499
2934
|
bytes: metadata.bytes,
|
|
2500
|
-
|
|
2935
|
+
body,
|
|
2501
2936
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2937
|
+
...(safeErrorCode === undefined ? {} : { safeErrorCode }),
|
|
2502
2938
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
2503
2939
|
});
|
|
2504
2940
|
return { status: "requeued" };
|
|
@@ -2527,7 +2963,7 @@ export class GatewayStore {
|
|
|
2527
2963
|
targetAlias: metadata.targetAlias,
|
|
2528
2964
|
state: progress,
|
|
2529
2965
|
bytes: metadata.bytes,
|
|
2530
|
-
|
|
2966
|
+
...(metadata.body === undefined ? {} : { body: metadata.body }),
|
|
2531
2967
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2532
2968
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
2533
2969
|
});
|
|
@@ -2947,6 +3383,42 @@ export class GatewayStore {
|
|
|
2947
3383
|
}
|
|
2948
3384
|
return route;
|
|
2949
3385
|
}
|
|
3386
|
+
requireStaleCodexOrphan(state, alias) {
|
|
3387
|
+
if (typeof alias !== "string" ||
|
|
3388
|
+
!ALIAS_PATTERN.test(alias) ||
|
|
3389
|
+
!alias.startsWith("codex-")) {
|
|
3390
|
+
throw new BridgeError("INVALID_CODEX_ORPHAN_RECOVERY", "Codex orphan recovery requires one exact Codex alias.");
|
|
3391
|
+
}
|
|
3392
|
+
const route = state.routes.find((candidate) => candidate.alias === alias);
|
|
3393
|
+
if (route === undefined) {
|
|
3394
|
+
throw new BridgeError("CODEX_ORPHAN_NOT_FOUND", "No Codex registration matches the requested alias.");
|
|
3395
|
+
}
|
|
3396
|
+
if (route.binding.provider !== "codex" ||
|
|
3397
|
+
route.registrationMode !== "explicit_opt_in" ||
|
|
3398
|
+
!route.enabled ||
|
|
3399
|
+
route.state !== "stale" ||
|
|
3400
|
+
route.compatibility !== "expired" ||
|
|
3401
|
+
route.queueDepth !== 0 ||
|
|
3402
|
+
state.queue.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias) ||
|
|
3403
|
+
state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
|
|
3404
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_NOT_SAFE", "Only an enabled, expired, empty stale Codex registration can be removed as an orphan.");
|
|
3405
|
+
}
|
|
3406
|
+
const connector = state.connectors.find((candidate) => candidate.provider === "codex" &&
|
|
3407
|
+
candidate.hostId === route.binding.hostId);
|
|
3408
|
+
const generationProvedDead = connector !== undefined &&
|
|
3409
|
+
(connector.endpointGeneration !== route.binding.endpointGeneration ||
|
|
3410
|
+
connector.health === "offline");
|
|
3411
|
+
if (!generationProvedDead) {
|
|
3412
|
+
throw new BridgeError("CODEX_ORPHAN_GENERATION_LIVE", "The Codex registration cannot be removed while its endpoint generation may still be live.");
|
|
3413
|
+
}
|
|
3414
|
+
const succession = this.journal(state);
|
|
3415
|
+
if (succession !== null &&
|
|
3416
|
+
(routeMatchesSuccessionIdentity(route, succession.old) ||
|
|
3417
|
+
routeMatchesSuccessionIdentity(route, succession.new))) {
|
|
3418
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BLOCKED", "A Codex registration owned by an active succession cannot be removed as an orphan.");
|
|
3419
|
+
}
|
|
3420
|
+
return route;
|
|
3421
|
+
}
|
|
2950
3422
|
requireAvailableRoute(state, alias) {
|
|
2951
3423
|
if (!ALIAS_PATTERN.test(alias)) {
|
|
2952
3424
|
throw new BridgeError("INVALID_GATEWAY_ALIAS", "The selected alias does not use the required lowercase ASCII grammar.");
|
|
@@ -3038,12 +3510,6 @@ export class GatewayStore {
|
|
|
3038
3510
|
this.recordRejection(state, sides, bytes, now, "MESSAGE_TOO_LARGE", input.steer, input.conversationIdSuffix);
|
|
3039
3511
|
throw new BridgeError("MESSAGE_TOO_LARGE", "The transient message exceeds the configured byte limit.");
|
|
3040
3512
|
}
|
|
3041
|
-
const hopCount = input.hopCount ?? 0;
|
|
3042
|
-
if (!isNonNegativeInteger(hopCount) ||
|
|
3043
|
-
hopCount > this.config.limits.maxHopCount) {
|
|
3044
|
-
this.recordRejection(state, sides, bytes, now, "HOP_LIMIT_EXCEEDED", input.steer, input.conversationIdSuffix);
|
|
3045
|
-
throw new BridgeError("HOP_LIMIT_EXCEEDED", "The message exceeds the configured gateway hop limit.");
|
|
3046
|
-
}
|
|
3047
3513
|
if (typeof input.dedupeKey !== "string" ||
|
|
3048
3514
|
input.dedupeKey.length === 0 ||
|
|
3049
3515
|
Buffer.byteLength(input.dedupeKey) > 512) {
|
|
@@ -3071,7 +3537,6 @@ export class GatewayStore {
|
|
|
3071
3537
|
? {}
|
|
3072
3538
|
: { conversationIdSuffix: duplicate.conversationIdSuffix }),
|
|
3073
3539
|
bytes,
|
|
3074
|
-
hopCount,
|
|
3075
3540
|
...(input.steer === true ? { steer: true } : {}),
|
|
3076
3541
|
});
|
|
3077
3542
|
return {
|
|
@@ -3160,8 +3625,11 @@ export class GatewayStore {
|
|
|
3160
3625
|
enqueuedAt: now.toISOString(),
|
|
3161
3626
|
deadlineAt: deadline.toISOString(),
|
|
3162
3627
|
bytes,
|
|
3163
|
-
|
|
3628
|
+
body: input.body,
|
|
3164
3629
|
...(sides.pair === true ? { pair: true } : {}),
|
|
3630
|
+
...(sides.transientTarget === true
|
|
3631
|
+
? { transientTarget: true }
|
|
3632
|
+
: {}),
|
|
3165
3633
|
...(input.steer === true ? { steer: true } : {}),
|
|
3166
3634
|
};
|
|
3167
3635
|
state.queue.push(metadata);
|
|
@@ -3208,7 +3676,7 @@ export class GatewayStore {
|
|
|
3208
3676
|
targetAlias: sides.targetAlias,
|
|
3209
3677
|
state: "queued",
|
|
3210
3678
|
bytes,
|
|
3211
|
-
|
|
3679
|
+
body: input.body,
|
|
3212
3680
|
...(input.steer === true ? { steer: true } : {}),
|
|
3213
3681
|
});
|
|
3214
3682
|
return {
|
|
@@ -3274,17 +3742,78 @@ export class GatewayStore {
|
|
|
3274
3742
|
targetAlias: sides.targetAlias,
|
|
3275
3743
|
state: "rejected",
|
|
3276
3744
|
bytes: Math.max(1, bytes),
|
|
3277
|
-
hopCount: 0,
|
|
3278
3745
|
safeErrorCode,
|
|
3279
3746
|
...(steer === true ? { steer: true } : {}),
|
|
3280
3747
|
});
|
|
3281
3748
|
}
|
|
3282
3749
|
appendEvent(state, event) {
|
|
3750
|
+
if (event.body !== undefined) {
|
|
3751
|
+
for (const retained of state.events) {
|
|
3752
|
+
if (retained.body !== undefined &&
|
|
3753
|
+
retained.messageIdSuffix === event.messageIdSuffix &&
|
|
3754
|
+
retained.direction === event.direction &&
|
|
3755
|
+
retained.sourceAlias === event.sourceAlias &&
|
|
3756
|
+
retained.targetAlias === event.targetAlias &&
|
|
3757
|
+
retained.conversationIdSuffix === event.conversationIdSuffix) {
|
|
3758
|
+
delete retained.body;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3283
3762
|
state.eventSequence += 1;
|
|
3284
3763
|
state.events.push({ sequence: state.eventSequence, ...event });
|
|
3285
3764
|
while (state.events.length > this.config.limits.eventCapacity) {
|
|
3286
3765
|
state.events.shift();
|
|
3287
3766
|
}
|
|
3767
|
+
this.pruneRetainedEventBodies(state);
|
|
3768
|
+
}
|
|
3769
|
+
pruneRetainedEventBodies(state) {
|
|
3770
|
+
const configured = this.config.limits.maxRetainedBodyBytes ??
|
|
3771
|
+
DEFAULT_RETAINED_BODY_BYTES;
|
|
3772
|
+
if (!Number.isSafeInteger(configured) ||
|
|
3773
|
+
configured < 1 ||
|
|
3774
|
+
configured > GATEWAY_MAX_STATE_FILE_BYTES) {
|
|
3775
|
+
throw new BridgeError("INVALID_GATEWAY_CONFIGURATION", "The retained message-body byte limit is invalid.");
|
|
3776
|
+
}
|
|
3777
|
+
let retainedBytes = state.events.reduce((total, event) => total +
|
|
3778
|
+
(event.body === undefined
|
|
3779
|
+
? 0
|
|
3780
|
+
: Buffer.byteLength(event.body, "utf8")), 0);
|
|
3781
|
+
for (const event of state.events) {
|
|
3782
|
+
if (retainedBytes <= configured)
|
|
3783
|
+
break;
|
|
3784
|
+
if (event.body === undefined)
|
|
3785
|
+
continue;
|
|
3786
|
+
retainedBytes -= Buffer.byteLength(event.body, "utf8");
|
|
3787
|
+
delete event.body;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
appendCodexEndpointRefreshEvent(state, event) {
|
|
3791
|
+
if (state.codexEndpointRefreshSequence >= Number.MAX_SAFE_INTEGER) {
|
|
3792
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_SEQUENCE_EXHAUSTED", "The bounded Codex endpoint-refresh sequence is exhausted.");
|
|
3793
|
+
}
|
|
3794
|
+
state.codexEndpointRefreshSequence += 1;
|
|
3795
|
+
state.codexEndpointRefreshEvents.push({
|
|
3796
|
+
sequence: state.codexEndpointRefreshSequence,
|
|
3797
|
+
...event,
|
|
3798
|
+
});
|
|
3799
|
+
while (state.codexEndpointRefreshEvents.length >
|
|
3800
|
+
CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY) {
|
|
3801
|
+
state.codexEndpointRefreshEvents.shift();
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
appendCodexOrphanRemovalEvent(state, event) {
|
|
3805
|
+
if (state.codexOrphanRemovalSequence >= Number.MAX_SAFE_INTEGER) {
|
|
3806
|
+
throw new BridgeError("CODEX_ORPHAN_REMOVAL_SEQUENCE_EXHAUSTED", "The bounded Codex orphan-removal sequence is exhausted.");
|
|
3807
|
+
}
|
|
3808
|
+
state.codexOrphanRemovalSequence += 1;
|
|
3809
|
+
state.codexOrphanRemovalEvents.push({
|
|
3810
|
+
sequence: state.codexOrphanRemovalSequence,
|
|
3811
|
+
...event,
|
|
3812
|
+
});
|
|
3813
|
+
while (state.codexOrphanRemovalEvents.length >
|
|
3814
|
+
CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY) {
|
|
3815
|
+
state.codexOrphanRemovalEvents.shift();
|
|
3816
|
+
}
|
|
3288
3817
|
}
|
|
3289
3818
|
/** Commit watch activity only after every message-admission check succeeds. */
|
|
3290
3819
|
applyProgressWatchMessageActivity(state, now, activity, sides) {
|
|
@@ -3493,7 +4022,7 @@ export class GatewayStore {
|
|
|
3493
4022
|
targetAlias: metadata.targetAlias,
|
|
3494
4023
|
state: deliveryState,
|
|
3495
4024
|
bytes: metadata.bytes,
|
|
3496
|
-
|
|
4025
|
+
...(metadata.body === undefined ? {} : { body: metadata.body }),
|
|
3497
4026
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
3498
4027
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
3499
4028
|
...(safeErrorCode ? { safeErrorCode } : {}),
|
|
@@ -3564,10 +4093,14 @@ export class GatewayStore {
|
|
|
3564
4093
|
}
|
|
3565
4094
|
return byMessageId;
|
|
3566
4095
|
}
|
|
3567
|
-
terminateAffectedMessages(state, aliases, now, safeErrorCode, inFlightSettlements) {
|
|
4096
|
+
terminateAffectedMessages(state, aliases, now, safeErrorCode, inFlightSettlements, preserveQueued = false) {
|
|
3568
4097
|
const settlements = [];
|
|
3569
|
-
const queued =
|
|
3570
|
-
|
|
4098
|
+
const queued = preserveQueued
|
|
4099
|
+
? []
|
|
4100
|
+
: state.queue.filter((item) => routeTerminationMatches(state, aliases, item));
|
|
4101
|
+
if (!preserveQueued) {
|
|
4102
|
+
state.queue = state.queue.filter((item) => !routeTerminationMatches(state, aliases, item));
|
|
4103
|
+
}
|
|
3571
4104
|
for (const item of queued) {
|
|
3572
4105
|
this.transientBodies.delete(item.messageId);
|
|
3573
4106
|
state.accounting.queuedBytes -= item.bytes;
|
|
@@ -3626,6 +4159,7 @@ export class GatewayStore {
|
|
|
3626
4159
|
.slice(-this.config.limits.dedupeCapacity);
|
|
3627
4160
|
state.rateBuckets = state.rateBuckets.filter((bucket) => now.getTime() - Date.parse(bucket.windowStartedAt) <
|
|
3628
4161
|
this.config.limits.rateWindowMs);
|
|
4162
|
+
this.pruneRetainedEventBodies(state);
|
|
3629
4163
|
}
|
|
3630
4164
|
recoverAfterRestart(now) {
|
|
3631
4165
|
const state = this.requireState();
|
|
@@ -3671,17 +4205,39 @@ export class GatewayStore {
|
|
|
3671
4205
|
route.safeErrorCode = "REOBSERVATION_REQUIRED";
|
|
3672
4206
|
route.queueDepth = 0;
|
|
3673
4207
|
}
|
|
4208
|
+
const retainedQueue = [];
|
|
4209
|
+
let retainedQueuedBytes = 0;
|
|
4210
|
+
this.transientBodies.clear();
|
|
3674
4211
|
for (const item of state.queue) {
|
|
3675
|
-
|
|
3676
|
-
|
|
4212
|
+
if (Date.parse(item.deadlineAt) <= now.getTime()) {
|
|
4213
|
+
this.finishMetadata(state, item, "expired", now, "MESSAGE_EXPIRED");
|
|
4214
|
+
continue;
|
|
4215
|
+
}
|
|
4216
|
+
if (item.body === undefined) {
|
|
4217
|
+
this.finishMetadata(state, item, "abandoned", now, "CONTROLLER_RESTARTED");
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
// A correlated native reply admitted through a transient Claude
|
|
4221
|
+
// capability cannot regain its exact UUID/generation after restart,
|
|
4222
|
+
// even when another session later claims the same public alias.
|
|
4223
|
+
if (item.transientTarget === true ||
|
|
4224
|
+
!state.routes.some((route) => route.alias === item.targetAlias)) {
|
|
4225
|
+
this.finishMetadata(state, item, "abandoned", now, "CONTROLLER_RESTARTED");
|
|
4226
|
+
continue;
|
|
4227
|
+
}
|
|
4228
|
+
retainedQueue.push(item);
|
|
4229
|
+
retainedQueuedBytes += item.bytes;
|
|
4230
|
+
this.transientBodies.set(item.messageId, item.body);
|
|
4231
|
+
const target = state.routes.find((route) => route.alias === item.targetAlias);
|
|
4232
|
+
if (target !== undefined)
|
|
4233
|
+
target.queueDepth += 1;
|
|
3677
4234
|
}
|
|
3678
4235
|
for (const item of state.inFlight) {
|
|
3679
4236
|
this.finishMetadata(state, item, "ambiguous", now, "CONTROLLER_RESTARTED");
|
|
3680
4237
|
}
|
|
3681
|
-
state.queue =
|
|
4238
|
+
state.queue = retainedQueue;
|
|
3682
4239
|
state.inFlight = [];
|
|
3683
|
-
state.accounting.queuedBytes =
|
|
3684
|
-
this.transientBodies.clear();
|
|
4240
|
+
state.accounting.queuedBytes = retainedQueuedBytes;
|
|
3685
4241
|
}
|
|
3686
4242
|
recoverCodexSuccessionAfterRestart(state, now) {
|
|
3687
4243
|
const journal = this.journal(state);
|
|
@@ -3942,6 +4498,10 @@ export class GatewayStore {
|
|
|
3942
4498
|
parsed = migratePrePairGraph(parsed);
|
|
3943
4499
|
parsed = migratePreProgressWatches(parsed);
|
|
3944
4500
|
parsed = migratePreCompatibilityAttestations(parsed);
|
|
4501
|
+
parsed = migratePreCodexEndpointRefreshJournal(parsed);
|
|
4502
|
+
parsed = migratePreCodexOrphanRemovalJournal(parsed);
|
|
4503
|
+
parsed = migrateLegacyHopCounts(parsed);
|
|
4504
|
+
parsed = migrateLegacyCompatibilityCertifications(parsed);
|
|
3945
4505
|
if (!isPersistedState(parsed)) {
|
|
3946
4506
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
|
|
3947
4507
|
}
|