agent-embassy 1.0.0 → 1.3.0

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