agent-embassy 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +50 -16
- package/README.md +57 -25
- package/README.zh-CN.md +45 -23
- package/SECURITY.md +68 -48
- package/dist/src/gateway/claude-helper-protocol.d.ts +1 -0
- package/dist/src/gateway/claude-helper-protocol.js +4 -2
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +1 -0
- package/dist/src/gateway/claude-helper-supervisor.js +3 -1
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +0 -2
- package/dist/src/gateway/claude-runtime.js +6 -17
- package/dist/src/gateway/claude-runtime.js.map +1 -1
- package/dist/src/gateway/cli-copy.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +4 -1
- package/dist/src/gateway/cli-copy.en.js +7 -3
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +3 -0
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +4 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +7 -3
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +2 -5
- package/dist/src/gateway/cli.js +41 -67
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +2 -8
- package/dist/src/gateway/codex-app-server.js +36 -62
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +4 -10
- package/dist/src/gateway/codex-local-transport.js +13 -16
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +0 -27
- package/dist/src/gateway/compatibility.js +1 -101
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/config.d.ts +1 -4
- package/dist/src/gateway/config.js +0 -10
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +2 -21
- package/dist/src/gateway/control.js +7 -36
- package/dist/src/gateway/control.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.d.ts +1 -1
- package/dist/src/gateway/dashboard-copy.en.d.ts +21 -8
- package/dist/src/gateway/dashboard-copy.en.js +40 -27
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +21 -8
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +21 -8
- package/dist/src/gateway/dashboard-copy.zh-CN.js +40 -27
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +4 -9
- package/dist/src/gateway/dashboard-model.js +159 -46
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.js +27 -16
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/instance-lease.d.ts +2 -3
- package/dist/src/gateway/instance-lease.js +17 -140
- package/dist/src/gateway/instance-lease.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +62 -63
- package/dist/src/gateway/live-dashboard-command.d.ts +5 -1
- package/dist/src/gateway/live-dashboard-command.js +8 -6
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +0 -4
- package/dist/src/gateway/live-dashboard-http.js +20 -94
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/live-dashboard-protocol.d.ts +1 -5
- package/dist/src/gateway/live-dashboard-protocol.js +0 -42
- package/dist/src/gateway/live-dashboard-protocol.js.map +1 -1
- package/dist/src/gateway/live-dashboard-server.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-server.js +18 -4
- package/dist/src/gateway/live-dashboard-server.js.map +1 -1
- package/dist/src/gateway/live-dashboard.d.ts +3 -6
- package/dist/src/gateway/live-dashboard.js +6 -42
- package/dist/src/gateway/live-dashboard.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +6 -5
- package/dist/src/gateway/progress-watch-machine.js +6 -6
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +1 -0
- package/dist/src/gateway/provenance-envelope.js +11 -4
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +2 -21
- package/dist/src/gateway/providers.js +21 -261
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.js +0 -3
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +5 -15
- package/dist/src/gateway/service.js +101 -265
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +9 -21
- package/dist/src/gateway/store.js +360 -140
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +1 -6
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +102 -0
- package/docs/CONFIGURATION.zh-CN.md +72 -0
- package/docs/DASHBOARD.md +103 -0
- package/docs/DASHBOARD.zh-CN.md +47 -0
- package/docs/DELIVERY.md +84 -0
- package/docs/DELIVERY.zh-CN.md +53 -0
- package/docs/GATEWAY-ARCHITECTURE.md +249 -153
- package/package.json +8 -4
- package/skills/embassy-peer/SKILL.md +25 -10
- package/dist/src/gateway/claude-compatibility-scratch.d.ts +0 -18
- package/dist/src/gateway/claude-compatibility-scratch.js +0 -158
- package/dist/src/gateway/claude-compatibility-scratch.js.map +0 -1
- package/dist/src/gateway/live-dashboard-bootstrap.d.ts +0 -36
- package/dist/src/gateway/live-dashboard-bootstrap.js +0 -176
- package/dist/src/gateway/live-dashboard-bootstrap.js.map +0 -1
- package/docs/DESIGN.md +0 -134
|
@@ -7,7 +7,7 @@ import { BridgeError } from "../errors.js";
|
|
|
7
7
|
import { KeyedMutex } from "../mutex.js";
|
|
8
8
|
import { isCodexRegistrationGeneration } from "./codex-registration-generation.js";
|
|
9
9
|
import { compatibilityCacheKey, isCompatibilityAttestation, } from "./compatibility.js";
|
|
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";
|
|
10
|
+
import { PROGRESS_WATCH_DEFAULT_CAPACITY, PROGRESS_WATCH_HARD_CAPACITY, PROGRESS_WATCH_MAX_IDLE_MS, PROGRESS_WATCH_MIN_IDLE_MS, createProgressWatchMachine, progressWatchCapabilities, progressWatchJournalKinds, progressWatchPhases, transitionProgressWatch, } from "./progress-watch-machine.js";
|
|
11
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";
|
|
@@ -271,7 +271,25 @@ function isPairRecord(value) {
|
|
|
271
271
|
isIsoTimestamp(value.updatedAt) &&
|
|
272
272
|
isRouteCounters(value.counters));
|
|
273
273
|
}
|
|
274
|
-
function
|
|
274
|
+
function progressWatchPairKey(watch) {
|
|
275
|
+
return [
|
|
276
|
+
`${watch.ownerAlias}\0${watch.ownerLease}`,
|
|
277
|
+
`${watch.workerAlias}\0${watch.workerLease}`,
|
|
278
|
+
]
|
|
279
|
+
.sort()
|
|
280
|
+
.join("\0");
|
|
281
|
+
}
|
|
282
|
+
function progressWatchMatchesPair(watch, pair) {
|
|
283
|
+
return ((watch.ownerAlias === pair.claudeAlias &&
|
|
284
|
+
watch.ownerLease === pair.claudeOwnerLease &&
|
|
285
|
+
watch.workerAlias === pair.codexAlias &&
|
|
286
|
+
watch.workerLease === pair.codexOwnerLease) ||
|
|
287
|
+
(watch.ownerAlias === pair.codexAlias &&
|
|
288
|
+
watch.ownerLease === pair.codexOwnerLease &&
|
|
289
|
+
watch.workerAlias === pair.claudeAlias &&
|
|
290
|
+
watch.workerLease === pair.claudeOwnerLease));
|
|
291
|
+
}
|
|
292
|
+
function isLegacyProgressWatchMachine(value) {
|
|
275
293
|
if (!isObject(value) ||
|
|
276
294
|
!hasOnlyKeys(value, [
|
|
277
295
|
"conversationId",
|
|
@@ -322,6 +340,10 @@ function isProgressWatchMachine(value) {
|
|
|
322
340
|
(value.workerReportedCompleteAt === undefined ||
|
|
323
341
|
isIsoTimestamp(value.workerReportedCompleteAt)));
|
|
324
342
|
}
|
|
343
|
+
function isProgressWatchMachine(value) {
|
|
344
|
+
return (isLegacyProgressWatchMachine(value) &&
|
|
345
|
+
!Object.hasOwn(value, "workerReportedCompleteAt"));
|
|
346
|
+
}
|
|
325
347
|
function isProgressWatchJournalEvent(value) {
|
|
326
348
|
return (isObject(value) &&
|
|
327
349
|
hasOnlyKeys(value, [
|
|
@@ -392,7 +414,6 @@ function isQueuedMetadata(value) {
|
|
|
392
414
|
"enqueuedAt",
|
|
393
415
|
"deadlineAt",
|
|
394
416
|
"bytes",
|
|
395
|
-
"hopCount",
|
|
396
417
|
], ["conversationIdSuffix", "body", "pair", "transientTarget", "steer"])) {
|
|
397
418
|
return false;
|
|
398
419
|
}
|
|
@@ -417,7 +438,6 @@ function isQueuedMetadata(value) {
|
|
|
417
438
|
value.body.length > 0 &&
|
|
418
439
|
!value.body.includes("\u0000") &&
|
|
419
440
|
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
420
|
-
isNonNegativeInteger(value.hopCount) &&
|
|
421
441
|
(value.pair === undefined || value.pair === true) &&
|
|
422
442
|
(value.transientTarget === undefined || value.transientTarget === true) &&
|
|
423
443
|
(value.steer === undefined || value.steer === true));
|
|
@@ -439,7 +459,6 @@ function isEvent(value) {
|
|
|
439
459
|
"targetAlias",
|
|
440
460
|
"state",
|
|
441
461
|
"bytes",
|
|
442
|
-
"hopCount",
|
|
443
462
|
], ["conversationIdSuffix", "body", "latencyMs", "safeErrorCode", "steer"])) {
|
|
444
463
|
return false;
|
|
445
464
|
}
|
|
@@ -464,7 +483,6 @@ function isEvent(value) {
|
|
|
464
483
|
value.body.length > 0 &&
|
|
465
484
|
!value.body.includes("\u0000") &&
|
|
466
485
|
Buffer.byteLength(value.body, "utf8") === value.bytes)) &&
|
|
467
|
-
isNonNegativeInteger(value.hopCount) &&
|
|
468
486
|
(value.latencyMs === undefined || isNonNegativeInteger(value.latencyMs)) &&
|
|
469
487
|
(value.safeErrorCode === undefined || isSafeCode(value.safeErrorCode)) &&
|
|
470
488
|
(value.steer === undefined || value.steer === true));
|
|
@@ -717,6 +735,168 @@ function migratePreCodexOrphanRemovalJournal(value) {
|
|
|
717
735
|
codexOrphanRemovalEvents: [],
|
|
718
736
|
};
|
|
719
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* v1.2.0 persisted hop counters on queue, in-flight, and event rows. Hop
|
|
740
|
+
* policy no longer exists; accept only the old bounded shape and discard the
|
|
741
|
+
* obsolete field before strict current-schema validation and persistence.
|
|
742
|
+
*/
|
|
743
|
+
function migrateLegacyHopCounts(value) {
|
|
744
|
+
if (!isObject(value) ||
|
|
745
|
+
value.schemaVersion !== 1 ||
|
|
746
|
+
!Array.isArray(value.queue) ||
|
|
747
|
+
!Array.isArray(value.inFlight) ||
|
|
748
|
+
!Array.isArray(value.events)) {
|
|
749
|
+
return value;
|
|
750
|
+
}
|
|
751
|
+
const strip = (entry) => {
|
|
752
|
+
if (!isObject(entry) || !Object.hasOwn(entry, "hopCount"))
|
|
753
|
+
return entry;
|
|
754
|
+
if (!isNonNegativeInteger(entry.hopCount) || entry.hopCount > 64) {
|
|
755
|
+
return entry;
|
|
756
|
+
}
|
|
757
|
+
const { hopCount: _legacyHopCount, ...current } = entry;
|
|
758
|
+
return current;
|
|
759
|
+
};
|
|
760
|
+
return {
|
|
761
|
+
...value,
|
|
762
|
+
queue: value.queue.map(strip),
|
|
763
|
+
inFlight: value.inFlight.map(strip),
|
|
764
|
+
events: value.events.map(strip),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function isLegacyCompatibilityCertification(value) {
|
|
768
|
+
if (!isObject(value))
|
|
769
|
+
return false;
|
|
770
|
+
const required = ["depth", "outcome", "certifiedAt"];
|
|
771
|
+
if (!hasOnlyKeys(value, required, ["safeErrorCode"]))
|
|
772
|
+
return false;
|
|
773
|
+
if ((value.depth !== "wire" &&
|
|
774
|
+
value.depth !== "thread_ops" &&
|
|
775
|
+
value.depth !== "turn") ||
|
|
776
|
+
(value.outcome !== "pass" && value.outcome !== "fail") ||
|
|
777
|
+
!isIsoTimestamp(value.certifiedAt) ||
|
|
778
|
+
new Date(Date.parse(value.certifiedAt)).toISOString() !==
|
|
779
|
+
value.certifiedAt ||
|
|
780
|
+
(value.safeErrorCode !== undefined && !isSafeCode(value.safeErrorCode))) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
return (value.outcome === "pass") === (value.safeErrorCode === undefined);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* v1.1-v1.2 compatibility rows could contain manual live-certification
|
|
787
|
+
* evidence. The ceremony is gone: accept only its exact old shape, discard
|
|
788
|
+
* it, and retain the automatic bounded probe evidence.
|
|
789
|
+
*/
|
|
790
|
+
function migrateLegacyCompatibilityCertifications(value) {
|
|
791
|
+
if (!isObject(value) ||
|
|
792
|
+
value.schemaVersion !== 1 ||
|
|
793
|
+
!Array.isArray(value.compatibilityAttestations)) {
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
let changed = false;
|
|
797
|
+
const compatibilityAttestations = value.compatibilityAttestations.map((entry) => {
|
|
798
|
+
if (!isObject(entry) || !Object.hasOwn(entry, "certification")) {
|
|
799
|
+
return entry;
|
|
800
|
+
}
|
|
801
|
+
if (!isLegacyCompatibilityCertification(entry.certification)) {
|
|
802
|
+
return entry;
|
|
803
|
+
}
|
|
804
|
+
const { certification: _legacyCertification, ...current } = entry;
|
|
805
|
+
if (!isCompatibilityAttestation(current))
|
|
806
|
+
return entry;
|
|
807
|
+
changed = true;
|
|
808
|
+
return current;
|
|
809
|
+
});
|
|
810
|
+
return changed ? { ...value, compatibilityAttestations } : value;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Before v1.4, separately tracked conversations could leave multiple active
|
|
814
|
+
* watches on one exact pair, and a worker completion could remain as a flag on
|
|
815
|
+
* an active watch. Settle both legacy shapes into the bounded journal. Losers
|
|
816
|
+
* are ordered by their stable original-array position, so a persisted result
|
|
817
|
+
* is idempotent when loaded again.
|
|
818
|
+
*/
|
|
819
|
+
function migrateLegacyProgressWatchState(value, eventCapacity) {
|
|
820
|
+
if (!isObject(value) ||
|
|
821
|
+
value.schemaVersion !== 1 ||
|
|
822
|
+
!isIsoTimestamp(value.updatedAt) ||
|
|
823
|
+
!isNonNegativeInteger(value.watchSequence) ||
|
|
824
|
+
!Array.isArray(value.progressWatches) ||
|
|
825
|
+
value.progressWatches.length > PROGRESS_WATCH_HARD_CAPACITY ||
|
|
826
|
+
!value.progressWatches.every(isLegacyProgressWatchMachine) ||
|
|
827
|
+
!Array.isArray(value.progressWatchEvents) ||
|
|
828
|
+
!value.progressWatchEvents.every(isProgressWatchJournalEvent) ||
|
|
829
|
+
value.progressWatchEvents.length > eventCapacity ||
|
|
830
|
+
new Set(value.progressWatches.map((watch) => watch.conversationId)).size !== value.progressWatches.length) {
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
const progressWatches = value.progressWatches;
|
|
834
|
+
const existingEvents = value.progressWatchEvents;
|
|
835
|
+
const watchSequenceBefore = value.watchSequence;
|
|
836
|
+
if (existingEvents.some((event, index) => event.sequence > watchSequenceBefore ||
|
|
837
|
+
(index > 0 && event.sequence <= existingEvents[index - 1].sequence))) {
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
const winnerByPair = new Map();
|
|
841
|
+
for (const [index, watch] of progressWatches.entries()) {
|
|
842
|
+
const key = progressWatchPairKey(watch);
|
|
843
|
+
const winnerIndex = winnerByPair.get(key);
|
|
844
|
+
const winner = winnerIndex === undefined
|
|
845
|
+
? undefined
|
|
846
|
+
: progressWatches[winnerIndex];
|
|
847
|
+
if (winner === undefined ||
|
|
848
|
+
Date.parse(watch.createdAt) >= Date.parse(winner.createdAt)) {
|
|
849
|
+
winnerByPair.set(key, index);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const winnerIndexes = new Set(winnerByPair.values());
|
|
853
|
+
const loserIndexes = progressWatches
|
|
854
|
+
.map((_, index) => index)
|
|
855
|
+
.filter((index) => !winnerIndexes.has(index));
|
|
856
|
+
const completedWinnerIndexes = [...winnerByPair.values()].filter((index) => progressWatches[index]?.workerReportedCompleteAt !== undefined);
|
|
857
|
+
const additions = loserIndexes.length + completedWinnerIndexes.length;
|
|
858
|
+
if (additions === 0)
|
|
859
|
+
return value;
|
|
860
|
+
if (watchSequenceBefore > Number.MAX_SAFE_INTEGER - additions) {
|
|
861
|
+
throw new BridgeError("PROGRESS_WATCH_SEQUENCE_EXHAUSTED", "The bounded progress-watch journal sequence is exhausted.");
|
|
862
|
+
}
|
|
863
|
+
const retained = new Set(winnerIndexes);
|
|
864
|
+
for (const index of completedWinnerIndexes)
|
|
865
|
+
retained.delete(index);
|
|
866
|
+
let watchSequence = watchSequenceBefore;
|
|
867
|
+
const progressWatchEvents = [...existingEvents];
|
|
868
|
+
const append = (watch, kind, timestamp) => {
|
|
869
|
+
watchSequence += 1;
|
|
870
|
+
progressWatchEvents.push({
|
|
871
|
+
sequence: watchSequence,
|
|
872
|
+
timestamp,
|
|
873
|
+
conversationId: watch.conversationId,
|
|
874
|
+
ownerAlias: watch.ownerAlias,
|
|
875
|
+
workerAlias: watch.workerAlias,
|
|
876
|
+
kind,
|
|
877
|
+
});
|
|
878
|
+
while (progressWatchEvents.length > eventCapacity) {
|
|
879
|
+
progressWatchEvents.shift();
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
for (const index of loserIndexes) {
|
|
883
|
+
const watch = progressWatches[index];
|
|
884
|
+
const winner = progressWatches[winnerByPair.get(progressWatchPairKey(watch))];
|
|
885
|
+
append(watch, "replaced", winner.createdAt);
|
|
886
|
+
}
|
|
887
|
+
for (const index of completedWinnerIndexes) {
|
|
888
|
+
const watch = progressWatches[index];
|
|
889
|
+
append(watch, "worker_reported_complete", watch.workerReportedCompleteAt);
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
...value,
|
|
893
|
+
watchSequence,
|
|
894
|
+
progressWatches: progressWatches
|
|
895
|
+
.filter((_, index) => retained.has(index))
|
|
896
|
+
.map(({ workerReportedCompleteAt: _legacy, ...watch }) => watch),
|
|
897
|
+
progressWatchEvents,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
720
900
|
function isPersistedState(value) {
|
|
721
901
|
if (!isObject(value) ||
|
|
722
902
|
!hasOnlyKeys(value, [
|
|
@@ -802,6 +982,9 @@ function isPersistedState(value) {
|
|
|
802
982
|
if (watchConversationIds.size !== candidate.progressWatches.length) {
|
|
803
983
|
return false;
|
|
804
984
|
}
|
|
985
|
+
const watchPairKeys = new Set(candidate.progressWatches.map(progressWatchPairKey));
|
|
986
|
+
if (watchPairKeys.size !== candidate.progressWatches.length)
|
|
987
|
+
return false;
|
|
805
988
|
const compatibilityKeys = candidate.compatibilityAttestations.map(compatibilityCacheKey);
|
|
806
989
|
if (new Set(compatibilityKeys).size !== compatibilityKeys.length) {
|
|
807
990
|
return false;
|
|
@@ -874,6 +1057,10 @@ function isPersistedState(value) {
|
|
|
874
1057
|
};
|
|
875
1058
|
const sequencesStrictlyIncrease = candidate.events.every((event, index) => index === 0 ||
|
|
876
1059
|
event.sequence > (candidate.events[index - 1]?.sequence ?? 0));
|
|
1060
|
+
const progressWatchSequencesStrictlyIncrease = candidate.progressWatchEvents.every((event, index) => event.sequence <= candidate.watchSequence &&
|
|
1061
|
+
(index === 0 ||
|
|
1062
|
+
event.sequence >
|
|
1063
|
+
(candidate.progressWatchEvents[index - 1]?.sequence ?? 0)));
|
|
877
1064
|
const endpointRefreshSequenceConsistent = candidate.codexEndpointRefreshEvents.length === 0
|
|
878
1065
|
? candidate.codexEndpointRefreshSequence === 0
|
|
879
1066
|
: candidate.codexEndpointRefreshEvents.at(-1)?.sequence ===
|
|
@@ -904,6 +1091,7 @@ function isPersistedState(value) {
|
|
|
904
1091
|
candidate.rateBuckets.length &&
|
|
905
1092
|
candidate.accounting.queuedBytes === expectedQueuedBytes &&
|
|
906
1093
|
sequencesStrictlyIncrease &&
|
|
1094
|
+
progressWatchSequencesStrictlyIncrease &&
|
|
907
1095
|
candidate.events.every((event) => event.sequence <= candidate.eventSequence) &&
|
|
908
1096
|
endpointRefreshSequenceConsistent &&
|
|
909
1097
|
orphanRemovalSequenceConsistent &&
|
|
@@ -1440,6 +1628,8 @@ export class GatewayStore {
|
|
|
1440
1628
|
// selected Claude routes and their independent consent edges survive.
|
|
1441
1629
|
const retired = selectedClaudeRoutes.filter((route) => route !== retained && route === aliasOwner);
|
|
1442
1630
|
const retiredAliases = new Set(retired.map((route) => route.alias));
|
|
1631
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => retiredAliases.has(watch.ownerAlias) ||
|
|
1632
|
+
retiredAliases.has(watch.workerAlias)).length);
|
|
1443
1633
|
const settlementPlan = this.validateAffectedInFlightSettlements(state, retiredAliases, input.inFlightSettlements ?? []);
|
|
1444
1634
|
const settlements = this.terminateAffectedMessages(state, retiredAliases, now, "ROUTE_UNREGISTERED", settlementPlan);
|
|
1445
1635
|
this.settleProgressWatchesForAliases(state, retiredAliases, now);
|
|
@@ -1816,6 +2006,7 @@ export class GatewayStore {
|
|
|
1816
2006
|
return await this.mutate(async (state, now) => {
|
|
1817
2007
|
const route = this.requireOwnedRoute(state, alias, ownerLease);
|
|
1818
2008
|
const aliases = new Set([route.alias]);
|
|
2009
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => aliases.has(watch.ownerAlias) || aliases.has(watch.workerAlias)).length);
|
|
1819
2010
|
const settlementPlan = this.validateAffectedInFlightSettlements(state, aliases, inFlightSettlements);
|
|
1820
2011
|
route.enabled = false;
|
|
1821
2012
|
route.state = "disabled";
|
|
@@ -2271,65 +2462,34 @@ export class GatewayStore {
|
|
|
2271
2462
|
.sort((left, right) => pairKey(left.claudeAlias, left.codexAlias).localeCompare(pairKey(right.claudeAlias, right.codexAlias)));
|
|
2272
2463
|
});
|
|
2273
2464
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2465
|
+
async inspectProgressWatches() {
|
|
2466
|
+
return this.mutex.run("gateway", async () => this.requireState().progressWatches.map((watch) => ({ ...watch })));
|
|
2467
|
+
}
|
|
2468
|
+
/** Bind one restarted route-capability watch to its first exact owner send. */
|
|
2469
|
+
async bindProgressWatchConversation(input) {
|
|
2276
2470
|
return this.mutate(async (state, now) => {
|
|
2277
|
-
if (this.config.trackingEnabled === false) {
|
|
2278
|
-
throw new BridgeError("PROGRESS_TRACKING_DISABLED", "Progress tracking is disabled by the controller configuration.");
|
|
2279
|
-
}
|
|
2280
2471
|
if (!CONVERSATION_ID_PATTERN.test(input.conversationId) ||
|
|
2281
2472
|
!ALIAS_PATTERN.test(input.ownerAlias) ||
|
|
2282
2473
|
!ALIAS_PATTERN.test(input.workerAlias) ||
|
|
2283
|
-
input.ownerAlias === input.workerAlias
|
|
2284
|
-
|
|
2285
|
-
input.idleMs < PROGRESS_WATCH_MIN_IDLE_MS ||
|
|
2286
|
-
input.idleMs > PROGRESS_WATCH_MAX_IDLE_MS) {
|
|
2287
|
-
throw new BridgeError("INVALID_PROGRESS_WATCH", "The progress-watch request is malformed or outside its idle bound.");
|
|
2288
|
-
}
|
|
2289
|
-
const existing = state.progressWatches.find((watch) => watch.conversationId === input.conversationId);
|
|
2290
|
-
if (existing !== undefined) {
|
|
2291
|
-
if (existing.ownerAlias !== input.ownerAlias ||
|
|
2292
|
-
existing.workerAlias !== input.workerAlias) {
|
|
2293
|
-
throw new BridgeError("PROGRESS_WATCH_OWNERSHIP_MISMATCH", "The conversation watch belongs to different exact endpoints.");
|
|
2294
|
-
}
|
|
2295
|
-
return { created: false, watch: { ...existing } };
|
|
2474
|
+
input.ownerAlias === input.workerAlias) {
|
|
2475
|
+
throw new BridgeError("INVALID_PROGRESS_WATCH", "The progress-watch conversation binding is malformed.");
|
|
2296
2476
|
}
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
throw new BridgeError("PROGRESS_WATCH_EDGE_MISMATCH", "The watch endpoints do not form one exact consent edge.");
|
|
2312
|
-
}
|
|
2313
|
-
const watch = createProgressWatchMachine({
|
|
2314
|
-
conversationId: input.conversationId,
|
|
2315
|
-
ownerAlias: owner.alias,
|
|
2316
|
-
workerAlias: worker.alias,
|
|
2317
|
-
ownerLease: owner.binding.ownerLease,
|
|
2318
|
-
workerLease: worker.binding.ownerLease,
|
|
2319
|
-
idleMs: input.idleMs,
|
|
2320
|
-
at: now.getTime(),
|
|
2321
|
-
});
|
|
2322
|
-
state.progressWatches.push(watch);
|
|
2323
|
-
this.appendProgressWatchEvent(state, watch, {
|
|
2324
|
-
kind: "opened",
|
|
2325
|
-
timestamp: now.toISOString(),
|
|
2326
|
-
});
|
|
2327
|
-
return { created: true, watch: { ...watch } };
|
|
2477
|
+
const routeWatch = state.progressWatches.find((watch) => watch.capability === "route" &&
|
|
2478
|
+
watch.ownerAlias === input.ownerAlias &&
|
|
2479
|
+
watch.workerAlias === input.workerAlias);
|
|
2480
|
+
if (routeWatch === undefined)
|
|
2481
|
+
return false;
|
|
2482
|
+
const pair = state.pairs.find((candidate) => pairMatchesMessage(candidate, {
|
|
2483
|
+
sourceAlias: input.ownerAlias,
|
|
2484
|
+
targetAlias: input.workerAlias,
|
|
2485
|
+
pair: true,
|
|
2486
|
+
}) && progressWatchMatchesPair(routeWatch, candidate));
|
|
2487
|
+
if (pair === undefined) {
|
|
2488
|
+
throw new BridgeError("PROGRESS_WATCH_EDGE_REQUIRED", "A restarted watch can bind only through its exact durable pair.");
|
|
2489
|
+
}
|
|
2490
|
+
return this.bindRouteProgressWatchConversation(state, now, input.conversationId, input.ownerAlias, input.workerAlias, pair);
|
|
2328
2491
|
});
|
|
2329
2492
|
}
|
|
2330
|
-
async inspectProgressWatches() {
|
|
2331
|
-
return this.mutex.run("gateway", async () => this.requireState().progressWatches.map((watch) => ({ ...watch })));
|
|
2332
|
-
}
|
|
2333
2493
|
async inspectCompatibilityAttestation(surface, version) {
|
|
2334
2494
|
return this.mutex.run("gateway", async () => {
|
|
2335
2495
|
const attestation = this.requireState().compatibilityAttestations.find((candidate) => candidate.surface === surface && candidate.version === version);
|
|
@@ -2362,37 +2522,6 @@ export class GatewayStore {
|
|
|
2362
2522
|
state.compatibilityAttestations.push(structuredClone(attestation));
|
|
2363
2523
|
});
|
|
2364
2524
|
}
|
|
2365
|
-
async touchProgressWatch(input) {
|
|
2366
|
-
return this.mutate(async (state, now) => {
|
|
2367
|
-
const index = state.progressWatches.findIndex((watch) => watch.conversationId === input.conversationId);
|
|
2368
|
-
const watch = state.progressWatches[index];
|
|
2369
|
-
if (watch === undefined)
|
|
2370
|
-
return false;
|
|
2371
|
-
if (input.actorAlias !== watch.ownerAlias &&
|
|
2372
|
-
input.actorAlias !== watch.workerAlias) {
|
|
2373
|
-
throw new BridgeError("PROGRESS_WATCH_OWNERSHIP_MISMATCH", "The watch activity did not come from an endpoint on its exact edge.");
|
|
2374
|
-
}
|
|
2375
|
-
const workerReportedComplete = input.workerReportedComplete === true &&
|
|
2376
|
-
input.actorAlias === watch.workerAlias;
|
|
2377
|
-
const transition = transitionProgressWatch(watch, {
|
|
2378
|
-
type: "activity",
|
|
2379
|
-
at: now.getTime(),
|
|
2380
|
-
...(workerReportedComplete
|
|
2381
|
-
? { workerReportedComplete: true }
|
|
2382
|
-
: {}),
|
|
2383
|
-
});
|
|
2384
|
-
if (transition.state === null)
|
|
2385
|
-
return false;
|
|
2386
|
-
state.progressWatches[index] = transition.state;
|
|
2387
|
-
if (workerReportedComplete) {
|
|
2388
|
-
this.appendProgressWatchEvent(state, transition.state, {
|
|
2389
|
-
kind: "worker_reported_complete",
|
|
2390
|
-
timestamp: now.toISOString(),
|
|
2391
|
-
});
|
|
2392
|
-
}
|
|
2393
|
-
return true;
|
|
2394
|
-
});
|
|
2395
|
-
}
|
|
2396
2525
|
async touchProgressWatchesForAlias(alias) {
|
|
2397
2526
|
return this.mutate(async (state, now) => {
|
|
2398
2527
|
if (!ALIAS_PATTERN.test(alias))
|
|
@@ -2437,6 +2566,7 @@ export class GatewayStore {
|
|
|
2437
2566
|
throw new BridgeError("INVALID_PROGRESS_WATCH_SETTLEMENT", "Unresponsive settlement is owned only by the watch deadline machine.");
|
|
2438
2567
|
}
|
|
2439
2568
|
const transition = transitionProgressWatch(watch, event);
|
|
2569
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
2440
2570
|
state.progressWatches.splice(index, 1);
|
|
2441
2571
|
this.appendProgressWatchEvent(state, watch, {
|
|
2442
2572
|
kind: input.outcome,
|
|
@@ -2448,6 +2578,12 @@ export class GatewayStore {
|
|
|
2448
2578
|
/** Advance every due watch once under one durable store mutation. */
|
|
2449
2579
|
async advanceDueProgressWatches() {
|
|
2450
2580
|
return this.mutate(async (state, now) => {
|
|
2581
|
+
const terminalCount = state.progressWatches.filter((watch) => {
|
|
2582
|
+
if (Date.parse(watch.nextActionAt) > now.getTime())
|
|
2583
|
+
return false;
|
|
2584
|
+
return this.progressWatchDueTransition(state, watch, now).effects.some((effect) => effect.type === "settled");
|
|
2585
|
+
}).length;
|
|
2586
|
+
this.requireProgressWatchJournalCapacity(state, terminalCount);
|
|
2451
2587
|
const actions = [];
|
|
2452
2588
|
const retained = [];
|
|
2453
2589
|
for (const watch of state.progressWatches) {
|
|
@@ -2550,6 +2686,7 @@ export class GatewayStore {
|
|
|
2550
2686
|
return this.mutate(async (state, now) => {
|
|
2551
2687
|
const { claude, codex } = this.requirePairRoutes(state, input, false);
|
|
2552
2688
|
const pair = this.requireExactPair(state, claude, codex);
|
|
2689
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => progressWatchMatchesPair(watch, pair)).length);
|
|
2553
2690
|
const plan = this.validateAffectedPairInFlightSettlements(state, pair, input.inFlightSettlements ?? []);
|
|
2554
2691
|
const settlements = this.terminateAffectedPairMessages(state, pair, now, "PAIR_REMOVED", plan);
|
|
2555
2692
|
this.settleProgressWatchesForPair(state, pair, now);
|
|
@@ -2735,7 +2872,6 @@ export class GatewayStore {
|
|
|
2735
2872
|
state: "dispatching",
|
|
2736
2873
|
bytes: metadata.bytes,
|
|
2737
2874
|
body,
|
|
2738
|
-
hopCount: metadata.hopCount,
|
|
2739
2875
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2740
2876
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
2741
2877
|
});
|
|
@@ -2864,7 +3000,6 @@ export class GatewayStore {
|
|
|
2864
3000
|
state: "held",
|
|
2865
3001
|
bytes: metadata.bytes,
|
|
2866
3002
|
body,
|
|
2867
|
-
hopCount: metadata.hopCount,
|
|
2868
3003
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2869
3004
|
...(safeErrorCode === undefined ? {} : { safeErrorCode }),
|
|
2870
3005
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
@@ -2896,7 +3031,6 @@ export class GatewayStore {
|
|
|
2896
3031
|
state: progress,
|
|
2897
3032
|
bytes: metadata.bytes,
|
|
2898
3033
|
...(metadata.body === undefined ? {} : { body: metadata.body }),
|
|
2899
|
-
hopCount: metadata.hopCount,
|
|
2900
3034
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2901
3035
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
2902
3036
|
});
|
|
@@ -3014,7 +3148,6 @@ export class GatewayStore {
|
|
|
3014
3148
|
nextActionAt: watch.nextActionAt,
|
|
3015
3149
|
idleMs: watch.idleMs,
|
|
3016
3150
|
nudgeCount: watch.nudgeCount,
|
|
3017
|
-
workerReportedComplete: watch.workerReportedCompleteAt !== undefined,
|
|
3018
3151
|
}))
|
|
3019
3152
|
.sort((left, right) => `${left.nextActionAt}\0${left.ownerAlias}\0${left.workerAlias}`.localeCompare(`${right.nextActionAt}\0${right.ownerAlias}\0${right.workerAlias}`));
|
|
3020
3153
|
const progressWatchEvents = state.progressWatchEvents
|
|
@@ -3248,6 +3381,9 @@ export class GatewayStore {
|
|
|
3248
3381
|
}
|
|
3249
3382
|
replaceCodexRouteForSuccession(state, route, identity, now, routeState, compatibility) {
|
|
3250
3383
|
const oldAlias = route.alias;
|
|
3384
|
+
const affectedAliases = new Set([oldAlias, identity.alias]);
|
|
3385
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => affectedAliases.has(watch.ownerAlias) ||
|
|
3386
|
+
affectedAliases.has(watch.workerAlias)).length);
|
|
3251
3387
|
route.alias = identity.alias;
|
|
3252
3388
|
route.binding = { ...identity.binding };
|
|
3253
3389
|
route.registrationMode = "explicit_opt_in";
|
|
@@ -3272,8 +3408,8 @@ export class GatewayStore {
|
|
|
3272
3408
|
record.targetAlias !== identity.alias);
|
|
3273
3409
|
state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== oldAlias &&
|
|
3274
3410
|
bucket.sourceAlias !== identity.alias);
|
|
3275
|
-
this.settleProgressWatchesForAliases(state,
|
|
3276
|
-
removePairsForAliases(state,
|
|
3411
|
+
this.settleProgressWatchesForAliases(state, affectedAliases, now);
|
|
3412
|
+
removePairsForAliases(state, affectedAliases);
|
|
3277
3413
|
}
|
|
3278
3414
|
assertAllowedIdentity(identity) {
|
|
3279
3415
|
if (!isPrivateEndpointIdentity(identity)) {
|
|
@@ -3443,12 +3579,6 @@ export class GatewayStore {
|
|
|
3443
3579
|
this.recordRejection(state, sides, bytes, now, "MESSAGE_TOO_LARGE", input.steer, input.conversationIdSuffix);
|
|
3444
3580
|
throw new BridgeError("MESSAGE_TOO_LARGE", "The transient message exceeds the configured byte limit.");
|
|
3445
3581
|
}
|
|
3446
|
-
const hopCount = input.hopCount ?? 0;
|
|
3447
|
-
if (!isNonNegativeInteger(hopCount) ||
|
|
3448
|
-
hopCount > this.config.limits.maxHopCount) {
|
|
3449
|
-
this.recordRejection(state, sides, bytes, now, "HOP_LIMIT_EXCEEDED", input.steer, input.conversationIdSuffix);
|
|
3450
|
-
throw new BridgeError("HOP_LIMIT_EXCEEDED", "The message exceeds the configured gateway hop limit.");
|
|
3451
|
-
}
|
|
3452
3582
|
if (typeof input.dedupeKey !== "string" ||
|
|
3453
3583
|
input.dedupeKey.length === 0 ||
|
|
3454
3584
|
Buffer.byteLength(input.dedupeKey) > 512) {
|
|
@@ -3476,7 +3606,6 @@ export class GatewayStore {
|
|
|
3476
3606
|
? {}
|
|
3477
3607
|
: { conversationIdSuffix: duplicate.conversationIdSuffix }),
|
|
3478
3608
|
bytes,
|
|
3479
|
-
hopCount,
|
|
3480
3609
|
...(input.steer === true ? { steer: true } : {}),
|
|
3481
3610
|
});
|
|
3482
3611
|
return {
|
|
@@ -3566,7 +3695,6 @@ export class GatewayStore {
|
|
|
3566
3695
|
deadlineAt: deadline.toISOString(),
|
|
3567
3696
|
bytes,
|
|
3568
3697
|
body: input.body,
|
|
3569
|
-
hopCount,
|
|
3570
3698
|
...(sides.pair === true ? { pair: true } : {}),
|
|
3571
3699
|
...(sides.transientTarget === true
|
|
3572
3700
|
? { transientTarget: true }
|
|
@@ -3618,7 +3746,6 @@ export class GatewayStore {
|
|
|
3618
3746
|
state: "queued",
|
|
3619
3747
|
bytes,
|
|
3620
3748
|
body: input.body,
|
|
3621
|
-
hopCount,
|
|
3622
3749
|
...(input.steer === true ? { steer: true } : {}),
|
|
3623
3750
|
});
|
|
3624
3751
|
return {
|
|
@@ -3684,7 +3811,6 @@ export class GatewayStore {
|
|
|
3684
3811
|
targetAlias: sides.targetAlias,
|
|
3685
3812
|
state: "rejected",
|
|
3686
3813
|
bytes: Math.max(1, bytes),
|
|
3687
|
-
hopCount: 0,
|
|
3688
3814
|
safeErrorCode,
|
|
3689
3815
|
...(steer === true ? { steer: true } : {}),
|
|
3690
3816
|
});
|
|
@@ -3772,43 +3898,62 @@ export class GatewayStore {
|
|
|
3772
3898
|
activity.openIdleMs !== undefined)) {
|
|
3773
3899
|
throw new BridgeError("INVALID_PROGRESS_WATCH", "The message watch activity is malformed or contradictory.");
|
|
3774
3900
|
}
|
|
3775
|
-
const
|
|
3901
|
+
const pair = sides.pair === true
|
|
3902
|
+
? state.pairs.find((candidate) => pairMatchesMessage(candidate, sides))
|
|
3903
|
+
: undefined;
|
|
3904
|
+
let index = state.progressWatches.findIndex((watch) => watch.conversationId === activity.conversationId);
|
|
3905
|
+
if (index < 0 &&
|
|
3906
|
+
activity.openIdleMs === undefined &&
|
|
3907
|
+
activity.completionSignal !== true &&
|
|
3908
|
+
pair !== undefined) {
|
|
3909
|
+
const routeMatches = state.progressWatches
|
|
3910
|
+
.map((watch, watchIndex) => ({ watch, watchIndex }))
|
|
3911
|
+
.filter(({ watch }) => watch.capability === "route" &&
|
|
3912
|
+
watch.ownerAlias === sides.sourceAlias &&
|
|
3913
|
+
watch.workerAlias === sides.targetAlias &&
|
|
3914
|
+
progressWatchMatchesPair(watch, pair));
|
|
3915
|
+
if (routeMatches.length === 1) {
|
|
3916
|
+
this.bindRouteProgressWatchConversation(state, now, activity.conversationId, sides.sourceAlias, sides.targetAlias, pair);
|
|
3917
|
+
index = routeMatches[0].watchIndex;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3776
3920
|
const existing = state.progressWatches[index];
|
|
3777
3921
|
if (existing !== undefined) {
|
|
3778
|
-
if (
|
|
3779
|
-
|
|
3922
|
+
if (pair === undefined ||
|
|
3923
|
+
!progressWatchMatchesPair(existing, pair) ||
|
|
3924
|
+
!((sides.sourceAlias === existing.ownerAlias &&
|
|
3925
|
+
sides.targetAlias === existing.workerAlias) ||
|
|
3926
|
+
(sides.sourceAlias === existing.workerAlias &&
|
|
3927
|
+
sides.targetAlias === existing.ownerAlias))) {
|
|
3780
3928
|
throw new BridgeError("PROGRESS_WATCH_OWNERSHIP_MISMATCH", "The watched conversation belongs to different exact endpoints.");
|
|
3781
3929
|
}
|
|
3782
3930
|
if (activity.openIdleMs !== undefined &&
|
|
3783
3931
|
activity.actorAlias !== existing.ownerAlias) {
|
|
3784
3932
|
throw new BridgeError("PROGRESS_WATCH_OWNER_REQUIRED", "Only the exact watch owner may reassert tracking options.");
|
|
3785
3933
|
}
|
|
3786
|
-
if (activity.completionSignal === true
|
|
3787
|
-
activity.actorAlias === existing.ownerAlias
|
|
3934
|
+
if (activity.completionSignal === true) {
|
|
3935
|
+
const transition = transitionProgressWatch(existing, activity.actorAlias === existing.ownerAlias
|
|
3936
|
+
? { type: "owner_done", at: now.getTime() }
|
|
3937
|
+
: { type: "worker_done", at: now.getTime() });
|
|
3938
|
+
if (transition.state !== null) {
|
|
3939
|
+
throw new BridgeError("INVALID_PROGRESS_WATCH_SETTLEMENT", "The completion signal did not settle its exact watch.");
|
|
3940
|
+
}
|
|
3941
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
3788
3942
|
state.progressWatches.splice(index, 1);
|
|
3789
3943
|
this.appendProgressWatchEvent(state, existing, {
|
|
3790
|
-
kind:
|
|
3944
|
+
kind: activity.actorAlias === existing.ownerAlias
|
|
3945
|
+
? "done"
|
|
3946
|
+
: "worker_reported_complete",
|
|
3791
3947
|
timestamp: now.toISOString(),
|
|
3792
3948
|
});
|
|
3793
3949
|
return;
|
|
3794
3950
|
}
|
|
3795
|
-
const workerReportedComplete = activity.completionSignal === true &&
|
|
3796
|
-
activity.actorAlias === existing.workerAlias;
|
|
3797
3951
|
const transition = transitionProgressWatch(existing, {
|
|
3798
3952
|
type: "activity",
|
|
3799
3953
|
at: now.getTime(),
|
|
3800
|
-
...(workerReportedComplete
|
|
3801
|
-
? { workerReportedComplete: true }
|
|
3802
|
-
: {}),
|
|
3803
3954
|
});
|
|
3804
3955
|
if (transition.state !== null) {
|
|
3805
3956
|
state.progressWatches[index] = transition.state;
|
|
3806
|
-
if (workerReportedComplete) {
|
|
3807
|
-
this.appendProgressWatchEvent(state, transition.state, {
|
|
3808
|
-
kind: "worker_reported_complete",
|
|
3809
|
-
timestamp: now.toISOString(),
|
|
3810
|
-
});
|
|
3811
|
-
}
|
|
3812
3957
|
}
|
|
3813
3958
|
return;
|
|
3814
3959
|
}
|
|
@@ -3817,13 +3962,9 @@ export class GatewayStore {
|
|
|
3817
3962
|
if (this.config.trackingEnabled === false) {
|
|
3818
3963
|
throw new BridgeError("PROGRESS_TRACKING_DISABLED", "Progress tracking is disabled by the controller configuration.");
|
|
3819
3964
|
}
|
|
3820
|
-
if (
|
|
3965
|
+
if (pair === undefined) {
|
|
3821
3966
|
throw new BridgeError("PROGRESS_WATCH_EDGE_REQUIRED", "Progress tracking requires one exact paired consent edge.");
|
|
3822
3967
|
}
|
|
3823
|
-
if (state.progressWatches.length >=
|
|
3824
|
-
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY)) {
|
|
3825
|
-
throw new BridgeError("PROGRESS_WATCH_CAPACITY_REACHED", "The bounded progress-watch inventory is full.", true);
|
|
3826
|
-
}
|
|
3827
3968
|
const owner = state.routes.find((route) => route.alias === sides.sourceAlias);
|
|
3828
3969
|
const worker = state.routes.find((route) => route.alias === sides.targetAlias);
|
|
3829
3970
|
if (owner === undefined || worker === undefined) {
|
|
@@ -3838,11 +3979,10 @@ export class GatewayStore {
|
|
|
3838
3979
|
idleMs: activity.openIdleMs,
|
|
3839
3980
|
at: now.getTime(),
|
|
3840
3981
|
});
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
});
|
|
3982
|
+
if (!progressWatchMatchesPair(watch, pair)) {
|
|
3983
|
+
throw new BridgeError("PROGRESS_WATCH_EDGE_MISMATCH", "The watch endpoints do not form one exact consent edge.");
|
|
3984
|
+
}
|
|
3985
|
+
this.installProgressWatch(state, watch, pair, now);
|
|
3846
3986
|
}
|
|
3847
3987
|
progressWatchDueTransition(state, watch, now) {
|
|
3848
3988
|
const owner = state.routes.find((route) => route.alias === watch.ownerAlias &&
|
|
@@ -3886,6 +4026,7 @@ export class GatewayStore {
|
|
|
3886
4026
|
effect.nudgeNumber === nudge.nudgeNumber)) {
|
|
3887
4027
|
throw new BridgeError("PROGRESS_WATCH_NUDGE_NOT_DUE", "The requested progress-watch nudge is not currently due.", true);
|
|
3888
4028
|
}
|
|
4029
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
3889
4030
|
state.progressWatches[index] = transition.state;
|
|
3890
4031
|
this.appendProgressWatchEvent(state, transition.state, {
|
|
3891
4032
|
kind: "nudge",
|
|
@@ -3893,7 +4034,41 @@ export class GatewayStore {
|
|
|
3893
4034
|
nudgeNumber: nudge.nudgeNumber,
|
|
3894
4035
|
});
|
|
3895
4036
|
}
|
|
4037
|
+
installProgressWatch(state, watch, pair, now) {
|
|
4038
|
+
const replacedIndex = state.progressWatches.findIndex((candidate) => progressWatchMatchesPair(candidate, pair));
|
|
4039
|
+
if (replacedIndex < 0 &&
|
|
4040
|
+
state.progressWatches.length >=
|
|
4041
|
+
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY)) {
|
|
4042
|
+
throw new BridgeError("PROGRESS_WATCH_CAPACITY_REACHED", "The bounded progress-watch inventory is full.", true);
|
|
4043
|
+
}
|
|
4044
|
+
if (replacedIndex >= 0 &&
|
|
4045
|
+
state.progressWatches[replacedIndex].ownerAlias !== watch.ownerAlias) {
|
|
4046
|
+
throw new BridgeError("PROGRESS_WATCH_REPLACEMENT_OWNER_REQUIRED", "Only the incumbent watch owner may replace this pair watch; ask that owner to untrack it first.");
|
|
4047
|
+
}
|
|
4048
|
+
this.requireProgressWatchJournalCapacity(state, replacedIndex < 0 ? 1 : 2);
|
|
4049
|
+
if (replacedIndex >= 0) {
|
|
4050
|
+
const replaced = state.progressWatches[replacedIndex];
|
|
4051
|
+
state.progressWatches.splice(replacedIndex, 1);
|
|
4052
|
+
this.appendProgressWatchEvent(state, replaced, {
|
|
4053
|
+
kind: "replaced",
|
|
4054
|
+
timestamp: now.toISOString(),
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
state.progressWatches.push(watch);
|
|
4058
|
+
this.appendProgressWatchEvent(state, watch, {
|
|
4059
|
+
kind: "opened",
|
|
4060
|
+
timestamp: now.toISOString(),
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
requireProgressWatchJournalCapacity(state, additions) {
|
|
4064
|
+
if (!Number.isSafeInteger(additions) ||
|
|
4065
|
+
additions < 0 ||
|
|
4066
|
+
state.watchSequence > Number.MAX_SAFE_INTEGER - additions) {
|
|
4067
|
+
throw new BridgeError("PROGRESS_WATCH_SEQUENCE_EXHAUSTED", "The bounded progress-watch journal sequence is exhausted.");
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
3896
4070
|
appendProgressWatchEvent(state, watch, event) {
|
|
4071
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
3897
4072
|
state.watchSequence += 1;
|
|
3898
4073
|
state.progressWatchEvents.push({
|
|
3899
4074
|
sequence: state.watchSequence,
|
|
@@ -3911,6 +4086,7 @@ export class GatewayStore {
|
|
|
3911
4086
|
}
|
|
3912
4087
|
}
|
|
3913
4088
|
settleProgressWatchesForAliases(state, aliases, now) {
|
|
4089
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => aliases.has(watch.ownerAlias) || aliases.has(watch.workerAlias)).length);
|
|
3914
4090
|
const retained = [];
|
|
3915
4091
|
for (const watch of state.progressWatches) {
|
|
3916
4092
|
if (!aliases.has(watch.ownerAlias) &&
|
|
@@ -3926,23 +4102,61 @@ export class GatewayStore {
|
|
|
3926
4102
|
state.progressWatches = retained;
|
|
3927
4103
|
}
|
|
3928
4104
|
settleProgressWatchesForPair(state, pair, now) {
|
|
4105
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => progressWatchMatchesPair(watch, pair)).length);
|
|
3929
4106
|
const retained = [];
|
|
3930
4107
|
for (const watch of state.progressWatches) {
|
|
3931
|
-
|
|
3932
|
-
watch.workerAlias === pair.codexAlias) ||
|
|
3933
|
-
(watch.ownerAlias === pair.codexAlias &&
|
|
3934
|
-
watch.workerAlias === pair.claudeAlias);
|
|
3935
|
-
if (!matches) {
|
|
4108
|
+
if (!progressWatchMatchesPair(watch, pair)) {
|
|
3936
4109
|
retained.push(watch);
|
|
3937
4110
|
continue;
|
|
3938
4111
|
}
|
|
3939
4112
|
this.appendProgressWatchEvent(state, watch, {
|
|
3940
|
-
kind: "
|
|
4113
|
+
kind: "pair_removed",
|
|
3941
4114
|
timestamp: now.toISOString(),
|
|
3942
4115
|
});
|
|
3943
4116
|
}
|
|
3944
4117
|
state.progressWatches = retained;
|
|
3945
4118
|
}
|
|
4119
|
+
bindRouteProgressWatchConversation(state, now, conversationId, ownerAlias, workerAlias, pair) {
|
|
4120
|
+
const conversationOwner = state.progressWatches.find((watch) => watch.conversationId === conversationId);
|
|
4121
|
+
if (conversationOwner !== undefined &&
|
|
4122
|
+
(conversationOwner.capability !== "route" ||
|
|
4123
|
+
conversationOwner.ownerAlias !== ownerAlias ||
|
|
4124
|
+
conversationOwner.workerAlias !== workerAlias ||
|
|
4125
|
+
!progressWatchMatchesPair(conversationOwner, pair))) {
|
|
4126
|
+
throw new BridgeError("PROGRESS_WATCH_OWNERSHIP_MISMATCH", "The fresh conversation capability belongs to another exact watch.");
|
|
4127
|
+
}
|
|
4128
|
+
const matches = state.progressWatches
|
|
4129
|
+
.map((watch, index) => ({ watch, index }))
|
|
4130
|
+
.filter(({ watch }) => watch.capability === "route" &&
|
|
4131
|
+
watch.ownerAlias === ownerAlias &&
|
|
4132
|
+
watch.workerAlias === workerAlias &&
|
|
4133
|
+
progressWatchMatchesPair(watch, pair));
|
|
4134
|
+
if (matches.length === 0)
|
|
4135
|
+
return false;
|
|
4136
|
+
if (matches.length !== 1) {
|
|
4137
|
+
throw new BridgeError("CORRUPT_GATEWAY_STATE", "More than one route-capability watch claims the exact pair.");
|
|
4138
|
+
}
|
|
4139
|
+
const match = matches[0];
|
|
4140
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
4141
|
+
const restored = transitionProgressWatch(match.watch, {
|
|
4142
|
+
type: "restart",
|
|
4143
|
+
at: now.getTime(),
|
|
4144
|
+
conversationCapabilityRestored: true,
|
|
4145
|
+
});
|
|
4146
|
+
if (restored.state === null) {
|
|
4147
|
+
throw new BridgeError("INVALID_PROGRESS_WATCH_REBIND", "The exact progress watch could not restore conversation authority.");
|
|
4148
|
+
}
|
|
4149
|
+
const rebound = {
|
|
4150
|
+
...restored.state,
|
|
4151
|
+
conversationId,
|
|
4152
|
+
};
|
|
4153
|
+
state.progressWatches[match.index] = rebound;
|
|
4154
|
+
this.appendProgressWatchEvent(state, rebound, {
|
|
4155
|
+
kind: "conversation_rebound",
|
|
4156
|
+
timestamp: now.toISOString(),
|
|
4157
|
+
});
|
|
4158
|
+
return true;
|
|
4159
|
+
}
|
|
3946
4160
|
finishMetadata(state, metadata, deliveryState, now, safeErrorCode) {
|
|
3947
4161
|
const counterKey = deliveryState;
|
|
3948
4162
|
state.accounting[counterKey] += 1;
|
|
@@ -3966,7 +4180,6 @@ export class GatewayStore {
|
|
|
3966
4180
|
state: deliveryState,
|
|
3967
4181
|
bytes: metadata.bytes,
|
|
3968
4182
|
...(metadata.body === undefined ? {} : { body: metadata.body }),
|
|
3969
|
-
hopCount: metadata.hopCount,
|
|
3970
4183
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
3971
4184
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
3972
4185
|
...(safeErrorCode ? { safeErrorCode } : {}),
|
|
@@ -4109,6 +4322,7 @@ export class GatewayStore {
|
|
|
4109
4322
|
const state = this.requireState();
|
|
4110
4323
|
this.recoverCodexSuccessionAfterRestart(state, now);
|
|
4111
4324
|
if (this.config.trackingEnabled === false) {
|
|
4325
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.length);
|
|
4112
4326
|
for (const watch of state.progressWatches) {
|
|
4113
4327
|
this.appendProgressWatchEvent(state, watch, {
|
|
4114
4328
|
kind: "disabled",
|
|
@@ -4117,6 +4331,9 @@ export class GatewayStore {
|
|
|
4117
4331
|
}
|
|
4118
4332
|
state.progressWatches = [];
|
|
4119
4333
|
}
|
|
4334
|
+
else {
|
|
4335
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => !watch.degradedNoticeSent).length);
|
|
4336
|
+
}
|
|
4120
4337
|
for (let index = 0; index < state.progressWatches.length; index += 1) {
|
|
4121
4338
|
const watch = state.progressWatches[index];
|
|
4122
4339
|
if (watch === undefined)
|
|
@@ -4129,7 +4346,7 @@ export class GatewayStore {
|
|
|
4129
4346
|
if (transition.state !== null) {
|
|
4130
4347
|
state.progressWatches[index] = transition.state;
|
|
4131
4348
|
}
|
|
4132
|
-
if (transition.effects.some((effect) => effect.type === "
|
|
4349
|
+
if (transition.effects.some((effect) => effect.type === "record_capability_degraded")) {
|
|
4133
4350
|
this.appendProgressWatchEvent(state, transition.state ?? watch, {
|
|
4134
4351
|
kind: "capability_degraded",
|
|
4135
4352
|
timestamp: now.toISOString(),
|
|
@@ -4444,6 +4661,9 @@ export class GatewayStore {
|
|
|
4444
4661
|
parsed = migratePreCompatibilityAttestations(parsed);
|
|
4445
4662
|
parsed = migratePreCodexEndpointRefreshJournal(parsed);
|
|
4446
4663
|
parsed = migratePreCodexOrphanRemovalJournal(parsed);
|
|
4664
|
+
parsed = migrateLegacyHopCounts(parsed);
|
|
4665
|
+
parsed = migrateLegacyCompatibilityCertifications(parsed);
|
|
4666
|
+
parsed = migrateLegacyProgressWatchState(parsed, this.config.limits.eventCapacity);
|
|
4447
4667
|
if (!isPersistedState(parsed)) {
|
|
4448
4668
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
|
|
4449
4669
|
}
|