agent-embassy 1.3.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 +17 -0
- package/CONTRIBUTING.md +25 -9
- package/README.md +39 -15
- package/README.zh-CN.md +34 -13
- package/SECURITY.md +13 -7
- 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 -0
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/cli-copy.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +3 -2
- package/dist/src/gateway/cli-copy.en.js +3 -1
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +1 -0
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +3 -2
- package/dist/src/gateway/cli-copy.zh-CN.js +3 -1
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +1 -1
- package/dist/src/gateway/cli.js +12 -1
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +1 -1
- package/dist/src/gateway/codex-app-server.js +32 -11
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/config.d.ts +1 -1
- package/dist/src/gateway/control.d.ts +1 -1
- package/dist/src/gateway/control.js +5 -3
- 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 +15 -1
- package/dist/src/gateway/dashboard-copy.en.js +25 -11
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +15 -1
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +15 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.js +25 -11
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +2 -3
- package/dist/src/gateway/dashboard-model.js +118 -30
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.js +22 -9
- 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 +45 -18
- package/dist/src/gateway/live-dashboard-http.js +1 -1
- package/dist/src/gateway/live-dashboard-http.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 +10 -3
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +1 -0
- package/dist/src/gateway/providers.js +6 -0
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/service.d.ts +2 -0
- package/dist/src/gateway/service.js +25 -8
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +9 -21
- package/dist/src/gateway/store.js +284 -122
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +1 -2
- 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 +177 -93
- package/package.json +8 -4
|
@@ -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, [
|
|
@@ -787,6 +809,94 @@ function migrateLegacyCompatibilityCertifications(value) {
|
|
|
787
809
|
});
|
|
788
810
|
return changed ? { ...value, compatibilityAttestations } : value;
|
|
789
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
|
+
}
|
|
790
900
|
function isPersistedState(value) {
|
|
791
901
|
if (!isObject(value) ||
|
|
792
902
|
!hasOnlyKeys(value, [
|
|
@@ -872,6 +982,9 @@ function isPersistedState(value) {
|
|
|
872
982
|
if (watchConversationIds.size !== candidate.progressWatches.length) {
|
|
873
983
|
return false;
|
|
874
984
|
}
|
|
985
|
+
const watchPairKeys = new Set(candidate.progressWatches.map(progressWatchPairKey));
|
|
986
|
+
if (watchPairKeys.size !== candidate.progressWatches.length)
|
|
987
|
+
return false;
|
|
875
988
|
const compatibilityKeys = candidate.compatibilityAttestations.map(compatibilityCacheKey);
|
|
876
989
|
if (new Set(compatibilityKeys).size !== compatibilityKeys.length) {
|
|
877
990
|
return false;
|
|
@@ -944,6 +1057,10 @@ function isPersistedState(value) {
|
|
|
944
1057
|
};
|
|
945
1058
|
const sequencesStrictlyIncrease = candidate.events.every((event, index) => index === 0 ||
|
|
946
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)));
|
|
947
1064
|
const endpointRefreshSequenceConsistent = candidate.codexEndpointRefreshEvents.length === 0
|
|
948
1065
|
? candidate.codexEndpointRefreshSequence === 0
|
|
949
1066
|
: candidate.codexEndpointRefreshEvents.at(-1)?.sequence ===
|
|
@@ -974,6 +1091,7 @@ function isPersistedState(value) {
|
|
|
974
1091
|
candidate.rateBuckets.length &&
|
|
975
1092
|
candidate.accounting.queuedBytes === expectedQueuedBytes &&
|
|
976
1093
|
sequencesStrictlyIncrease &&
|
|
1094
|
+
progressWatchSequencesStrictlyIncrease &&
|
|
977
1095
|
candidate.events.every((event) => event.sequence <= candidate.eventSequence) &&
|
|
978
1096
|
endpointRefreshSequenceConsistent &&
|
|
979
1097
|
orphanRemovalSequenceConsistent &&
|
|
@@ -1510,6 +1628,8 @@ export class GatewayStore {
|
|
|
1510
1628
|
// selected Claude routes and their independent consent edges survive.
|
|
1511
1629
|
const retired = selectedClaudeRoutes.filter((route) => route !== retained && route === aliasOwner);
|
|
1512
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);
|
|
1513
1633
|
const settlementPlan = this.validateAffectedInFlightSettlements(state, retiredAliases, input.inFlightSettlements ?? []);
|
|
1514
1634
|
const settlements = this.terminateAffectedMessages(state, retiredAliases, now, "ROUTE_UNREGISTERED", settlementPlan);
|
|
1515
1635
|
this.settleProgressWatchesForAliases(state, retiredAliases, now);
|
|
@@ -1886,6 +2006,7 @@ export class GatewayStore {
|
|
|
1886
2006
|
return await this.mutate(async (state, now) => {
|
|
1887
2007
|
const route = this.requireOwnedRoute(state, alias, ownerLease);
|
|
1888
2008
|
const aliases = new Set([route.alias]);
|
|
2009
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => aliases.has(watch.ownerAlias) || aliases.has(watch.workerAlias)).length);
|
|
1889
2010
|
const settlementPlan = this.validateAffectedInFlightSettlements(state, aliases, inFlightSettlements);
|
|
1890
2011
|
route.enabled = false;
|
|
1891
2012
|
route.state = "disabled";
|
|
@@ -2341,65 +2462,34 @@ export class GatewayStore {
|
|
|
2341
2462
|
.sort((left, right) => pairKey(left.claudeAlias, left.codexAlias).localeCompare(pairKey(right.claudeAlias, right.codexAlias)));
|
|
2342
2463
|
});
|
|
2343
2464
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
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) {
|
|
2346
2470
|
return this.mutate(async (state, now) => {
|
|
2347
|
-
if (this.config.trackingEnabled === false) {
|
|
2348
|
-
throw new BridgeError("PROGRESS_TRACKING_DISABLED", "Progress tracking is disabled by the controller configuration.");
|
|
2349
|
-
}
|
|
2350
2471
|
if (!CONVERSATION_ID_PATTERN.test(input.conversationId) ||
|
|
2351
2472
|
!ALIAS_PATTERN.test(input.ownerAlias) ||
|
|
2352
2473
|
!ALIAS_PATTERN.test(input.workerAlias) ||
|
|
2353
|
-
input.ownerAlias === input.workerAlias
|
|
2354
|
-
|
|
2355
|
-
input.idleMs < PROGRESS_WATCH_MIN_IDLE_MS ||
|
|
2356
|
-
input.idleMs > PROGRESS_WATCH_MAX_IDLE_MS) {
|
|
2357
|
-
throw new BridgeError("INVALID_PROGRESS_WATCH", "The progress-watch request is malformed or outside its idle bound.");
|
|
2474
|
+
input.ownerAlias === input.workerAlias) {
|
|
2475
|
+
throw new BridgeError("INVALID_PROGRESS_WATCH", "The progress-watch conversation binding is malformed.");
|
|
2358
2476
|
}
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
const pairAliases = pairAliasesForRoutes(owner, worker);
|
|
2374
|
-
this.requireExactPair(state, owner.binding.provider === "claude" ? owner : worker, owner.binding.provider === "codex" ? owner : worker);
|
|
2375
|
-
if (pairAliases.claudeAlias !==
|
|
2376
|
-
(owner.binding.provider === "claude"
|
|
2377
|
-
? owner.alias
|
|
2378
|
-
: worker.alias) ||
|
|
2379
|
-
pairAliases.codexAlias !==
|
|
2380
|
-
(owner.binding.provider === "codex" ? owner.alias : worker.alias)) {
|
|
2381
|
-
throw new BridgeError("PROGRESS_WATCH_EDGE_MISMATCH", "The watch endpoints do not form one exact consent edge.");
|
|
2382
|
-
}
|
|
2383
|
-
const watch = createProgressWatchMachine({
|
|
2384
|
-
conversationId: input.conversationId,
|
|
2385
|
-
ownerAlias: owner.alias,
|
|
2386
|
-
workerAlias: worker.alias,
|
|
2387
|
-
ownerLease: owner.binding.ownerLease,
|
|
2388
|
-
workerLease: worker.binding.ownerLease,
|
|
2389
|
-
idleMs: input.idleMs,
|
|
2390
|
-
at: now.getTime(),
|
|
2391
|
-
});
|
|
2392
|
-
state.progressWatches.push(watch);
|
|
2393
|
-
this.appendProgressWatchEvent(state, watch, {
|
|
2394
|
-
kind: "opened",
|
|
2395
|
-
timestamp: now.toISOString(),
|
|
2396
|
-
});
|
|
2397
|
-
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);
|
|
2398
2491
|
});
|
|
2399
2492
|
}
|
|
2400
|
-
async inspectProgressWatches() {
|
|
2401
|
-
return this.mutex.run("gateway", async () => this.requireState().progressWatches.map((watch) => ({ ...watch })));
|
|
2402
|
-
}
|
|
2403
2493
|
async inspectCompatibilityAttestation(surface, version) {
|
|
2404
2494
|
return this.mutex.run("gateway", async () => {
|
|
2405
2495
|
const attestation = this.requireState().compatibilityAttestations.find((candidate) => candidate.surface === surface && candidate.version === version);
|
|
@@ -2432,37 +2522,6 @@ export class GatewayStore {
|
|
|
2432
2522
|
state.compatibilityAttestations.push(structuredClone(attestation));
|
|
2433
2523
|
});
|
|
2434
2524
|
}
|
|
2435
|
-
async touchProgressWatch(input) {
|
|
2436
|
-
return this.mutate(async (state, now) => {
|
|
2437
|
-
const index = state.progressWatches.findIndex((watch) => watch.conversationId === input.conversationId);
|
|
2438
|
-
const watch = state.progressWatches[index];
|
|
2439
|
-
if (watch === undefined)
|
|
2440
|
-
return false;
|
|
2441
|
-
if (input.actorAlias !== watch.ownerAlias &&
|
|
2442
|
-
input.actorAlias !== watch.workerAlias) {
|
|
2443
|
-
throw new BridgeError("PROGRESS_WATCH_OWNERSHIP_MISMATCH", "The watch activity did not come from an endpoint on its exact edge.");
|
|
2444
|
-
}
|
|
2445
|
-
const workerReportedComplete = input.workerReportedComplete === true &&
|
|
2446
|
-
input.actorAlias === watch.workerAlias;
|
|
2447
|
-
const transition = transitionProgressWatch(watch, {
|
|
2448
|
-
type: "activity",
|
|
2449
|
-
at: now.getTime(),
|
|
2450
|
-
...(workerReportedComplete
|
|
2451
|
-
? { workerReportedComplete: true }
|
|
2452
|
-
: {}),
|
|
2453
|
-
});
|
|
2454
|
-
if (transition.state === null)
|
|
2455
|
-
return false;
|
|
2456
|
-
state.progressWatches[index] = transition.state;
|
|
2457
|
-
if (workerReportedComplete) {
|
|
2458
|
-
this.appendProgressWatchEvent(state, transition.state, {
|
|
2459
|
-
kind: "worker_reported_complete",
|
|
2460
|
-
timestamp: now.toISOString(),
|
|
2461
|
-
});
|
|
2462
|
-
}
|
|
2463
|
-
return true;
|
|
2464
|
-
});
|
|
2465
|
-
}
|
|
2466
2525
|
async touchProgressWatchesForAlias(alias) {
|
|
2467
2526
|
return this.mutate(async (state, now) => {
|
|
2468
2527
|
if (!ALIAS_PATTERN.test(alias))
|
|
@@ -2507,6 +2566,7 @@ export class GatewayStore {
|
|
|
2507
2566
|
throw new BridgeError("INVALID_PROGRESS_WATCH_SETTLEMENT", "Unresponsive settlement is owned only by the watch deadline machine.");
|
|
2508
2567
|
}
|
|
2509
2568
|
const transition = transitionProgressWatch(watch, event);
|
|
2569
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
2510
2570
|
state.progressWatches.splice(index, 1);
|
|
2511
2571
|
this.appendProgressWatchEvent(state, watch, {
|
|
2512
2572
|
kind: input.outcome,
|
|
@@ -2518,6 +2578,12 @@ export class GatewayStore {
|
|
|
2518
2578
|
/** Advance every due watch once under one durable store mutation. */
|
|
2519
2579
|
async advanceDueProgressWatches() {
|
|
2520
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);
|
|
2521
2587
|
const actions = [];
|
|
2522
2588
|
const retained = [];
|
|
2523
2589
|
for (const watch of state.progressWatches) {
|
|
@@ -2620,6 +2686,7 @@ export class GatewayStore {
|
|
|
2620
2686
|
return this.mutate(async (state, now) => {
|
|
2621
2687
|
const { claude, codex } = this.requirePairRoutes(state, input, false);
|
|
2622
2688
|
const pair = this.requireExactPair(state, claude, codex);
|
|
2689
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => progressWatchMatchesPair(watch, pair)).length);
|
|
2623
2690
|
const plan = this.validateAffectedPairInFlightSettlements(state, pair, input.inFlightSettlements ?? []);
|
|
2624
2691
|
const settlements = this.terminateAffectedPairMessages(state, pair, now, "PAIR_REMOVED", plan);
|
|
2625
2692
|
this.settleProgressWatchesForPair(state, pair, now);
|
|
@@ -3081,7 +3148,6 @@ export class GatewayStore {
|
|
|
3081
3148
|
nextActionAt: watch.nextActionAt,
|
|
3082
3149
|
idleMs: watch.idleMs,
|
|
3083
3150
|
nudgeCount: watch.nudgeCount,
|
|
3084
|
-
workerReportedComplete: watch.workerReportedCompleteAt !== undefined,
|
|
3085
3151
|
}))
|
|
3086
3152
|
.sort((left, right) => `${left.nextActionAt}\0${left.ownerAlias}\0${left.workerAlias}`.localeCompare(`${right.nextActionAt}\0${right.ownerAlias}\0${right.workerAlias}`));
|
|
3087
3153
|
const progressWatchEvents = state.progressWatchEvents
|
|
@@ -3315,6 +3381,9 @@ export class GatewayStore {
|
|
|
3315
3381
|
}
|
|
3316
3382
|
replaceCodexRouteForSuccession(state, route, identity, now, routeState, compatibility) {
|
|
3317
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);
|
|
3318
3387
|
route.alias = identity.alias;
|
|
3319
3388
|
route.binding = { ...identity.binding };
|
|
3320
3389
|
route.registrationMode = "explicit_opt_in";
|
|
@@ -3339,8 +3408,8 @@ export class GatewayStore {
|
|
|
3339
3408
|
record.targetAlias !== identity.alias);
|
|
3340
3409
|
state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== oldAlias &&
|
|
3341
3410
|
bucket.sourceAlias !== identity.alias);
|
|
3342
|
-
this.settleProgressWatchesForAliases(state,
|
|
3343
|
-
removePairsForAliases(state,
|
|
3411
|
+
this.settleProgressWatchesForAliases(state, affectedAliases, now);
|
|
3412
|
+
removePairsForAliases(state, affectedAliases);
|
|
3344
3413
|
}
|
|
3345
3414
|
assertAllowedIdentity(identity) {
|
|
3346
3415
|
if (!isPrivateEndpointIdentity(identity)) {
|
|
@@ -3829,43 +3898,62 @@ export class GatewayStore {
|
|
|
3829
3898
|
activity.openIdleMs !== undefined)) {
|
|
3830
3899
|
throw new BridgeError("INVALID_PROGRESS_WATCH", "The message watch activity is malformed or contradictory.");
|
|
3831
3900
|
}
|
|
3832
|
-
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
|
+
}
|
|
3833
3920
|
const existing = state.progressWatches[index];
|
|
3834
3921
|
if (existing !== undefined) {
|
|
3835
|
-
if (
|
|
3836
|
-
|
|
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))) {
|
|
3837
3928
|
throw new BridgeError("PROGRESS_WATCH_OWNERSHIP_MISMATCH", "The watched conversation belongs to different exact endpoints.");
|
|
3838
3929
|
}
|
|
3839
3930
|
if (activity.openIdleMs !== undefined &&
|
|
3840
3931
|
activity.actorAlias !== existing.ownerAlias) {
|
|
3841
3932
|
throw new BridgeError("PROGRESS_WATCH_OWNER_REQUIRED", "Only the exact watch owner may reassert tracking options.");
|
|
3842
3933
|
}
|
|
3843
|
-
if (activity.completionSignal === true
|
|
3844
|
-
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);
|
|
3845
3942
|
state.progressWatches.splice(index, 1);
|
|
3846
3943
|
this.appendProgressWatchEvent(state, existing, {
|
|
3847
|
-
kind:
|
|
3944
|
+
kind: activity.actorAlias === existing.ownerAlias
|
|
3945
|
+
? "done"
|
|
3946
|
+
: "worker_reported_complete",
|
|
3848
3947
|
timestamp: now.toISOString(),
|
|
3849
3948
|
});
|
|
3850
3949
|
return;
|
|
3851
3950
|
}
|
|
3852
|
-
const workerReportedComplete = activity.completionSignal === true &&
|
|
3853
|
-
activity.actorAlias === existing.workerAlias;
|
|
3854
3951
|
const transition = transitionProgressWatch(existing, {
|
|
3855
3952
|
type: "activity",
|
|
3856
3953
|
at: now.getTime(),
|
|
3857
|
-
...(workerReportedComplete
|
|
3858
|
-
? { workerReportedComplete: true }
|
|
3859
|
-
: {}),
|
|
3860
3954
|
});
|
|
3861
3955
|
if (transition.state !== null) {
|
|
3862
3956
|
state.progressWatches[index] = transition.state;
|
|
3863
|
-
if (workerReportedComplete) {
|
|
3864
|
-
this.appendProgressWatchEvent(state, transition.state, {
|
|
3865
|
-
kind: "worker_reported_complete",
|
|
3866
|
-
timestamp: now.toISOString(),
|
|
3867
|
-
});
|
|
3868
|
-
}
|
|
3869
3957
|
}
|
|
3870
3958
|
return;
|
|
3871
3959
|
}
|
|
@@ -3874,13 +3962,9 @@ export class GatewayStore {
|
|
|
3874
3962
|
if (this.config.trackingEnabled === false) {
|
|
3875
3963
|
throw new BridgeError("PROGRESS_TRACKING_DISABLED", "Progress tracking is disabled by the controller configuration.");
|
|
3876
3964
|
}
|
|
3877
|
-
if (
|
|
3965
|
+
if (pair === undefined) {
|
|
3878
3966
|
throw new BridgeError("PROGRESS_WATCH_EDGE_REQUIRED", "Progress tracking requires one exact paired consent edge.");
|
|
3879
3967
|
}
|
|
3880
|
-
if (state.progressWatches.length >=
|
|
3881
|
-
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY)) {
|
|
3882
|
-
throw new BridgeError("PROGRESS_WATCH_CAPACITY_REACHED", "The bounded progress-watch inventory is full.", true);
|
|
3883
|
-
}
|
|
3884
3968
|
const owner = state.routes.find((route) => route.alias === sides.sourceAlias);
|
|
3885
3969
|
const worker = state.routes.find((route) => route.alias === sides.targetAlias);
|
|
3886
3970
|
if (owner === undefined || worker === undefined) {
|
|
@@ -3895,11 +3979,10 @@ export class GatewayStore {
|
|
|
3895
3979
|
idleMs: activity.openIdleMs,
|
|
3896
3980
|
at: now.getTime(),
|
|
3897
3981
|
});
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
});
|
|
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);
|
|
3903
3986
|
}
|
|
3904
3987
|
progressWatchDueTransition(state, watch, now) {
|
|
3905
3988
|
const owner = state.routes.find((route) => route.alias === watch.ownerAlias &&
|
|
@@ -3943,6 +4026,7 @@ export class GatewayStore {
|
|
|
3943
4026
|
effect.nudgeNumber === nudge.nudgeNumber)) {
|
|
3944
4027
|
throw new BridgeError("PROGRESS_WATCH_NUDGE_NOT_DUE", "The requested progress-watch nudge is not currently due.", true);
|
|
3945
4028
|
}
|
|
4029
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
3946
4030
|
state.progressWatches[index] = transition.state;
|
|
3947
4031
|
this.appendProgressWatchEvent(state, transition.state, {
|
|
3948
4032
|
kind: "nudge",
|
|
@@ -3950,7 +4034,41 @@ export class GatewayStore {
|
|
|
3950
4034
|
nudgeNumber: nudge.nudgeNumber,
|
|
3951
4035
|
});
|
|
3952
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
|
+
}
|
|
3953
4070
|
appendProgressWatchEvent(state, watch, event) {
|
|
4071
|
+
this.requireProgressWatchJournalCapacity(state, 1);
|
|
3954
4072
|
state.watchSequence += 1;
|
|
3955
4073
|
state.progressWatchEvents.push({
|
|
3956
4074
|
sequence: state.watchSequence,
|
|
@@ -3968,6 +4086,7 @@ export class GatewayStore {
|
|
|
3968
4086
|
}
|
|
3969
4087
|
}
|
|
3970
4088
|
settleProgressWatchesForAliases(state, aliases, now) {
|
|
4089
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => aliases.has(watch.ownerAlias) || aliases.has(watch.workerAlias)).length);
|
|
3971
4090
|
const retained = [];
|
|
3972
4091
|
for (const watch of state.progressWatches) {
|
|
3973
4092
|
if (!aliases.has(watch.ownerAlias) &&
|
|
@@ -3983,23 +4102,61 @@ export class GatewayStore {
|
|
|
3983
4102
|
state.progressWatches = retained;
|
|
3984
4103
|
}
|
|
3985
4104
|
settleProgressWatchesForPair(state, pair, now) {
|
|
4105
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => progressWatchMatchesPair(watch, pair)).length);
|
|
3986
4106
|
const retained = [];
|
|
3987
4107
|
for (const watch of state.progressWatches) {
|
|
3988
|
-
|
|
3989
|
-
watch.workerAlias === pair.codexAlias) ||
|
|
3990
|
-
(watch.ownerAlias === pair.codexAlias &&
|
|
3991
|
-
watch.workerAlias === pair.claudeAlias);
|
|
3992
|
-
if (!matches) {
|
|
4108
|
+
if (!progressWatchMatchesPair(watch, pair)) {
|
|
3993
4109
|
retained.push(watch);
|
|
3994
4110
|
continue;
|
|
3995
4111
|
}
|
|
3996
4112
|
this.appendProgressWatchEvent(state, watch, {
|
|
3997
|
-
kind: "
|
|
4113
|
+
kind: "pair_removed",
|
|
3998
4114
|
timestamp: now.toISOString(),
|
|
3999
4115
|
});
|
|
4000
4116
|
}
|
|
4001
4117
|
state.progressWatches = retained;
|
|
4002
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
|
+
}
|
|
4003
4160
|
finishMetadata(state, metadata, deliveryState, now, safeErrorCode) {
|
|
4004
4161
|
const counterKey = deliveryState;
|
|
4005
4162
|
state.accounting[counterKey] += 1;
|
|
@@ -4165,6 +4322,7 @@ export class GatewayStore {
|
|
|
4165
4322
|
const state = this.requireState();
|
|
4166
4323
|
this.recoverCodexSuccessionAfterRestart(state, now);
|
|
4167
4324
|
if (this.config.trackingEnabled === false) {
|
|
4325
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.length);
|
|
4168
4326
|
for (const watch of state.progressWatches) {
|
|
4169
4327
|
this.appendProgressWatchEvent(state, watch, {
|
|
4170
4328
|
kind: "disabled",
|
|
@@ -4173,6 +4331,9 @@ export class GatewayStore {
|
|
|
4173
4331
|
}
|
|
4174
4332
|
state.progressWatches = [];
|
|
4175
4333
|
}
|
|
4334
|
+
else {
|
|
4335
|
+
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => !watch.degradedNoticeSent).length);
|
|
4336
|
+
}
|
|
4176
4337
|
for (let index = 0; index < state.progressWatches.length; index += 1) {
|
|
4177
4338
|
const watch = state.progressWatches[index];
|
|
4178
4339
|
if (watch === undefined)
|
|
@@ -4185,7 +4346,7 @@ export class GatewayStore {
|
|
|
4185
4346
|
if (transition.state !== null) {
|
|
4186
4347
|
state.progressWatches[index] = transition.state;
|
|
4187
4348
|
}
|
|
4188
|
-
if (transition.effects.some((effect) => effect.type === "
|
|
4349
|
+
if (transition.effects.some((effect) => effect.type === "record_capability_degraded")) {
|
|
4189
4350
|
this.appendProgressWatchEvent(state, transition.state ?? watch, {
|
|
4190
4351
|
kind: "capability_degraded",
|
|
4191
4352
|
timestamp: now.toISOString(),
|
|
@@ -4502,6 +4663,7 @@ export class GatewayStore {
|
|
|
4502
4663
|
parsed = migratePreCodexOrphanRemovalJournal(parsed);
|
|
4503
4664
|
parsed = migrateLegacyHopCounts(parsed);
|
|
4504
4665
|
parsed = migrateLegacyCompatibilityCertifications(parsed);
|
|
4666
|
+
parsed = migrateLegacyProgressWatchState(parsed, this.config.limits.eventCapacity);
|
|
4505
4667
|
if (!isPersistedState(parsed)) {
|
|
4506
4668
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
|
|
4507
4669
|
}
|