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.
Files changed (73) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +50 -5
  3. package/README.zh-CN.md +32 -5
  4. package/SECURITY.md +26 -2
  5. package/assets/live-dashboard/app.css +14 -5
  6. package/dist/src/gateway/claude-helper-protocol.d.ts +3 -0
  7. package/dist/src/gateway/claude-helper-protocol.js +15 -1
  8. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  9. package/dist/src/gateway/claude-helper-supervisor.d.ts +2 -0
  10. package/dist/src/gateway/claude-helper-supervisor.js +25 -1
  11. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  12. package/dist/src/gateway/cli-copy.en.d.ts +1 -0
  13. package/dist/src/gateway/cli-copy.en.js +1 -0
  14. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  15. package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -0
  16. package/dist/src/gateway/cli-copy.zh-CN.js +1 -0
  17. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  18. package/dist/src/gateway/cli.d.ts +1 -1
  19. package/dist/src/gateway/cli.js +20 -7
  20. package/dist/src/gateway/cli.js.map +1 -1
  21. package/dist/src/gateway/codex-app-server.js +6 -2
  22. package/dist/src/gateway/codex-app-server.js.map +1 -1
  23. package/dist/src/gateway/codex-local-transport.d.ts +13 -0
  24. package/dist/src/gateway/codex-local-transport.js +21 -2
  25. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  26. package/dist/src/gateway/config.js +5 -3
  27. package/dist/src/gateway/config.js.map +1 -1
  28. package/dist/src/gateway/control.d.ts +11 -1
  29. package/dist/src/gateway/control.js +48 -14
  30. package/dist/src/gateway/control.js.map +1 -1
  31. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  32. package/dist/src/gateway/dashboard-copy.en.d.ts +6 -0
  33. package/dist/src/gateway/dashboard-copy.en.js +11 -5
  34. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  35. package/dist/src/gateway/dashboard-copy.js +6 -0
  36. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  37. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +6 -0
  38. package/dist/src/gateway/dashboard-copy.zh-CN.js +11 -5
  39. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  40. package/dist/src/gateway/dashboard-model.d.ts +4 -2
  41. package/dist/src/gateway/dashboard-model.js +6 -0
  42. package/dist/src/gateway/dashboard-model.js.map +1 -1
  43. package/dist/src/gateway/dashboard.js +5 -3
  44. package/dist/src/gateway/dashboard.js.map +1 -1
  45. package/dist/src/gateway/live-dashboard-app/app.js +48 -10
  46. package/dist/src/gateway/live-dashboard-command.js +10 -0
  47. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  48. package/dist/src/gateway/live-dashboard-http.d.ts +3 -0
  49. package/dist/src/gateway/live-dashboard-http.js +13 -0
  50. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  51. package/dist/src/gateway/live-dashboard-stream.js +26 -25
  52. package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
  53. package/dist/src/gateway/provenance-envelope.d.ts +16 -0
  54. package/dist/src/gateway/provenance-envelope.js +80 -0
  55. package/dist/src/gateway/provenance-envelope.js.map +1 -0
  56. package/dist/src/gateway/providers.d.ts +43 -16
  57. package/dist/src/gateway/providers.js +688 -61
  58. package/dist/src/gateway/providers.js.map +1 -1
  59. package/dist/src/gateway/server.d.ts +2 -0
  60. package/dist/src/gateway/server.js +9 -3
  61. package/dist/src/gateway/server.js.map +1 -1
  62. package/dist/src/gateway/service.d.ts +102 -10
  63. package/dist/src/gateway/service.js +1309 -54
  64. package/dist/src/gateway/service.js.map +1 -1
  65. package/dist/src/gateway/store.d.ts +39 -3
  66. package/dist/src/gateway/store.js +528 -26
  67. package/dist/src/gateway/store.js.map +1 -1
  68. package/dist/src/gateway/types.d.ts +90 -5
  69. package/dist/src/gateway/types.js +6 -0
  70. package/dist/src/gateway/types.js.map +1 -1
  71. package/docs/GATEWAY-ARCHITECTURE.md +98 -11
  72. package/package.json +3 -2
  73. 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: 0,
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: 0,
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, empty stale route can be rebound.");
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
- // Restart recovery has already emptied queue/in-flight state. Keep the
1501
- // metadata rewrite complete anyway so this mutation stays atomic if a
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 ? { 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) || typeof body !== "string") {
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 = state.queue.filter((item) => routeTerminationMatches(state, aliases, item));
3570
- state.queue = state.queue.filter((item) => !routeTerminationMatches(state, aliases, item));
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
- state.accounting.queuedBytes -= item.bytes;
3676
- this.finishMetadata(state, item, "abandoned", now, "CONTROLLER_RESTARTED");
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 = 0;
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
  }