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
|
@@ -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, [
|
|
@@ -349,7 +393,7 @@ function isQueuedMetadata(value) {
|
|
|
349
393
|
"deadlineAt",
|
|
350
394
|
"bytes",
|
|
351
395
|
"hopCount",
|
|
352
|
-
], ["conversationIdSuffix", "pair", "steer"])) {
|
|
396
|
+
], ["conversationIdSuffix", "body", "pair", "transientTarget", "steer"])) {
|
|
353
397
|
return false;
|
|
354
398
|
}
|
|
355
399
|
return (typeof value.messageId === "string" &&
|
|
@@ -368,8 +412,14 @@ function isQueuedMetadata(value) {
|
|
|
368
412
|
isIsoTimestamp(value.enqueuedAt) &&
|
|
369
413
|
isIsoTimestamp(value.deadlineAt) &&
|
|
370
414
|
isPositiveInteger(value.bytes) &&
|
|
415
|
+
(value.body === undefined ||
|
|
416
|
+
(typeof value.body === "string" &&
|
|
417
|
+
value.body.length > 0 &&
|
|
418
|
+
!value.body.includes("\u0000") &&
|
|
419
|
+
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
371
420
|
isNonNegativeInteger(value.hopCount) &&
|
|
372
421
|
(value.pair === undefined || value.pair === true) &&
|
|
422
|
+
(value.transientTarget === undefined || value.transientTarget === true) &&
|
|
373
423
|
(value.steer === undefined || value.steer === true));
|
|
374
424
|
}
|
|
375
425
|
function isInFlightMetadata(value) {
|
|
@@ -390,7 +440,7 @@ function isEvent(value) {
|
|
|
390
440
|
"state",
|
|
391
441
|
"bytes",
|
|
392
442
|
"hopCount",
|
|
393
|
-
], ["conversationIdSuffix", "latencyMs", "safeErrorCode", "steer"])) {
|
|
443
|
+
], ["conversationIdSuffix", "body", "latencyMs", "safeErrorCode", "steer"])) {
|
|
394
444
|
return false;
|
|
395
445
|
}
|
|
396
446
|
return (isPositiveInteger(value.sequence) &&
|
|
@@ -409,6 +459,11 @@ function isEvent(value) {
|
|
|
409
459
|
typeof value.state === "string" &&
|
|
410
460
|
DELIVERY_STATES.has(value.state) &&
|
|
411
461
|
isPositiveInteger(value.bytes) &&
|
|
462
|
+
(value.body === undefined ||
|
|
463
|
+
(typeof value.body === "string" &&
|
|
464
|
+
value.body.length > 0 &&
|
|
465
|
+
!value.body.includes("\u0000") &&
|
|
466
|
+
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
412
467
|
isNonNegativeInteger(value.hopCount) &&
|
|
413
468
|
(value.latencyMs === undefined || isNonNegativeInteger(value.latencyMs)) &&
|
|
414
469
|
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
|
|
@@ -627,6 +682,41 @@ function migratePreCompatibilityAttestations(value) {
|
|
|
627
682
|
}
|
|
628
683
|
return { ...value, compatibilityAttestations: [] };
|
|
629
684
|
}
|
|
685
|
+
const PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS = [
|
|
686
|
+
...PRE_COMPATIBILITY_ATTESTATION_STATE_KEYS,
|
|
687
|
+
"compatibilityAttestations",
|
|
688
|
+
];
|
|
689
|
+
/** Add an empty bounded private endpoint-refresh journal to exact legacy v1. */
|
|
690
|
+
function migratePreCodexEndpointRefreshJournal(value) {
|
|
691
|
+
if (!isObject(value) ||
|
|
692
|
+
value.schemaVersion !== 1 ||
|
|
693
|
+
!hasOnlyKeys(value, PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS)) {
|
|
694
|
+
return value;
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
...value,
|
|
698
|
+
codexEndpointRefreshSequence: 0,
|
|
699
|
+
codexEndpointRefreshEvents: [],
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
const PRE_CODEX_ORPHAN_REMOVAL_STATE_KEYS = [
|
|
703
|
+
...PRE_CODEX_ENDPOINT_REFRESH_STATE_KEYS,
|
|
704
|
+
"codexEndpointRefreshSequence",
|
|
705
|
+
"codexEndpointRefreshEvents",
|
|
706
|
+
];
|
|
707
|
+
/** Add an empty private orphan-removal journal to exact preceding v1 state. */
|
|
708
|
+
function migratePreCodexOrphanRemovalJournal(value) {
|
|
709
|
+
if (!isObject(value) ||
|
|
710
|
+
value.schemaVersion !== 1 ||
|
|
711
|
+
!hasOnlyKeys(value, PRE_CODEX_ORPHAN_REMOVAL_STATE_KEYS)) {
|
|
712
|
+
return value;
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
...value,
|
|
716
|
+
codexOrphanRemovalSequence: 0,
|
|
717
|
+
codexOrphanRemovalEvents: [],
|
|
718
|
+
};
|
|
719
|
+
}
|
|
630
720
|
function isPersistedState(value) {
|
|
631
721
|
if (!isObject(value) ||
|
|
632
722
|
!hasOnlyKeys(value, [
|
|
@@ -647,6 +737,10 @@ function isPersistedState(value) {
|
|
|
647
737
|
"progressWatches",
|
|
648
738
|
"progressWatchEvents",
|
|
649
739
|
"compatibilityAttestations",
|
|
740
|
+
"codexEndpointRefreshSequence",
|
|
741
|
+
"codexEndpointRefreshEvents",
|
|
742
|
+
"codexOrphanRemovalSequence",
|
|
743
|
+
"codexOrphanRemovalEvents",
|
|
650
744
|
"codexSuccession",
|
|
651
745
|
]) ||
|
|
652
746
|
value.schemaVersion !== 1 ||
|
|
@@ -654,6 +748,8 @@ function isPersistedState(value) {
|
|
|
654
748
|
!isIsoTimestamp(value.updatedAt) ||
|
|
655
749
|
!isNonNegativeInteger(value.eventSequence) ||
|
|
656
750
|
!isNonNegativeInteger(value.watchSequence) ||
|
|
751
|
+
!isNonNegativeInteger(value.codexEndpointRefreshSequence) ||
|
|
752
|
+
!isNonNegativeInteger(value.codexOrphanRemovalSequence) ||
|
|
657
753
|
!Array.isArray(value.routes) ||
|
|
658
754
|
!value.routes.every(isRouteRecord) ||
|
|
659
755
|
!Array.isArray(value.pairs) ||
|
|
@@ -668,6 +764,14 @@ function isPersistedState(value) {
|
|
|
668
764
|
value.compatibilityAttestations.length >
|
|
669
765
|
COMPATIBILITY_ATTESTATION_CAPACITY ||
|
|
670
766
|
!value.compatibilityAttestations.every(isCompatibilityAttestation) ||
|
|
767
|
+
!Array.isArray(value.codexEndpointRefreshEvents) ||
|
|
768
|
+
value.codexEndpointRefreshEvents.length >
|
|
769
|
+
CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY ||
|
|
770
|
+
!value.codexEndpointRefreshEvents.every(isCodexEndpointRefreshJournalEvent) ||
|
|
771
|
+
!Array.isArray(value.codexOrphanRemovalEvents) ||
|
|
772
|
+
value.codexOrphanRemovalEvents.length >
|
|
773
|
+
CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY ||
|
|
774
|
+
!value.codexOrphanRemovalEvents.every(isCodexOrphanRemovalJournalEvent) ||
|
|
671
775
|
!Array.isArray(value.connectors) ||
|
|
672
776
|
!value.connectors.every(isConnectorRecord) ||
|
|
673
777
|
!Array.isArray(value.queue) ||
|
|
@@ -770,6 +874,23 @@ function isPersistedState(value) {
|
|
|
770
874
|
};
|
|
771
875
|
const sequencesStrictlyIncrease = candidate.events.every((event, index) => index === 0 ||
|
|
772
876
|
event.sequence > (candidate.events[index - 1]?.sequence ?? 0));
|
|
877
|
+
const endpointRefreshSequenceConsistent = candidate.codexEndpointRefreshEvents.length === 0
|
|
878
|
+
? candidate.codexEndpointRefreshSequence === 0
|
|
879
|
+
: candidate.codexEndpointRefreshEvents.at(-1)?.sequence ===
|
|
880
|
+
candidate.codexEndpointRefreshSequence &&
|
|
881
|
+
candidate.codexEndpointRefreshEvents.every((event, index) => index === 0 ||
|
|
882
|
+
event.sequence ===
|
|
883
|
+
(candidate.codexEndpointRefreshEvents[index - 1]?.sequence ??
|
|
884
|
+
0) +
|
|
885
|
+
1);
|
|
886
|
+
const orphanRemovalSequenceConsistent = candidate.codexOrphanRemovalEvents.length === 0
|
|
887
|
+
? candidate.codexOrphanRemovalSequence === 0
|
|
888
|
+
: candidate.codexOrphanRemovalEvents.at(-1)?.sequence ===
|
|
889
|
+
candidate.codexOrphanRemovalSequence &&
|
|
890
|
+
candidate.codexOrphanRemovalEvents.every((event, index) => index === 0 ||
|
|
891
|
+
event.sequence ===
|
|
892
|
+
(candidate.codexOrphanRemovalEvents[index - 1]?.sequence ?? 0) +
|
|
893
|
+
1);
|
|
773
894
|
return (aliases.size === candidate.routes.length &&
|
|
774
895
|
new Set(pairKeys).size === pairKeys.length &&
|
|
775
896
|
pairsValid &&
|
|
@@ -784,6 +905,8 @@ function isPersistedState(value) {
|
|
|
784
905
|
candidate.accounting.queuedBytes === expectedQueuedBytes &&
|
|
785
906
|
sequencesStrictlyIncrease &&
|
|
786
907
|
candidate.events.every((event) => event.sequence <= candidate.eventSequence) &&
|
|
908
|
+
endpointRefreshSequenceConsistent &&
|
|
909
|
+
orphanRemovalSequenceConsistent &&
|
|
787
910
|
candidate.routes.every((route) => route.alias.endsWith(`@${route.binding.hostId}`) &&
|
|
788
911
|
((route.binding.provider === "codex" &&
|
|
789
912
|
route.registrationMode === "explicit_opt_in") ||
|
|
@@ -1105,6 +1228,10 @@ export class GatewayStore {
|
|
|
1105
1228
|
progressWatches: [],
|
|
1106
1229
|
progressWatchEvents: [],
|
|
1107
1230
|
compatibilityAttestations: [],
|
|
1231
|
+
codexEndpointRefreshSequence: 0,
|
|
1232
|
+
codexEndpointRefreshEvents: [],
|
|
1233
|
+
codexOrphanRemovalSequence: 0,
|
|
1234
|
+
codexOrphanRemovalEvents: [],
|
|
1108
1235
|
codexSuccession: null,
|
|
1109
1236
|
};
|
|
1110
1237
|
if (this.state.pairs.length > this.config.limits.maxPairs) {
|
|
@@ -1183,7 +1310,7 @@ export class GatewayStore {
|
|
|
1183
1310
|
}
|
|
1184
1311
|
});
|
|
1185
1312
|
}
|
|
1186
|
-
async markConnectorOffline(identity, safeErrorCode = "CONNECTOR_OFFLINE", inFlightSettlements = []) {
|
|
1313
|
+
async markConnectorOffline(identity, safeErrorCode = "CONNECTOR_OFFLINE", inFlightSettlements = [], options = {}) {
|
|
1187
1314
|
return await this.mutate(async (state, now) => {
|
|
1188
1315
|
this.assertAllowedIdentity(identity);
|
|
1189
1316
|
if (!SAFE_CODE_PATTERN.test(safeErrorCode)) {
|
|
@@ -1211,7 +1338,7 @@ export class GatewayStore {
|
|
|
1211
1338
|
route.updatedAt = now.toISOString();
|
|
1212
1339
|
route.safeErrorCode = safeErrorCode;
|
|
1213
1340
|
}
|
|
1214
|
-
return this.terminateAffectedMessages(state, affectedAliases, now, safeErrorCode, settlementPlan);
|
|
1341
|
+
return this.terminateAffectedMessages(state, affectedAliases, now, safeErrorCode, settlementPlan, options.preserveQueued === true);
|
|
1215
1342
|
});
|
|
1216
1343
|
}
|
|
1217
1344
|
async registerRoute(input) {
|
|
@@ -1261,7 +1388,7 @@ export class GatewayStore {
|
|
|
1261
1388
|
registeredAt: now.toISOString(),
|
|
1262
1389
|
updatedAt: now.toISOString(),
|
|
1263
1390
|
lastSeenAt: now.toISOString(),
|
|
1264
|
-
queueDepth:
|
|
1391
|
+
queueDepth: state.queue.filter((item) => item.targetAlias === input.alias).length,
|
|
1265
1392
|
counters: emptyCounters(),
|
|
1266
1393
|
});
|
|
1267
1394
|
});
|
|
@@ -1372,7 +1499,7 @@ export class GatewayStore {
|
|
|
1372
1499
|
registeredAt: now.toISOString(),
|
|
1373
1500
|
updatedAt: now.toISOString(),
|
|
1374
1501
|
lastSeenAt: now.toISOString(),
|
|
1375
|
-
queueDepth:
|
|
1502
|
+
queueDepth: state.queue.filter((item) => item.targetAlias === replacement.alias).length,
|
|
1376
1503
|
counters: emptyCounters(),
|
|
1377
1504
|
});
|
|
1378
1505
|
}
|
|
@@ -1449,7 +1576,9 @@ export class GatewayStore {
|
|
|
1449
1576
|
if (!ALIAS_PATTERN.test(input.alias) ||
|
|
1450
1577
|
!ALIAS_PATTERN.test(newAlias) ||
|
|
1451
1578
|
!isPrivateRouteBinding(input.newBinding) ||
|
|
1452
|
-
!newAlias.endsWith(`@${input.newBinding.hostId}`)
|
|
1579
|
+
!newAlias.endsWith(`@${input.newBinding.hostId}`) ||
|
|
1580
|
+
(input.journalReason !== undefined &&
|
|
1581
|
+
input.journalReason !== "boot_reactivation")) {
|
|
1453
1582
|
throw new BridgeError("INVALID_ROUTE_REBIND", "The stale-route rebind request is malformed.");
|
|
1454
1583
|
}
|
|
1455
1584
|
this.assertAllowedIdentity(endpointOf(input.newBinding));
|
|
@@ -1457,10 +1586,8 @@ export class GatewayStore {
|
|
|
1457
1586
|
if (!route.enabled ||
|
|
1458
1587
|
route.state !== "stale" ||
|
|
1459
1588
|
route.compatibility !== "expired" ||
|
|
1460
|
-
route.queueDepth !== 0 ||
|
|
1461
|
-
state.queue.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias) ||
|
|
1462
1589
|
state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
|
|
1463
|
-
throw new BridgeError("ROUTE_REBIND_NOT_SAFE", "Only an enabled, expired
|
|
1590
|
+
throw new BridgeError("ROUTE_REBIND_NOT_SAFE", "Only an enabled, expired stale route without in-flight work can be rebound.");
|
|
1464
1591
|
}
|
|
1465
1592
|
if (route.binding.provider !== input.newBinding.provider ||
|
|
1466
1593
|
route.binding.hostId !== input.newBinding.hostId) {
|
|
@@ -1476,6 +1603,10 @@ export class GatewayStore {
|
|
|
1476
1603
|
(!claudeReasonAllowed || !sameLogicalRoute))) {
|
|
1477
1604
|
throw new BridgeError("ROUTE_RESELECTION_REQUIRED", "A stale route may rebind only the same provider-native logical identity and ownership lease.");
|
|
1478
1605
|
}
|
|
1606
|
+
if (input.journalReason === "boot_reactivation" &&
|
|
1607
|
+
route.binding.provider !== "codex") {
|
|
1608
|
+
throw new BridgeError("INVALID_ROUTE_REBIND", "Only an exact retained Codex route may record boot reactivation.");
|
|
1609
|
+
}
|
|
1479
1610
|
const connector = state.connectors.find((candidate) => sameEndpoint(candidate, input.newBinding));
|
|
1480
1611
|
if (!connector ||
|
|
1481
1612
|
!["healthy", "degraded"].includes(connector.health) ||
|
|
@@ -1488,7 +1619,12 @@ export class GatewayStore {
|
|
|
1488
1619
|
candidate.binding.ownerLease === input.newBinding.ownerLease))) {
|
|
1489
1620
|
throw new BridgeError("ROUTE_BINDING_COLLISION", "The replacement private binding already belongs to another alias.");
|
|
1490
1621
|
}
|
|
1622
|
+
if (input.journalReason === "boot_reactivation" &&
|
|
1623
|
+
state.codexEndpointRefreshSequence >= Number.MAX_SAFE_INTEGER) {
|
|
1624
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_SEQUENCE_EXHAUSTED", "The bounded Codex endpoint-refresh sequence is exhausted.");
|
|
1625
|
+
}
|
|
1491
1626
|
const previousAlias = route.alias;
|
|
1627
|
+
const oldEndpointGeneration = route.binding.endpointGeneration;
|
|
1492
1628
|
route.binding = { ...input.newBinding };
|
|
1493
1629
|
route.alias = newAlias;
|
|
1494
1630
|
route.state = input.state ?? "idle";
|
|
@@ -1497,9 +1633,8 @@ export class GatewayStore {
|
|
|
1497
1633
|
route.lastSeenAt = now.toISOString();
|
|
1498
1634
|
delete route.safeErrorCode;
|
|
1499
1635
|
if (previousAlias !== newAlias) {
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
// future caller uses the same primitive after another invalidation.
|
|
1636
|
+
// Retained queued mail follows only the exact re-observed logical
|
|
1637
|
+
// route. In-flight work remains forbidden above.
|
|
1503
1638
|
for (const item of [...state.queue, ...state.inFlight]) {
|
|
1504
1639
|
if (item.sourceAlias === previousAlias)
|
|
1505
1640
|
item.sourceAlias = newAlias;
|
|
@@ -1517,6 +1652,164 @@ export class GatewayStore {
|
|
|
1517
1652
|
renameRateBucket(state, previousAlias, newAlias);
|
|
1518
1653
|
renamePairAlias(state, previousAlias, newAlias, now);
|
|
1519
1654
|
}
|
|
1655
|
+
if (input.journalReason === "boot_reactivation") {
|
|
1656
|
+
this.appendCodexEndpointRefreshEvent(state, {
|
|
1657
|
+
timestamp: now.toISOString(),
|
|
1658
|
+
alias: route.alias,
|
|
1659
|
+
hostId: route.binding.hostId,
|
|
1660
|
+
threadId: route.binding.routeHandle,
|
|
1661
|
+
oldEndpointGeneration,
|
|
1662
|
+
newEndpointGeneration: route.binding.endpointGeneration,
|
|
1663
|
+
reason: "boot_reactivation",
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Atomically re-anchor only the exact Codex tasks proved present on a newly
|
|
1670
|
+
* compatible App Server generation. Omitted stale tasks stay untouched.
|
|
1671
|
+
*/
|
|
1672
|
+
async reanchorCodexRoutes(input) {
|
|
1673
|
+
return await this.mutate(async (state, now) => {
|
|
1674
|
+
if (!isObject(input) ||
|
|
1675
|
+
!hasOnlyKeys(input, ["oldEndpoint", "newEndpoint", "routes"]) ||
|
|
1676
|
+
!isPrivateEndpointIdentity(input.oldEndpoint) ||
|
|
1677
|
+
!isPrivateEndpointIdentity(input.newEndpoint) ||
|
|
1678
|
+
input.oldEndpoint.provider !== "codex" ||
|
|
1679
|
+
input.newEndpoint.provider !== "codex" ||
|
|
1680
|
+
input.oldEndpoint.hostId !== input.newEndpoint.hostId ||
|
|
1681
|
+
input.oldEndpoint.endpointGeneration ===
|
|
1682
|
+
input.newEndpoint.endpointGeneration ||
|
|
1683
|
+
!Array.isArray(input.routes) ||
|
|
1684
|
+
input.routes.length > this.config.limits.maxRoutes) {
|
|
1685
|
+
throw new BridgeError("INVALID_CODEX_ENDPOINT_REFRESH", "Codex endpoint refresh requires two distinct exact generations on one allowlisted host and a bounded route subset.");
|
|
1686
|
+
}
|
|
1687
|
+
this.assertAllowedIdentity(input.oldEndpoint);
|
|
1688
|
+
this.assertAllowedIdentity(input.newEndpoint);
|
|
1689
|
+
const newConnector = state.connectors.find((candidate) => sameEndpoint(candidate, input.newEndpoint));
|
|
1690
|
+
if (newConnector === undefined ||
|
|
1691
|
+
!["healthy", "degraded"].includes(newConnector.health) ||
|
|
1692
|
+
newConnector.compatibility !== "compatible") {
|
|
1693
|
+
throw new BridgeError("ROUTE_ENDPOINT_NOT_OBSERVED", "The replacement Codex endpoint generation must be positively observed and compatible.");
|
|
1694
|
+
}
|
|
1695
|
+
const aliases = new Set();
|
|
1696
|
+
const threadIds = new Set();
|
|
1697
|
+
const ownerLeases = new Set();
|
|
1698
|
+
const replacements = [];
|
|
1699
|
+
for (const candidate of input.routes) {
|
|
1700
|
+
if (!isObject(candidate) ||
|
|
1701
|
+
!hasOnlyKeys(candidate, ["alias", "threadId", "ownerLease"], ["state"]) ||
|
|
1702
|
+
typeof candidate.alias !== "string" ||
|
|
1703
|
+
!ALIAS_PATTERN.test(candidate.alias) ||
|
|
1704
|
+
!candidate.alias.startsWith("codex-") ||
|
|
1705
|
+
!candidate.alias.endsWith(`@${input.oldEndpoint.hostId}`) ||
|
|
1706
|
+
!isPrivateToken(candidate.threadId) ||
|
|
1707
|
+
!isPrivateToken(candidate.ownerLease) ||
|
|
1708
|
+
(candidate.state !== undefined &&
|
|
1709
|
+
candidate.state !== "idle" &&
|
|
1710
|
+
candidate.state !== "busy" &&
|
|
1711
|
+
candidate.state !== "awaiting_approval")) {
|
|
1712
|
+
throw new BridgeError("INVALID_CODEX_ENDPOINT_REFRESH", "A Codex endpoint refresh route proof is malformed.");
|
|
1713
|
+
}
|
|
1714
|
+
if (aliases.has(candidate.alias) ||
|
|
1715
|
+
threadIds.has(candidate.threadId) ||
|
|
1716
|
+
ownerLeases.has(candidate.ownerLease)) {
|
|
1717
|
+
throw new BridgeError("AMBIGUOUS_CODEX_ENDPOINT_REFRESH", "A Codex endpoint refresh cannot contain duplicate alias, task, or lease claims.");
|
|
1718
|
+
}
|
|
1719
|
+
aliases.add(candidate.alias);
|
|
1720
|
+
threadIds.add(candidate.threadId);
|
|
1721
|
+
ownerLeases.add(candidate.ownerLease);
|
|
1722
|
+
const route = state.routes.find((stored) => stored.alias === candidate.alias);
|
|
1723
|
+
if (route === undefined ||
|
|
1724
|
+
route.binding.provider !== "codex" ||
|
|
1725
|
+
route.registrationMode !== "explicit_opt_in" ||
|
|
1726
|
+
!route.enabled ||
|
|
1727
|
+
route.state !== "stale" ||
|
|
1728
|
+
route.compatibility !== "expired" ||
|
|
1729
|
+
!sameEndpoint(route.binding, input.oldEndpoint) ||
|
|
1730
|
+
route.binding.routeHandle !== candidate.threadId ||
|
|
1731
|
+
route.binding.ownerLease !== candidate.ownerLease ||
|
|
1732
|
+
state.inFlight.some((item) => item.sourceAlias === route.alias ||
|
|
1733
|
+
item.targetAlias === route.alias)) {
|
|
1734
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_NOT_SAFE", "Only an exact enabled, expired stale Codex task without in-flight work may be refreshed.");
|
|
1735
|
+
}
|
|
1736
|
+
const newBinding = {
|
|
1737
|
+
...route.binding,
|
|
1738
|
+
endpointGeneration: input.newEndpoint.endpointGeneration,
|
|
1739
|
+
};
|
|
1740
|
+
if (state.routes.some((stored) => stored !== route &&
|
|
1741
|
+
(sameRouteTarget(stored.binding, newBinding) ||
|
|
1742
|
+
stored.binding.ownerLease === newBinding.ownerLease))) {
|
|
1743
|
+
throw new BridgeError("ROUTE_BINDING_COLLISION", "A refreshed Codex task or ownership lease is already claimed by another route.");
|
|
1744
|
+
}
|
|
1745
|
+
replacements.push({ route, state: candidate.state ?? "idle" });
|
|
1746
|
+
}
|
|
1747
|
+
if (state.codexEndpointRefreshSequence >
|
|
1748
|
+
Number.MAX_SAFE_INTEGER - replacements.length) {
|
|
1749
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_SEQUENCE_EXHAUSTED", "The bounded Codex endpoint-refresh sequence is exhausted.");
|
|
1750
|
+
}
|
|
1751
|
+
for (const replacement of replacements) {
|
|
1752
|
+
const oldEndpointGeneration = replacement.route.binding.endpointGeneration;
|
|
1753
|
+
replacement.route.binding = {
|
|
1754
|
+
...replacement.route.binding,
|
|
1755
|
+
endpointGeneration: input.newEndpoint.endpointGeneration,
|
|
1756
|
+
};
|
|
1757
|
+
replacement.route.state = replacement.state;
|
|
1758
|
+
replacement.route.compatibility = "compatible";
|
|
1759
|
+
replacement.route.updatedAt = now.toISOString();
|
|
1760
|
+
replacement.route.lastSeenAt = now.toISOString();
|
|
1761
|
+
delete replacement.route.safeErrorCode;
|
|
1762
|
+
this.appendCodexEndpointRefreshEvent(state, {
|
|
1763
|
+
timestamp: now.toISOString(),
|
|
1764
|
+
alias: replacement.route.alias,
|
|
1765
|
+
hostId: replacement.route.binding.hostId,
|
|
1766
|
+
threadId: replacement.route.binding.routeHandle,
|
|
1767
|
+
oldEndpointGeneration,
|
|
1768
|
+
newEndpointGeneration: input.newEndpoint.endpointGeneration,
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
return { reboundAliases: replacements.map(({ route }) => route.alias) };
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Dashboard recovery primitive for one abandoned Codex registration. The
|
|
1776
|
+
* alias is only authority to request evaluation; durable state must prove
|
|
1777
|
+
* the task is empty, stale, expired, and on a dead or superseded generation.
|
|
1778
|
+
*/
|
|
1779
|
+
async removeStaleCodexOrphan(input) {
|
|
1780
|
+
return await this.mutate(async (state, now) => {
|
|
1781
|
+
if (!isObject(input) ||
|
|
1782
|
+
!hasOnlyKeys(input, ["alias"]) ||
|
|
1783
|
+
typeof input.alias !== "string") {
|
|
1784
|
+
throw new BridgeError("INVALID_CODEX_ORPHAN_RECOVERY", "Codex orphan recovery requires one exact Codex alias.");
|
|
1785
|
+
}
|
|
1786
|
+
const route = this.requireStaleCodexOrphan(state, input.alias);
|
|
1787
|
+
const aliases = new Set([route.alias]);
|
|
1788
|
+
const removedPairs = state.pairs
|
|
1789
|
+
.filter((pair) => pair.claudeAlias === route.alias || pair.codexAlias === route.alias)
|
|
1790
|
+
.map((pair) => ({
|
|
1791
|
+
claudeAlias: pair.claudeAlias,
|
|
1792
|
+
codexAlias: pair.codexAlias,
|
|
1793
|
+
}));
|
|
1794
|
+
if (state.codexOrphanRemovalSequence >= Number.MAX_SAFE_INTEGER) {
|
|
1795
|
+
throw new BridgeError("CODEX_ORPHAN_REMOVAL_SEQUENCE_EXHAUSTED", "The bounded Codex orphan-removal sequence is exhausted.");
|
|
1796
|
+
}
|
|
1797
|
+
this.settleProgressWatchesForAliases(state, aliases, now);
|
|
1798
|
+
state.routes = state.routes.filter((candidate) => candidate !== route);
|
|
1799
|
+
removePairsForAliases(state, aliases);
|
|
1800
|
+
state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== route.alias);
|
|
1801
|
+
state.dedupe = state.dedupe.filter((record) => record.sourceAlias !== route.alias &&
|
|
1802
|
+
record.targetAlias !== route.alias);
|
|
1803
|
+
this.appendCodexOrphanRemovalEvent(state, {
|
|
1804
|
+
timestamp: now.toISOString(),
|
|
1805
|
+
alias: route.alias,
|
|
1806
|
+
hostId: route.binding.hostId,
|
|
1807
|
+
});
|
|
1808
|
+
return {
|
|
1809
|
+
alias: route.alias,
|
|
1810
|
+
binding: { ...route.binding },
|
|
1811
|
+
removedPairs,
|
|
1812
|
+
};
|
|
1520
1813
|
});
|
|
1521
1814
|
}
|
|
1522
1815
|
async disableRoute(alias, ownerLease, inFlightSettlements = []) {
|
|
@@ -1651,6 +1944,64 @@ export class GatewayStore {
|
|
|
1651
1944
|
};
|
|
1652
1945
|
});
|
|
1653
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Read-only preflight for service-side provider cleanup. Removal repeats the
|
|
1949
|
+
* identical proof atomically so no intervening state change can authorize it.
|
|
1950
|
+
*/
|
|
1951
|
+
async inspectStaleCodexOrphan(alias) {
|
|
1952
|
+
return this.mutex.run("gateway", async () => {
|
|
1953
|
+
const route = this.requireStaleCodexOrphan(this.requireState(), alias);
|
|
1954
|
+
return { ...route.binding };
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Capture the exact pre-mutation authority needed to reconcile a possible
|
|
1959
|
+
* post-rename orphan-removal commit. This value remains controller-private.
|
|
1960
|
+
*/
|
|
1961
|
+
async inspectStaleCodexOrphanRemovalAuthority(alias) {
|
|
1962
|
+
return this.mutex.run("gateway", async () => {
|
|
1963
|
+
const state = this.requireState();
|
|
1964
|
+
const route = this.requireStaleCodexOrphan(state, alias);
|
|
1965
|
+
return {
|
|
1966
|
+
binding: { ...route.binding },
|
|
1967
|
+
previousSequence: state.codexOrphanRemovalSequence,
|
|
1968
|
+
};
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Fail-closed reconciliation for one exact removal whose durable rename may
|
|
1973
|
+
* have committed before a directory-sync error. The latest private journal
|
|
1974
|
+
* row, monotonic predecessor, alias absence, and native authority absence
|
|
1975
|
+
* must all corroborate the same mutation.
|
|
1976
|
+
*/
|
|
1977
|
+
async wasStaleCodexOrphanRemovalCommitted(input) {
|
|
1978
|
+
return this.mutex.run("gateway", async () => {
|
|
1979
|
+
if (!isObject(input) ||
|
|
1980
|
+
!hasOnlyKeys(input, ["alias", "binding", "previousSequence"]) ||
|
|
1981
|
+
typeof input.alias !== "string" ||
|
|
1982
|
+
!ALIAS_PATTERN.test(input.alias) ||
|
|
1983
|
+
!input.alias.startsWith("codex-") ||
|
|
1984
|
+
!isPrivateRouteBinding(input.binding) ||
|
|
1985
|
+
input.binding.provider !== "codex" ||
|
|
1986
|
+
!input.alias.endsWith(`@${input.binding.hostId}`) ||
|
|
1987
|
+
!isNonNegativeInteger(input.previousSequence) ||
|
|
1988
|
+
input.previousSequence >= Number.MAX_SAFE_INTEGER) {
|
|
1989
|
+
throw new BridgeError("INVALID_CODEX_ORPHAN_COMMIT_PROOF", "Codex orphan-removal reconciliation requires one exact private pre-mutation authority.");
|
|
1990
|
+
}
|
|
1991
|
+
this.assertAllowedIdentity(endpointOf(input.binding));
|
|
1992
|
+
const state = this.requireState();
|
|
1993
|
+
const expectedSequence = input.previousSequence + 1;
|
|
1994
|
+
const latest = state.codexOrphanRemovalEvents.at(-1);
|
|
1995
|
+
const routeOrAuthorityPresent = state.routes.some((route) => route.alias === input.alias ||
|
|
1996
|
+
sameRouteTarget(route.binding, input.binding) ||
|
|
1997
|
+
route.binding.ownerLease === input.binding.ownerLease);
|
|
1998
|
+
return (!routeOrAuthorityPresent &&
|
|
1999
|
+
state.codexOrphanRemovalSequence === expectedSequence &&
|
|
2000
|
+
latest?.sequence === expectedSequence &&
|
|
2001
|
+
latest.alias === input.alias &&
|
|
2002
|
+
latest.hostId === input.binding.hostId);
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
1654
2005
|
/**
|
|
1655
2006
|
* Controller-internal inventory used to preserve exact Codex registration
|
|
1656
2007
|
* identities across restart. Native task handles never cross the control
|
|
@@ -2336,7 +2687,9 @@ export class GatewayStore {
|
|
|
2336
2687
|
targetAlias: input.target.alias,
|
|
2337
2688
|
direction: "codex_to_claude",
|
|
2338
2689
|
sourceRoute: source,
|
|
2339
|
-
...(input.pair === true
|
|
2690
|
+
...(input.pair === true
|
|
2691
|
+
? { pair: true }
|
|
2692
|
+
: { transientTarget: true }),
|
|
2340
2693
|
});
|
|
2341
2694
|
});
|
|
2342
2695
|
}
|
|
@@ -2363,7 +2716,7 @@ export class GatewayStore {
|
|
|
2363
2716
|
const target = state.routes.find((route) => route.alias === metadata.targetAlias);
|
|
2364
2717
|
if (target)
|
|
2365
2718
|
target.queueDepth = Math.max(0, target.queueDepth - 1);
|
|
2366
|
-
const body = this.transientBodies.get(metadata.messageId);
|
|
2719
|
+
const body = this.transientBodies.get(metadata.messageId) ?? metadata.body;
|
|
2367
2720
|
this.transientBodies.delete(metadata.messageId);
|
|
2368
2721
|
if (body === undefined) {
|
|
2369
2722
|
this.finishMetadata(state, metadata, "abandoned", now, "TRANSIENT_BODY_UNAVAILABLE");
|
|
@@ -2381,6 +2734,7 @@ export class GatewayStore {
|
|
|
2381
2734
|
targetAlias: metadata.targetAlias,
|
|
2382
2735
|
state: "dispatching",
|
|
2383
2736
|
bytes: metadata.bytes,
|
|
2737
|
+
body,
|
|
2384
2738
|
hopCount: metadata.hopCount,
|
|
2385
2739
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2386
2740
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
@@ -2388,6 +2742,10 @@ export class GatewayStore {
|
|
|
2388
2742
|
return { ...metadata, body };
|
|
2389
2743
|
});
|
|
2390
2744
|
}
|
|
2745
|
+
/** Controller-private shutdown inventory; full IDs never cross control output. */
|
|
2746
|
+
async inspectQueuedMessageIds() {
|
|
2747
|
+
return this.mutex.run("gateway", async () => this.requireState().queue.map((item) => item.messageId));
|
|
2748
|
+
}
|
|
2391
2749
|
/**
|
|
2392
2750
|
* Atomically terminalizes every message whose delivery deadline is due.
|
|
2393
2751
|
* Returned settlements are emitted only by the mutation that removed the
|
|
@@ -2462,9 +2820,13 @@ export class GatewayStore {
|
|
|
2462
2820
|
};
|
|
2463
2821
|
});
|
|
2464
2822
|
}
|
|
2465
|
-
async requeueInFlightMessage(messageId, body) {
|
|
2823
|
+
async requeueInFlightMessage(messageId, body, safeErrorCode) {
|
|
2466
2824
|
return this.mutate(async (state, now) => {
|
|
2467
|
-
if (!MESSAGE_ID_PATTERN.test(messageId) ||
|
|
2825
|
+
if (!MESSAGE_ID_PATTERN.test(messageId) ||
|
|
2826
|
+
typeof body !== "string" ||
|
|
2827
|
+
body.length === 0 ||
|
|
2828
|
+
body.includes("\u0000") ||
|
|
2829
|
+
(safeErrorCode !== undefined && !SAFE_CODE_PATTERN.test(safeErrorCode))) {
|
|
2468
2830
|
throw new BridgeError("INVALID_GATEWAY_MESSAGE", "Only an exact in-flight message can be returned to the queue.");
|
|
2469
2831
|
}
|
|
2470
2832
|
const index = state.inFlight.findIndex((item) => item.messageId === messageId);
|
|
@@ -2472,6 +2834,9 @@ export class GatewayStore {
|
|
|
2472
2834
|
if (metadata === undefined || index < 0) {
|
|
2473
2835
|
return { status: "not_in_flight" };
|
|
2474
2836
|
}
|
|
2837
|
+
if (Buffer.byteLength(body, "utf8") !== metadata.bytes) {
|
|
2838
|
+
throw new BridgeError("INVALID_GATEWAY_MESSAGE", "A requeued message body must exactly match its admitted byte count.");
|
|
2839
|
+
}
|
|
2475
2840
|
state.inFlight.splice(index, 1);
|
|
2476
2841
|
if (Date.parse(metadata.deadlineAt) <= now.getTime()) {
|
|
2477
2842
|
return {
|
|
@@ -2480,6 +2845,7 @@ export class GatewayStore {
|
|
|
2480
2845
|
};
|
|
2481
2846
|
}
|
|
2482
2847
|
const { dispatchedAt: _dispatchedAt, ...queuedMetadata } = metadata;
|
|
2848
|
+
queuedMetadata.body = body;
|
|
2483
2849
|
state.queue.unshift(queuedMetadata);
|
|
2484
2850
|
this.transientBodies.set(messageId, body);
|
|
2485
2851
|
state.accounting.queuedBytes += metadata.bytes;
|
|
@@ -2497,8 +2863,10 @@ export class GatewayStore {
|
|
|
2497
2863
|
targetAlias: metadata.targetAlias,
|
|
2498
2864
|
state: "held",
|
|
2499
2865
|
bytes: metadata.bytes,
|
|
2866
|
+
body,
|
|
2500
2867
|
hopCount: metadata.hopCount,
|
|
2501
2868
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2869
|
+
...(safeErrorCode === undefined ? {} : { safeErrorCode }),
|
|
2502
2870
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
2503
2871
|
});
|
|
2504
2872
|
return { status: "requeued" };
|
|
@@ -2527,6 +2895,7 @@ export class GatewayStore {
|
|
|
2527
2895
|
targetAlias: metadata.targetAlias,
|
|
2528
2896
|
state: progress,
|
|
2529
2897
|
bytes: metadata.bytes,
|
|
2898
|
+
...(metadata.body === undefined ? {} : { body: metadata.body }),
|
|
2530
2899
|
hopCount: metadata.hopCount,
|
|
2531
2900
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2532
2901
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
@@ -2947,6 +3316,42 @@ export class GatewayStore {
|
|
|
2947
3316
|
}
|
|
2948
3317
|
return route;
|
|
2949
3318
|
}
|
|
3319
|
+
requireStaleCodexOrphan(state, alias) {
|
|
3320
|
+
if (typeof alias !== "string" ||
|
|
3321
|
+
!ALIAS_PATTERN.test(alias) ||
|
|
3322
|
+
!alias.startsWith("codex-")) {
|
|
3323
|
+
throw new BridgeError("INVALID_CODEX_ORPHAN_RECOVERY", "Codex orphan recovery requires one exact Codex alias.");
|
|
3324
|
+
}
|
|
3325
|
+
const route = state.routes.find((candidate) => candidate.alias === alias);
|
|
3326
|
+
if (route === undefined) {
|
|
3327
|
+
throw new BridgeError("CODEX_ORPHAN_NOT_FOUND", "No Codex registration matches the requested alias.");
|
|
3328
|
+
}
|
|
3329
|
+
if (route.binding.provider !== "codex" ||
|
|
3330
|
+
route.registrationMode !== "explicit_opt_in" ||
|
|
3331
|
+
!route.enabled ||
|
|
3332
|
+
route.state !== "stale" ||
|
|
3333
|
+
route.compatibility !== "expired" ||
|
|
3334
|
+
route.queueDepth !== 0 ||
|
|
3335
|
+
state.queue.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias) ||
|
|
3336
|
+
state.inFlight.some((item) => item.sourceAlias === route.alias || item.targetAlias === route.alias)) {
|
|
3337
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_NOT_SAFE", "Only an enabled, expired, empty stale Codex registration can be removed as an orphan.");
|
|
3338
|
+
}
|
|
3339
|
+
const connector = state.connectors.find((candidate) => candidate.provider === "codex" &&
|
|
3340
|
+
candidate.hostId === route.binding.hostId);
|
|
3341
|
+
const generationProvedDead = connector !== undefined &&
|
|
3342
|
+
(connector.endpointGeneration !== route.binding.endpointGeneration ||
|
|
3343
|
+
connector.health === "offline");
|
|
3344
|
+
if (!generationProvedDead) {
|
|
3345
|
+
throw new BridgeError("CODEX_ORPHAN_GENERATION_LIVE", "The Codex registration cannot be removed while its endpoint generation may still be live.");
|
|
3346
|
+
}
|
|
3347
|
+
const succession = this.journal(state);
|
|
3348
|
+
if (succession !== null &&
|
|
3349
|
+
(routeMatchesSuccessionIdentity(route, succession.old) ||
|
|
3350
|
+
routeMatchesSuccessionIdentity(route, succession.new))) {
|
|
3351
|
+
throw new BridgeError("CODEX_ORPHAN_RECOVERY_BLOCKED", "A Codex registration owned by an active succession cannot be removed as an orphan.");
|
|
3352
|
+
}
|
|
3353
|
+
return route;
|
|
3354
|
+
}
|
|
2950
3355
|
requireAvailableRoute(state, alias) {
|
|
2951
3356
|
if (!ALIAS_PATTERN.test(alias)) {
|
|
2952
3357
|
throw new BridgeError("INVALID_GATEWAY_ALIAS", "The selected alias does not use the required lowercase ASCII grammar.");
|
|
@@ -3160,8 +3565,12 @@ export class GatewayStore {
|
|
|
3160
3565
|
enqueuedAt: now.toISOString(),
|
|
3161
3566
|
deadlineAt: deadline.toISOString(),
|
|
3162
3567
|
bytes,
|
|
3568
|
+
body: input.body,
|
|
3163
3569
|
hopCount,
|
|
3164
3570
|
...(sides.pair === true ? { pair: true } : {}),
|
|
3571
|
+
...(sides.transientTarget === true
|
|
3572
|
+
? { transientTarget: true }
|
|
3573
|
+
: {}),
|
|
3165
3574
|
...(input.steer === true ? { steer: true } : {}),
|
|
3166
3575
|
};
|
|
3167
3576
|
state.queue.push(metadata);
|
|
@@ -3208,6 +3617,7 @@ export class GatewayStore {
|
|
|
3208
3617
|
targetAlias: sides.targetAlias,
|
|
3209
3618
|
state: "queued",
|
|
3210
3619
|
bytes,
|
|
3620
|
+
body: input.body,
|
|
3211
3621
|
hopCount,
|
|
3212
3622
|
...(input.steer === true ? { steer: true } : {}),
|
|
3213
3623
|
});
|
|
@@ -3280,11 +3690,73 @@ export class GatewayStore {
|
|
|
3280
3690
|
});
|
|
3281
3691
|
}
|
|
3282
3692
|
appendEvent(state, event) {
|
|
3693
|
+
if (event.body !== undefined) {
|
|
3694
|
+
for (const retained of state.events) {
|
|
3695
|
+
if (retained.body !== undefined &&
|
|
3696
|
+
retained.messageIdSuffix === event.messageIdSuffix &&
|
|
3697
|
+
retained.direction === event.direction &&
|
|
3698
|
+
retained.sourceAlias === event.sourceAlias &&
|
|
3699
|
+
retained.targetAlias === event.targetAlias &&
|
|
3700
|
+
retained.conversationIdSuffix === event.conversationIdSuffix) {
|
|
3701
|
+
delete retained.body;
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3283
3705
|
state.eventSequence += 1;
|
|
3284
3706
|
state.events.push({ sequence: state.eventSequence, ...event });
|
|
3285
3707
|
while (state.events.length > this.config.limits.eventCapacity) {
|
|
3286
3708
|
state.events.shift();
|
|
3287
3709
|
}
|
|
3710
|
+
this.pruneRetainedEventBodies(state);
|
|
3711
|
+
}
|
|
3712
|
+
pruneRetainedEventBodies(state) {
|
|
3713
|
+
const configured = this.config.limits.maxRetainedBodyBytes ??
|
|
3714
|
+
DEFAULT_RETAINED_BODY_BYTES;
|
|
3715
|
+
if (!Number.isSafeInteger(configured) ||
|
|
3716
|
+
configured < 1 ||
|
|
3717
|
+
configured > GATEWAY_MAX_STATE_FILE_BYTES) {
|
|
3718
|
+
throw new BridgeError("INVALID_GATEWAY_CONFIGURATION", "The retained message-body byte limit is invalid.");
|
|
3719
|
+
}
|
|
3720
|
+
let retainedBytes = state.events.reduce((total, event) => total +
|
|
3721
|
+
(event.body === undefined
|
|
3722
|
+
? 0
|
|
3723
|
+
: Buffer.byteLength(event.body, "utf8")), 0);
|
|
3724
|
+
for (const event of state.events) {
|
|
3725
|
+
if (retainedBytes <= configured)
|
|
3726
|
+
break;
|
|
3727
|
+
if (event.body === undefined)
|
|
3728
|
+
continue;
|
|
3729
|
+
retainedBytes -= Buffer.byteLength(event.body, "utf8");
|
|
3730
|
+
delete event.body;
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
appendCodexEndpointRefreshEvent(state, event) {
|
|
3734
|
+
if (state.codexEndpointRefreshSequence >= Number.MAX_SAFE_INTEGER) {
|
|
3735
|
+
throw new BridgeError("CODEX_ENDPOINT_REFRESH_SEQUENCE_EXHAUSTED", "The bounded Codex endpoint-refresh sequence is exhausted.");
|
|
3736
|
+
}
|
|
3737
|
+
state.codexEndpointRefreshSequence += 1;
|
|
3738
|
+
state.codexEndpointRefreshEvents.push({
|
|
3739
|
+
sequence: state.codexEndpointRefreshSequence,
|
|
3740
|
+
...event,
|
|
3741
|
+
});
|
|
3742
|
+
while (state.codexEndpointRefreshEvents.length >
|
|
3743
|
+
CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY) {
|
|
3744
|
+
state.codexEndpointRefreshEvents.shift();
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
appendCodexOrphanRemovalEvent(state, event) {
|
|
3748
|
+
if (state.codexOrphanRemovalSequence >= Number.MAX_SAFE_INTEGER) {
|
|
3749
|
+
throw new BridgeError("CODEX_ORPHAN_REMOVAL_SEQUENCE_EXHAUSTED", "The bounded Codex orphan-removal sequence is exhausted.");
|
|
3750
|
+
}
|
|
3751
|
+
state.codexOrphanRemovalSequence += 1;
|
|
3752
|
+
state.codexOrphanRemovalEvents.push({
|
|
3753
|
+
sequence: state.codexOrphanRemovalSequence,
|
|
3754
|
+
...event,
|
|
3755
|
+
});
|
|
3756
|
+
while (state.codexOrphanRemovalEvents.length >
|
|
3757
|
+
CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY) {
|
|
3758
|
+
state.codexOrphanRemovalEvents.shift();
|
|
3759
|
+
}
|
|
3288
3760
|
}
|
|
3289
3761
|
/** Commit watch activity only after every message-admission check succeeds. */
|
|
3290
3762
|
applyProgressWatchMessageActivity(state, now, activity, sides) {
|
|
@@ -3493,6 +3965,7 @@ export class GatewayStore {
|
|
|
3493
3965
|
targetAlias: metadata.targetAlias,
|
|
3494
3966
|
state: deliveryState,
|
|
3495
3967
|
bytes: metadata.bytes,
|
|
3968
|
+
...(metadata.body === undefined ? {} : { body: metadata.body }),
|
|
3496
3969
|
hopCount: metadata.hopCount,
|
|
3497
3970
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
3498
3971
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
@@ -3564,10 +4037,14 @@ export class GatewayStore {
|
|
|
3564
4037
|
}
|
|
3565
4038
|
return byMessageId;
|
|
3566
4039
|
}
|
|
3567
|
-
terminateAffectedMessages(state, aliases, now, safeErrorCode, inFlightSettlements) {
|
|
4040
|
+
terminateAffectedMessages(state, aliases, now, safeErrorCode, inFlightSettlements, preserveQueued = false) {
|
|
3568
4041
|
const settlements = [];
|
|
3569
|
-
const queued =
|
|
3570
|
-
|
|
4042
|
+
const queued = preserveQueued
|
|
4043
|
+
? []
|
|
4044
|
+
: state.queue.filter((item) => routeTerminationMatches(state, aliases, item));
|
|
4045
|
+
if (!preserveQueued) {
|
|
4046
|
+
state.queue = state.queue.filter((item) => !routeTerminationMatches(state, aliases, item));
|
|
4047
|
+
}
|
|
3571
4048
|
for (const item of queued) {
|
|
3572
4049
|
this.transientBodies.delete(item.messageId);
|
|
3573
4050
|
state.accounting.queuedBytes -= item.bytes;
|
|
@@ -3626,6 +4103,7 @@ export class GatewayStore {
|
|
|
3626
4103
|
.slice(-this.config.limits.dedupeCapacity);
|
|
3627
4104
|
state.rateBuckets = state.rateBuckets.filter((bucket) => now.getTime() - Date.parse(bucket.windowStartedAt) <
|
|
3628
4105
|
this.config.limits.rateWindowMs);
|
|
4106
|
+
this.pruneRetainedEventBodies(state);
|
|
3629
4107
|
}
|
|
3630
4108
|
recoverAfterRestart(now) {
|
|
3631
4109
|
const state = this.requireState();
|
|
@@ -3671,17 +4149,39 @@ export class GatewayStore {
|
|
|
3671
4149
|
route.safeErrorCode = "REOBSERVATION_REQUIRED";
|
|
3672
4150
|
route.queueDepth = 0;
|
|
3673
4151
|
}
|
|
4152
|
+
const retainedQueue = [];
|
|
4153
|
+
let retainedQueuedBytes = 0;
|
|
4154
|
+
this.transientBodies.clear();
|
|
3674
4155
|
for (const item of state.queue) {
|
|
3675
|
-
|
|
3676
|
-
|
|
4156
|
+
if (Date.parse(item.deadlineAt) <= now.getTime()) {
|
|
4157
|
+
this.finishMetadata(state, item, "expired", now, "MESSAGE_EXPIRED");
|
|
4158
|
+
continue;
|
|
4159
|
+
}
|
|
4160
|
+
if (item.body === undefined) {
|
|
4161
|
+
this.finishMetadata(state, item, "abandoned", now, "CONTROLLER_RESTARTED");
|
|
4162
|
+
continue;
|
|
4163
|
+
}
|
|
4164
|
+
// A correlated native reply admitted through a transient Claude
|
|
4165
|
+
// capability cannot regain its exact UUID/generation after restart,
|
|
4166
|
+
// even when another session later claims the same public alias.
|
|
4167
|
+
if (item.transientTarget === true ||
|
|
4168
|
+
!state.routes.some((route) => route.alias === item.targetAlias)) {
|
|
4169
|
+
this.finishMetadata(state, item, "abandoned", now, "CONTROLLER_RESTARTED");
|
|
4170
|
+
continue;
|
|
4171
|
+
}
|
|
4172
|
+
retainedQueue.push(item);
|
|
4173
|
+
retainedQueuedBytes += item.bytes;
|
|
4174
|
+
this.transientBodies.set(item.messageId, item.body);
|
|
4175
|
+
const target = state.routes.find((route) => route.alias === item.targetAlias);
|
|
4176
|
+
if (target !== undefined)
|
|
4177
|
+
target.queueDepth += 1;
|
|
3677
4178
|
}
|
|
3678
4179
|
for (const item of state.inFlight) {
|
|
3679
4180
|
this.finishMetadata(state, item, "ambiguous", now, "CONTROLLER_RESTARTED");
|
|
3680
4181
|
}
|
|
3681
|
-
state.queue =
|
|
4182
|
+
state.queue = retainedQueue;
|
|
3682
4183
|
state.inFlight = [];
|
|
3683
|
-
state.accounting.queuedBytes =
|
|
3684
|
-
this.transientBodies.clear();
|
|
4184
|
+
state.accounting.queuedBytes = retainedQueuedBytes;
|
|
3685
4185
|
}
|
|
3686
4186
|
recoverCodexSuccessionAfterRestart(state, now) {
|
|
3687
4187
|
const journal = this.journal(state);
|
|
@@ -3942,6 +4442,8 @@ export class GatewayStore {
|
|
|
3942
4442
|
parsed = migratePrePairGraph(parsed);
|
|
3943
4443
|
parsed = migratePreProgressWatches(parsed);
|
|
3944
4444
|
parsed = migratePreCompatibilityAttestations(parsed);
|
|
4445
|
+
parsed = migratePreCodexEndpointRefreshJournal(parsed);
|
|
4446
|
+
parsed = migratePreCodexOrphanRemovalJournal(parsed);
|
|
3945
4447
|
if (!isPersistedState(parsed)) {
|
|
3946
4448
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
|
|
3947
4449
|
}
|