agent-embassy 1.4.1 → 1.6.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 +40 -1
- package/CONTRIBUTING.md +69 -6
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/SECURITY.md +152 -27
- package/dist/src/gateway/claude-peer.d.ts +7 -4
- package/dist/src/gateway/claude-peer.js +80 -36
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +4 -0
- package/dist/src/gateway/claude-runtime.js +50 -39
- 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 +1 -1
- package/dist/src/gateway/cli-copy.en.js +1 -1
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +1 -1
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +1 -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 +6 -7
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +44 -2
- package/dist/src/gateway/codex-app-server.js +536 -2
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +10 -6
- package/dist/src/gateway/codex-local-transport.js +101 -56
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +42 -3
- package/dist/src/gateway/compatibility.js +190 -37
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/control.js +28 -30
- 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 +42 -17
- package/dist/src/gateway/dashboard-copy.en.js +56 -31
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +42 -17
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +42 -17
- package/dist/src/gateway/dashboard-copy.zh-CN.js +56 -31
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +199 -11
- package/dist/src/gateway/dashboard-model.js +478 -75
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.d.ts +3 -0
- package/dist/src/gateway/dashboard.js +62 -137
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/deepseek-detect.d.ts +13 -0
- package/dist/src/gateway/deepseek-detect.js +125 -0
- package/dist/src/gateway/deepseek-detect.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +158 -256
- package/dist/src/gateway/live-dashboard-assets.js +2 -1
- package/dist/src/gateway/live-dashboard-assets.js.map +1 -1
- package/dist/src/gateway/live-dashboard-stream.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-stream.js +2 -2
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +50 -64
- package/dist/src/gateway/progress-watch-machine.js +42 -133
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +2 -0
- package/dist/src/gateway/provenance-envelope.js +13 -3
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +80 -16
- package/dist/src/gateway/providers.js +782 -115
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.d.ts +10 -5
- package/dist/src/gateway/server.js +100 -13
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +46 -3
- package/dist/src/gateway/service.js +363 -101
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +23 -28
- package/dist/src/gateway/store.js +479 -467
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +40 -12
- package/dist/src/gateway/types.js +145 -4
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +8 -8
- package/docs/CONFIGURATION.zh-CN.md +8 -8
- package/docs/DASHBOARD.md +35 -6
- package/docs/DASHBOARD.zh-CN.md +1 -1
- package/docs/GATEWAY-ARCHITECTURE.md +173 -61
- package/package.json +1 -1
- package/skills/embassy-peer/SKILL.md +2 -2
|
@@ -6,9 +6,9 @@ import path from "node:path";
|
|
|
6
6
|
import { BridgeError } from "../errors.js";
|
|
7
7
|
import { KeyedMutex } from "../mutex.js";
|
|
8
8
|
import { isCodexRegistrationGeneration } from "./codex-registration-generation.js";
|
|
9
|
-
import { compatibilityCacheKey, isCompatibilityAttestation, } from "./compatibility.js";
|
|
10
|
-
import { PROGRESS_WATCH_DEFAULT_CAPACITY, PROGRESS_WATCH_HARD_CAPACITY, PROGRESS_WATCH_MAX_IDLE_MS, PROGRESS_WATCH_MIN_IDLE_MS,
|
|
11
|
-
import { CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY, CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY, compatibilityStates, connectorHealthStates, deliveryStates, gatewayProviders, messageDirections, projectGatewayPublicSnapshot, routeRegistrationModes, routeStates, } from "./types.js";
|
|
9
|
+
import { compatibilityCacheKey, isCompatibilityAttestation, isPersistedCompatibilityAttestation, } from "./compatibility.js";
|
|
10
|
+
import { PROGRESS_WATCH_DEFAULT_CAPACITY, PROGRESS_WATCH_HARD_CAPACITY, PROGRESS_WATCH_MAX_IDLE_MS, PROGRESS_WATCH_MIN_IDLE_MS, commitProgressWatchNudge, createProgressWatch, deferProgressWatchNudge, inspectProgressWatchDue, progressWatchJournalKinds, recordProgressWatchActivity, } from "./progress-watch-machine.js";
|
|
11
|
+
import { CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY, CODEX_ORPHAN_REMOVAL_JOURNAL_CAPACITY, compatibilityStates, connectorHealthStates, deliveryStates, gatewayPublicSnapshotLimits, 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";
|
|
@@ -44,8 +44,6 @@ const ROUTE_STATES = new Set(routeStates);
|
|
|
44
44
|
const REGISTRATION_MODES = new Set(routeRegistrationModes);
|
|
45
45
|
const DIRECTIONS = new Set(messageDirections);
|
|
46
46
|
const DELIVERY_STATES = new Set(deliveryStates);
|
|
47
|
-
const PROGRESS_WATCH_PHASES = new Set(progressWatchPhases);
|
|
48
|
-
const PROGRESS_WATCH_CAPABILITIES = new Set(progressWatchCapabilities);
|
|
49
47
|
const PROGRESS_WATCH_JOURNAL_KINDS = new Set(progressWatchJournalKinds);
|
|
50
48
|
export const codexSuccessionJournalStages = [
|
|
51
49
|
"prepared",
|
|
@@ -289,7 +287,30 @@ function progressWatchMatchesPair(watch, pair) {
|
|
|
289
287
|
watch.workerAlias === pair.claudeAlias &&
|
|
290
288
|
watch.workerLease === pair.claudeOwnerLease));
|
|
291
289
|
}
|
|
292
|
-
function
|
|
290
|
+
function progressWatchJournalEvent(watch, timestamp, transition) {
|
|
291
|
+
return {
|
|
292
|
+
timestamp,
|
|
293
|
+
conversationId: watch.conversationId,
|
|
294
|
+
ownerAlias: watch.ownerAlias,
|
|
295
|
+
workerAlias: watch.workerAlias,
|
|
296
|
+
...transition,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const LEGACY_V14_PROGRESS_WATCH_JOURNAL_KINDS = new Set([
|
|
300
|
+
"opened",
|
|
301
|
+
"replaced",
|
|
302
|
+
"activity",
|
|
303
|
+
"nudge",
|
|
304
|
+
"worker_reported_complete",
|
|
305
|
+
"capability_degraded",
|
|
306
|
+
"conversation_rebound",
|
|
307
|
+
"done",
|
|
308
|
+
"unresponsive",
|
|
309
|
+
"pair_removed",
|
|
310
|
+
"endpoint_retired",
|
|
311
|
+
"disabled",
|
|
312
|
+
]);
|
|
313
|
+
function isLegacyV14ProgressWatch(value) {
|
|
293
314
|
if (!isObject(value) ||
|
|
294
315
|
!hasOnlyKeys(value, [
|
|
295
316
|
"conversationId",
|
|
@@ -306,7 +327,7 @@ function isLegacyProgressWatchMachine(value) {
|
|
|
306
327
|
"nextActionAt",
|
|
307
328
|
"capability",
|
|
308
329
|
"degradedNoticeSent",
|
|
309
|
-
]
|
|
330
|
+
])) {
|
|
310
331
|
return false;
|
|
311
332
|
}
|
|
312
333
|
return (typeof value.conversationId === "string" &&
|
|
@@ -325,8 +346,7 @@ function isLegacyProgressWatchMachine(value) {
|
|
|
325
346
|
isPositiveInteger(value.idleMs) &&
|
|
326
347
|
value.idleMs >= PROGRESS_WATCH_MIN_IDLE_MS &&
|
|
327
348
|
value.idleMs <= PROGRESS_WATCH_MAX_IDLE_MS &&
|
|
328
|
-
|
|
329
|
-
PROGRESS_WATCH_PHASES.has(value.phase) &&
|
|
349
|
+
(value.phase === "quiet" || value.phase === "episode") &&
|
|
330
350
|
(value.nudgeCount === 0 ||
|
|
331
351
|
value.nudgeCount === 1 ||
|
|
332
352
|
value.nudgeCount === 2) &&
|
|
@@ -334,17 +354,10 @@ function isLegacyProgressWatchMachine(value) {
|
|
|
334
354
|
? value.nudgeCount === 0
|
|
335
355
|
: value.nudgeCount === 1 || value.nudgeCount === 2) &&
|
|
336
356
|
isIsoTimestamp(value.nextActionAt) &&
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
typeof value.degradedNoticeSent === "boolean" &&
|
|
340
|
-
(value.workerReportedCompleteAt === undefined ||
|
|
341
|
-
isIsoTimestamp(value.workerReportedCompleteAt)));
|
|
357
|
+
(value.capability === "conversation" || value.capability === "route") &&
|
|
358
|
+
typeof value.degradedNoticeSent === "boolean");
|
|
342
359
|
}
|
|
343
|
-
function
|
|
344
|
-
return (isLegacyProgressWatchMachine(value) &&
|
|
345
|
-
!Object.hasOwn(value, "workerReportedCompleteAt"));
|
|
346
|
-
}
|
|
347
|
-
function isProgressWatchJournalEvent(value) {
|
|
360
|
+
function isLegacyV14ProgressWatchJournalEvent(value) {
|
|
348
361
|
return (isObject(value) &&
|
|
349
362
|
hasOnlyKeys(value, [
|
|
350
363
|
"sequence",
|
|
@@ -364,14 +377,94 @@ function isProgressWatchJournalEvent(value) {
|
|
|
364
377
|
ALIAS_PATTERN.test(value.workerAlias) &&
|
|
365
378
|
value.ownerAlias !== value.workerAlias &&
|
|
366
379
|
typeof value.kind === "string" &&
|
|
367
|
-
|
|
368
|
-
(value.nudgeNumber === undefined ||
|
|
369
|
-
value.nudgeNumber === 1 ||
|
|
370
|
-
value.nudgeNumber === 2) &&
|
|
380
|
+
LEGACY_V14_PROGRESS_WATCH_JOURNAL_KINDS.has(value.kind) &&
|
|
371
381
|
(value.kind === "nudge"
|
|
372
382
|
? value.nudgeNumber === 1 || value.nudgeNumber === 2
|
|
373
383
|
: value.nudgeNumber === undefined));
|
|
374
384
|
}
|
|
385
|
+
function isProgressWatch(value) {
|
|
386
|
+
return (isObject(value) &&
|
|
387
|
+
hasOnlyKeys(value, [
|
|
388
|
+
"conversationId",
|
|
389
|
+
"ownerAlias",
|
|
390
|
+
"workerAlias",
|
|
391
|
+
"ownerLease",
|
|
392
|
+
"workerLease",
|
|
393
|
+
"lastActivityAt",
|
|
394
|
+
"idleMs",
|
|
395
|
+
"nudgeCount",
|
|
396
|
+
"nextActionAt",
|
|
397
|
+
]) &&
|
|
398
|
+
typeof value.conversationId === "string" &&
|
|
399
|
+
CONVERSATION_ID_PATTERN.test(value.conversationId) &&
|
|
400
|
+
typeof value.ownerAlias === "string" &&
|
|
401
|
+
ALIAS_PATTERN.test(value.ownerAlias) &&
|
|
402
|
+
typeof value.workerAlias === "string" &&
|
|
403
|
+
ALIAS_PATTERN.test(value.workerAlias) &&
|
|
404
|
+
value.ownerAlias !== value.workerAlias &&
|
|
405
|
+
isPrivateToken(value.ownerLease) &&
|
|
406
|
+
isPrivateToken(value.workerLease) &&
|
|
407
|
+
value.ownerLease !== value.workerLease &&
|
|
408
|
+
isIsoTimestamp(value.lastActivityAt) &&
|
|
409
|
+
isPositiveInteger(value.idleMs) &&
|
|
410
|
+
value.idleMs >= PROGRESS_WATCH_MIN_IDLE_MS &&
|
|
411
|
+
value.idleMs <= PROGRESS_WATCH_MAX_IDLE_MS &&
|
|
412
|
+
(value.nudgeCount === 0 ||
|
|
413
|
+
value.nudgeCount === 1 ||
|
|
414
|
+
value.nudgeCount === 2) &&
|
|
415
|
+
isIsoTimestamp(value.nextActionAt));
|
|
416
|
+
}
|
|
417
|
+
function isProgressWatchJournalEvent(value) {
|
|
418
|
+
if (!isObject(value) ||
|
|
419
|
+
!hasOnlyKeys(value, [
|
|
420
|
+
"sequence",
|
|
421
|
+
"timestamp",
|
|
422
|
+
"conversationId",
|
|
423
|
+
"ownerAlias",
|
|
424
|
+
"workerAlias",
|
|
425
|
+
"kind",
|
|
426
|
+
"actor",
|
|
427
|
+
], ["reason"]) ||
|
|
428
|
+
!isPositiveInteger(value.sequence) ||
|
|
429
|
+
!isIsoTimestamp(value.timestamp) ||
|
|
430
|
+
typeof value.conversationId !== "string" ||
|
|
431
|
+
!CONVERSATION_ID_PATTERN.test(value.conversationId) ||
|
|
432
|
+
typeof value.ownerAlias !== "string" ||
|
|
433
|
+
!ALIAS_PATTERN.test(value.ownerAlias) ||
|
|
434
|
+
typeof value.workerAlias !== "string" ||
|
|
435
|
+
!ALIAS_PATTERN.test(value.workerAlias) ||
|
|
436
|
+
value.ownerAlias === value.workerAlias ||
|
|
437
|
+
typeof value.kind !== "string" ||
|
|
438
|
+
!PROGRESS_WATCH_JOURNAL_KINDS.has(value.kind)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (value.kind === "opened") {
|
|
442
|
+
return value.actor === "owner" && value.reason === undefined;
|
|
443
|
+
}
|
|
444
|
+
if (value.kind === "replaced") {
|
|
445
|
+
return ((value.actor === "owner" || value.actor === "unknown") &&
|
|
446
|
+
value.reason === undefined);
|
|
447
|
+
}
|
|
448
|
+
if (value.kind !== "settled" || typeof value.reason !== "string") {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (value.reason === "done") {
|
|
452
|
+
return value.actor === "owner" || value.actor === "worker";
|
|
453
|
+
}
|
|
454
|
+
if (value.reason === "untracked")
|
|
455
|
+
return value.actor === "operator";
|
|
456
|
+
if (value.reason === "pair_removed")
|
|
457
|
+
return value.actor === "operator";
|
|
458
|
+
if (value.reason === "endpoint_retired") {
|
|
459
|
+
return value.actor === "gateway" || value.actor === "operator";
|
|
460
|
+
}
|
|
461
|
+
if (value.reason === "idle_timeout" ||
|
|
462
|
+
value.reason === "tracking_disabled" ||
|
|
463
|
+
value.reason === "legacy_upgrade") {
|
|
464
|
+
return value.actor === "gateway";
|
|
465
|
+
}
|
|
466
|
+
return value.reason === "legacy_done" && value.actor === "unknown";
|
|
467
|
+
}
|
|
375
468
|
function isConnectorRecord(value) {
|
|
376
469
|
if (!isObject(value) ||
|
|
377
470
|
!hasOnlyKeys(value, [
|
|
@@ -784,8 +877,9 @@ function isLegacyCompatibilityCertification(value) {
|
|
|
784
877
|
}
|
|
785
878
|
/**
|
|
786
879
|
* v1.1-v1.2 compatibility rows could contain manual live-certification
|
|
787
|
-
* evidence.
|
|
788
|
-
*
|
|
880
|
+
* evidence. Discard only an exact old certification, retain the automatic
|
|
881
|
+
* bounded probes as schema-attested evidence, and leave malformed rows
|
|
882
|
+
* untouched for strict state validation to reject.
|
|
789
883
|
*/
|
|
790
884
|
function migrateLegacyCompatibilityCertifications(value) {
|
|
791
885
|
if (!isObject(value) ||
|
|
@@ -795,105 +889,180 @@ function migrateLegacyCompatibilityCertifications(value) {
|
|
|
795
889
|
}
|
|
796
890
|
let changed = false;
|
|
797
891
|
const compatibilityAttestations = value.compatibilityAttestations.map((entry) => {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
892
|
+
let candidate = entry;
|
|
893
|
+
if (isObject(entry) &&
|
|
894
|
+
Object.hasOwn(entry, "certification") &&
|
|
895
|
+
isLegacyCompatibilityCertification(entry.certification)) {
|
|
896
|
+
const { certification: _legacyCertification, ...current } = entry;
|
|
897
|
+
candidate =
|
|
898
|
+
current.tier === "certified"
|
|
899
|
+
? { ...current, tier: "schema_attested" }
|
|
900
|
+
: current;
|
|
901
|
+
}
|
|
902
|
+
if (!isPersistedCompatibilityAttestation(candidate))
|
|
802
903
|
return entry;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
return entry;
|
|
807
|
-
changed = true;
|
|
808
|
-
return current;
|
|
904
|
+
if (candidate !== entry)
|
|
905
|
+
changed = true;
|
|
906
|
+
return candidate;
|
|
809
907
|
});
|
|
810
908
|
return changed ? { ...value, compatibilityAttestations } : value;
|
|
811
909
|
}
|
|
910
|
+
function legacyProgressWatchGraphIsValid(value, watches) {
|
|
911
|
+
if (!isObject(value) ||
|
|
912
|
+
!Array.isArray(value.routes) ||
|
|
913
|
+
!value.routes.every(isRouteRecord) ||
|
|
914
|
+
!Array.isArray(value.pairs) ||
|
|
915
|
+
value.pairs.length > 256 ||
|
|
916
|
+
!value.pairs.every(isPairRecord) ||
|
|
917
|
+
new Set(watches.map((watch) => watch.conversationId)).size !==
|
|
918
|
+
watches.length ||
|
|
919
|
+
new Set(watches.map(progressWatchPairKey)).size !== watches.length) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
const routes = value.routes;
|
|
923
|
+
const pairs = value.pairs;
|
|
924
|
+
for (const watch of watches) {
|
|
925
|
+
const owner = routes.find((route) => route.alias === watch.ownerAlias &&
|
|
926
|
+
route.binding.ownerLease === watch.ownerLease);
|
|
927
|
+
const worker = routes.find((route) => route.alias === watch.workerAlias &&
|
|
928
|
+
route.binding.ownerLease === watch.workerLease);
|
|
929
|
+
if (owner === undefined ||
|
|
930
|
+
worker === undefined ||
|
|
931
|
+
owner.binding.provider === worker.binding.provider ||
|
|
932
|
+
owner.binding.hostId !== worker.binding.hostId) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
const claude = owner.binding.provider === "claude" ? owner : worker;
|
|
936
|
+
const codex = owner.binding.provider === "codex" ? owner : worker;
|
|
937
|
+
if (!pairs.some((pair) => pair.claudeAlias === claude.alias &&
|
|
938
|
+
pair.codexAlias === codex.alias &&
|
|
939
|
+
pair.claudeOwnerLease === claude.binding.ownerLease &&
|
|
940
|
+
pair.codexOwnerLease === codex.binding.ownerLease)) {
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
function mapLegacyProgressWatchEvent(event) {
|
|
947
|
+
const base = {
|
|
948
|
+
timestamp: event.timestamp,
|
|
949
|
+
conversationId: event.conversationId,
|
|
950
|
+
ownerAlias: event.ownerAlias,
|
|
951
|
+
workerAlias: event.workerAlias,
|
|
952
|
+
};
|
|
953
|
+
switch (event.kind) {
|
|
954
|
+
case "opened":
|
|
955
|
+
return { ...base, kind: "opened", actor: "owner" };
|
|
956
|
+
case "replaced":
|
|
957
|
+
// v1.4 used this kind for both owner replacement and an automatic
|
|
958
|
+
// duplicate migration. Its actor cannot be reconstructed honestly.
|
|
959
|
+
return { ...base, kind: "replaced", actor: "unknown" };
|
|
960
|
+
case "worker_reported_complete":
|
|
961
|
+
return { ...base, kind: "settled", actor: "worker", reason: "done" };
|
|
962
|
+
case "done":
|
|
963
|
+
return {
|
|
964
|
+
...base,
|
|
965
|
+
kind: "settled",
|
|
966
|
+
actor: "unknown",
|
|
967
|
+
reason: "legacy_done",
|
|
968
|
+
};
|
|
969
|
+
case "unresponsive":
|
|
970
|
+
return {
|
|
971
|
+
...base,
|
|
972
|
+
kind: "settled",
|
|
973
|
+
actor: "gateway",
|
|
974
|
+
reason: "idle_timeout",
|
|
975
|
+
};
|
|
976
|
+
case "pair_removed":
|
|
977
|
+
return {
|
|
978
|
+
...base,
|
|
979
|
+
kind: "settled",
|
|
980
|
+
actor: "operator",
|
|
981
|
+
reason: "pair_removed",
|
|
982
|
+
};
|
|
983
|
+
case "endpoint_retired":
|
|
984
|
+
return {
|
|
985
|
+
...base,
|
|
986
|
+
kind: "settled",
|
|
987
|
+
actor: "gateway",
|
|
988
|
+
reason: "endpoint_retired",
|
|
989
|
+
};
|
|
990
|
+
case "disabled":
|
|
991
|
+
return {
|
|
992
|
+
...base,
|
|
993
|
+
kind: "settled",
|
|
994
|
+
actor: "gateway",
|
|
995
|
+
reason: "tracking_disabled",
|
|
996
|
+
};
|
|
997
|
+
case "activity":
|
|
998
|
+
case "nudge":
|
|
999
|
+
case "capability_degraded":
|
|
1000
|
+
case "conversation_rebound":
|
|
1001
|
+
return undefined;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
812
1004
|
/**
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
* is idempotent when loaded again.
|
|
1005
|
+
* v1.4.x watches carried capability and phase state that no longer has product
|
|
1006
|
+
* meaning. Strictly recognize the complete old graph, preserve representable
|
|
1007
|
+
* history, and settle every live row once at upgrade. Rebase the bounded
|
|
1008
|
+
* private sequence so a historical counter cannot make the new format fatal.
|
|
818
1009
|
*/
|
|
819
|
-
function
|
|
1010
|
+
function migrateV14ProgressWatchState(value, migratedAt) {
|
|
820
1011
|
if (!isObject(value) ||
|
|
821
1012
|
value.schemaVersion !== 1 ||
|
|
822
1013
|
!isIsoTimestamp(value.updatedAt) ||
|
|
1014
|
+
!isIsoTimestamp(migratedAt) ||
|
|
823
1015
|
!isNonNegativeInteger(value.watchSequence) ||
|
|
824
1016
|
!Array.isArray(value.progressWatches) ||
|
|
825
|
-
value.
|
|
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) {
|
|
1017
|
+
!Array.isArray(value.progressWatchEvents)) {
|
|
831
1018
|
return value;
|
|
832
1019
|
}
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (existingEvents.some((event, index) => event.sequence > watchSequenceBefore ||
|
|
837
|
-
(index > 0 && event.sequence <= existingEvents[index - 1].sequence))) {
|
|
1020
|
+
const hasLegacyRows = value.progressWatches.some(isLegacyV14ProgressWatch) ||
|
|
1021
|
+
value.progressWatchEvents.some(isLegacyV14ProgressWatchJournalEvent);
|
|
1022
|
+
if (!hasLegacyRows)
|
|
838
1023
|
return value;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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)
|
|
1024
|
+
if (value.progressWatches.length > PROGRESS_WATCH_HARD_CAPACITY ||
|
|
1025
|
+
!value.progressWatches.every(isLegacyV14ProgressWatch) ||
|
|
1026
|
+
value.progressWatchEvents.length > gatewayPublicSnapshotLimits.messages ||
|
|
1027
|
+
!value.progressWatchEvents.every(isLegacyV14ProgressWatchJournalEvent)) {
|
|
859
1028
|
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
1029
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1030
|
+
const watches = value.progressWatches;
|
|
1031
|
+
const legacyEvents = value.progressWatchEvents;
|
|
1032
|
+
const watchSequence = value.watchSequence;
|
|
1033
|
+
if (!legacyProgressWatchGraphIsValid(value, watches) ||
|
|
1034
|
+
legacyEvents.some((event, index) => event.sequence > watchSequence ||
|
|
1035
|
+
(index > 0 && event.sequence <= legacyEvents[index - 1].sequence))) {
|
|
1036
|
+
return value;
|
|
890
1037
|
}
|
|
1038
|
+
const pending = [];
|
|
1039
|
+
for (const event of legacyEvents) {
|
|
1040
|
+
const mapped = mapLegacyProgressWatchEvent(event);
|
|
1041
|
+
if (mapped !== undefined)
|
|
1042
|
+
pending.push(mapped);
|
|
1043
|
+
}
|
|
1044
|
+
const settlements = watches.map((watch) => ({
|
|
1045
|
+
timestamp: migratedAt,
|
|
1046
|
+
conversationId: watch.conversationId,
|
|
1047
|
+
ownerAlias: watch.ownerAlias,
|
|
1048
|
+
workerAlias: watch.workerAlias,
|
|
1049
|
+
kind: "settled",
|
|
1050
|
+
actor: "gateway",
|
|
1051
|
+
reason: "legacy_upgrade",
|
|
1052
|
+
}));
|
|
1053
|
+
const retainedHistoryCount = gatewayPublicSnapshotLimits.messages - settlements.length;
|
|
1054
|
+
const retained = [
|
|
1055
|
+
...pending.slice(Math.max(0, pending.length - retainedHistoryCount)),
|
|
1056
|
+
...settlements,
|
|
1057
|
+
];
|
|
1058
|
+
const progressWatchEvents = retained.map((event, index) => ({
|
|
1059
|
+
...event,
|
|
1060
|
+
sequence: index + 1,
|
|
1061
|
+
}));
|
|
891
1062
|
return {
|
|
892
1063
|
...value,
|
|
893
|
-
watchSequence,
|
|
894
|
-
progressWatches:
|
|
895
|
-
.filter((_, index) => retained.has(index))
|
|
896
|
-
.map(({ workerReportedCompleteAt: _legacy, ...watch }) => watch),
|
|
1064
|
+
watchSequence: progressWatchEvents.length,
|
|
1065
|
+
progressWatches: [],
|
|
897
1066
|
progressWatchEvents,
|
|
898
1067
|
};
|
|
899
1068
|
}
|
|
@@ -936,14 +1105,15 @@ function isPersistedState(value) {
|
|
|
936
1105
|
value.pairs.length > 256 ||
|
|
937
1106
|
!value.pairs.every(isPairRecord) ||
|
|
938
1107
|
!Array.isArray(value.progressWatches) ||
|
|
939
|
-
value.progressWatches.length >
|
|
940
|
-
!value.progressWatches.every(
|
|
1108
|
+
value.progressWatches.length > PROGRESS_WATCH_HARD_CAPACITY ||
|
|
1109
|
+
!value.progressWatches.every(isProgressWatch) ||
|
|
941
1110
|
!Array.isArray(value.progressWatchEvents) ||
|
|
1111
|
+
value.progressWatchEvents.length > gatewayPublicSnapshotLimits.messages ||
|
|
942
1112
|
!value.progressWatchEvents.every(isProgressWatchJournalEvent) ||
|
|
943
1113
|
!Array.isArray(value.compatibilityAttestations) ||
|
|
944
1114
|
value.compatibilityAttestations.length >
|
|
945
1115
|
COMPATIBILITY_ATTESTATION_CAPACITY ||
|
|
946
|
-
!value.compatibilityAttestations.every(
|
|
1116
|
+
!value.compatibilityAttestations.every(isPersistedCompatibilityAttestation) ||
|
|
947
1117
|
!Array.isArray(value.codexEndpointRefreshEvents) ||
|
|
948
1118
|
value.codexEndpointRefreshEvents.length >
|
|
949
1119
|
CODEX_ENDPOINT_REFRESH_JOURNAL_CAPACITY ||
|
|
@@ -959,6 +1129,7 @@ function isPersistedState(value) {
|
|
|
959
1129
|
!Array.isArray(value.inFlight) ||
|
|
960
1130
|
!value.inFlight.every(isInFlightMetadata) ||
|
|
961
1131
|
!Array.isArray(value.events) ||
|
|
1132
|
+
value.events.length > gatewayPublicSnapshotLimits.messages ||
|
|
962
1133
|
!value.events.every(isEvent) ||
|
|
963
1134
|
!Array.isArray(value.dedupe) ||
|
|
964
1135
|
!value.dedupe.every(isDedupeRecord) ||
|
|
@@ -1333,6 +1504,15 @@ function renamePairAlias(state, previousAlias, newAlias, now) {
|
|
|
1333
1504
|
}
|
|
1334
1505
|
}
|
|
1335
1506
|
}
|
|
1507
|
+
function renameProgressWatchAlias(state, previousAlias, newAlias) {
|
|
1508
|
+
if (previousAlias === newAlias)
|
|
1509
|
+
return;
|
|
1510
|
+
state.progressWatches = state.progressWatches.map((watch) => watch.ownerAlias === previousAlias
|
|
1511
|
+
? { ...watch, ownerAlias: newAlias }
|
|
1512
|
+
: watch.workerAlias === previousAlias
|
|
1513
|
+
? { ...watch, workerAlias: newAlias }
|
|
1514
|
+
: watch);
|
|
1515
|
+
}
|
|
1336
1516
|
function removePairsForAliases(state, aliases) {
|
|
1337
1517
|
state.pairs = state.pairs.filter((pair) => !aliases.has(pair.claudeAlias) && !aliases.has(pair.codexAlias));
|
|
1338
1518
|
}
|
|
@@ -1396,8 +1576,8 @@ export class GatewayStore {
|
|
|
1396
1576
|
}
|
|
1397
1577
|
await this.acquireLock();
|
|
1398
1578
|
try {
|
|
1399
|
-
const loaded = await this.loadStateFile();
|
|
1400
1579
|
const now = this.now();
|
|
1580
|
+
const loaded = await this.loadStateFile(now);
|
|
1401
1581
|
this.state = loaded ?? {
|
|
1402
1582
|
schemaVersion: 1,
|
|
1403
1583
|
createdAt: now.toISOString(),
|
|
@@ -1627,22 +1807,21 @@ export class GatewayStore {
|
|
|
1627
1807
|
// Replacing an endpoint is scoped to the replacement alias. Other
|
|
1628
1808
|
// selected Claude routes and their independent consent edges survive.
|
|
1629
1809
|
const retired = selectedClaudeRoutes.filter((route) => route !== retained && route === aliasOwner);
|
|
1810
|
+
if (state.routes.some((route) => route !== retained &&
|
|
1811
|
+
!retired.includes(route) &&
|
|
1812
|
+
(sameRouteTarget(route.binding, replacement.binding) ||
|
|
1813
|
+
route.binding.ownerLease === replacement.binding.ownerLease))) {
|
|
1814
|
+
throw new BridgeError("ROUTE_BINDING_COLLISION", "The replacement Claude authority collides with a non-retired route.");
|
|
1815
|
+
}
|
|
1630
1816
|
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);
|
|
1633
1817
|
const settlementPlan = this.validateAffectedInFlightSettlements(state, retiredAliases, input.inFlightSettlements ?? []);
|
|
1634
|
-
const settlements = this.terminateAffectedMessages(state, retiredAliases, now, "ROUTE_UNREGISTERED", settlementPlan);
|
|
1635
1818
|
this.settleProgressWatchesForAliases(state, retiredAliases, now);
|
|
1819
|
+
const settlements = this.terminateAffectedMessages(state, retiredAliases, now, "ROUTE_UNREGISTERED", settlementPlan);
|
|
1636
1820
|
state.routes = state.routes.filter((route) => !retired.includes(route));
|
|
1637
1821
|
removePairsForAliases(state, retiredAliases);
|
|
1638
1822
|
state.rateBuckets = state.rateBuckets.filter((bucket) => !retiredAliases.has(bucket.sourceAlias));
|
|
1639
1823
|
state.dedupe = state.dedupe.filter((record) => !retiredAliases.has(record.sourceAlias) &&
|
|
1640
1824
|
!retiredAliases.has(record.targetAlias));
|
|
1641
|
-
if (state.routes.some((route) => route !== retained &&
|
|
1642
|
-
(sameRouteTarget(route.binding, replacement.binding) ||
|
|
1643
|
-
route.binding.ownerLease === replacement.binding.ownerLease))) {
|
|
1644
|
-
throw new BridgeError("ROUTE_BINDING_COLLISION", "The replacement Claude authority collides with a non-retired route.");
|
|
1645
|
-
}
|
|
1646
1825
|
if (retained !== undefined) {
|
|
1647
1826
|
const previousAlias = retained.alias;
|
|
1648
1827
|
retained.alias = replacement.alias;
|
|
@@ -1672,6 +1851,7 @@ export class GatewayStore {
|
|
|
1672
1851
|
}
|
|
1673
1852
|
renameRateBucket(state, previousAlias, replacement.alias);
|
|
1674
1853
|
renamePairAlias(state, previousAlias, replacement.alias, now);
|
|
1854
|
+
renameProgressWatchAlias(state, previousAlias, replacement.alias);
|
|
1675
1855
|
}
|
|
1676
1856
|
}
|
|
1677
1857
|
else {
|
|
@@ -2006,7 +2186,6 @@ export class GatewayStore {
|
|
|
2006
2186
|
return await this.mutate(async (state, now) => {
|
|
2007
2187
|
const route = this.requireOwnedRoute(state, alias, ownerLease);
|
|
2008
2188
|
const aliases = new Set([route.alias]);
|
|
2009
|
-
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => aliases.has(watch.ownerAlias) || aliases.has(watch.workerAlias)).length);
|
|
2010
2189
|
const settlementPlan = this.validateAffectedInFlightSettlements(state, aliases, inFlightSettlements);
|
|
2011
2190
|
route.enabled = false;
|
|
2012
2191
|
route.state = "disabled";
|
|
@@ -2014,13 +2193,13 @@ export class GatewayStore {
|
|
|
2014
2193
|
return this.terminateAffectedMessages(state, aliases, now, "ROUTE_DISABLED", settlementPlan);
|
|
2015
2194
|
});
|
|
2016
2195
|
}
|
|
2017
|
-
async unregisterRoute(alias, ownerLease, inFlightSettlements = []) {
|
|
2196
|
+
async unregisterRoute(alias, ownerLease, inFlightSettlements = [], progressWatchSettlementActor = "gateway") {
|
|
2018
2197
|
return await this.mutate(async (state, now) => {
|
|
2019
2198
|
const route = this.requireOwnedRoute(state, alias, ownerLease);
|
|
2020
2199
|
const aliases = new Set([route.alias]);
|
|
2021
2200
|
const settlementPlan = this.validateAffectedInFlightSettlements(state, aliases, inFlightSettlements);
|
|
2201
|
+
this.settleProgressWatchesForAliases(state, aliases, now, progressWatchSettlementActor);
|
|
2022
2202
|
const settlements = this.terminateAffectedMessages(state, aliases, now, "ROUTE_UNREGISTERED", settlementPlan);
|
|
2023
|
-
this.settleProgressWatchesForAliases(state, aliases, now);
|
|
2024
2203
|
state.routes = state.routes.filter((candidate) => candidate !== route);
|
|
2025
2204
|
removePairsForAliases(state, aliases);
|
|
2026
2205
|
const remainingCodexHosts = new Set(state.routes
|
|
@@ -2068,25 +2247,7 @@ export class GatewayStore {
|
|
|
2068
2247
|
}
|
|
2069
2248
|
renameRateBucket(state, alias, newAlias);
|
|
2070
2249
|
renamePairAlias(state, alias, newAlias, now);
|
|
2071
|
-
|
|
2072
|
-
const watch = state.progressWatches[index];
|
|
2073
|
-
if (watch === undefined)
|
|
2074
|
-
continue;
|
|
2075
|
-
if (watch.ownerAlias === alias) {
|
|
2076
|
-
state.progressWatches[index] = {
|
|
2077
|
-
...watch,
|
|
2078
|
-
ownerAlias: newAlias,
|
|
2079
|
-
updatedAt: now.toISOString(),
|
|
2080
|
-
};
|
|
2081
|
-
}
|
|
2082
|
-
else if (watch.workerAlias === alias) {
|
|
2083
|
-
state.progressWatches[index] = {
|
|
2084
|
-
...watch,
|
|
2085
|
-
workerAlias: newAlias,
|
|
2086
|
-
updatedAt: now.toISOString(),
|
|
2087
|
-
};
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2250
|
+
renameProgressWatchAlias(state, alias, newAlias);
|
|
2090
2251
|
});
|
|
2091
2252
|
}
|
|
2092
2253
|
async resolveRoute(alias) {
|
|
@@ -2462,32 +2623,42 @@ export class GatewayStore {
|
|
|
2462
2623
|
.sort((left, right) => pairKey(left.claudeAlias, left.codexAlias).localeCompare(pairKey(right.claudeAlias, right.codexAlias)));
|
|
2463
2624
|
});
|
|
2464
2625
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
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.");
|
|
2626
|
+
/** Resolve only an exact live watch; recovery may substitute its suffix. */
|
|
2627
|
+
async resolveProgressWatchDispatch(input) {
|
|
2628
|
+
return this.mutex.run("gateway", async () => {
|
|
2629
|
+
if (!ALIAS_PATTERN.test(input.sourceAlias) ||
|
|
2630
|
+
!ALIAS_PATTERN.test(input.targetAlias) ||
|
|
2631
|
+
input.sourceAlias === input.targetAlias ||
|
|
2632
|
+
(input.conversationId === undefined) ===
|
|
2633
|
+
(input.recoveredConversationIdSuffix === undefined) ||
|
|
2634
|
+
(input.conversationId !== undefined &&
|
|
2635
|
+
!CONVERSATION_ID_PATTERN.test(input.conversationId)) ||
|
|
2636
|
+
(input.recoveredConversationIdSuffix !== undefined &&
|
|
2637
|
+
!CONVERSATION_SUFFIX_PATTERN.test(input.recoveredConversationIdSuffix))) {
|
|
2638
|
+
throw new BridgeError("INVALID_PROGRESS_WATCH", "The progress-watch dispatch lookup is malformed.");
|
|
2489
2639
|
}
|
|
2490
|
-
|
|
2640
|
+
const state = this.requireState();
|
|
2641
|
+
const watch = state.progressWatches.find((watch) => (input.conversationId !== undefined
|
|
2642
|
+
? watch.conversationId === input.conversationId
|
|
2643
|
+
: watch.conversationId.endsWith(input.recoveredConversationIdSuffix)) &&
|
|
2644
|
+
((watch.ownerAlias === input.sourceAlias &&
|
|
2645
|
+
watch.workerAlias === input.targetAlias) ||
|
|
2646
|
+
(watch.ownerAlias === input.targetAlias &&
|
|
2647
|
+
watch.workerAlias === input.sourceAlias)));
|
|
2648
|
+
if (watch === undefined)
|
|
2649
|
+
return { markerActive: false };
|
|
2650
|
+
const hasExactPair = state.pairs.some((candidate) => pairMatchesMessage(candidate, {
|
|
2651
|
+
sourceAlias: input.sourceAlias,
|
|
2652
|
+
targetAlias: input.targetAlias,
|
|
2653
|
+
pair: true,
|
|
2654
|
+
}) && progressWatchMatchesPair(watch, candidate));
|
|
2655
|
+
if (!hasExactPair)
|
|
2656
|
+
return { markerActive: false };
|
|
2657
|
+
return {
|
|
2658
|
+
conversationId: watch.conversationId,
|
|
2659
|
+
markerActive: watch.ownerAlias === input.sourceAlias &&
|
|
2660
|
+
watch.workerAlias === input.targetAlias,
|
|
2661
|
+
};
|
|
2491
2662
|
});
|
|
2492
2663
|
}
|
|
2493
2664
|
async inspectCompatibilityAttestation(surface, version) {
|
|
@@ -2533,97 +2704,67 @@ export class GatewayStore {
|
|
|
2533
2704
|
(watch.ownerAlias !== alias && watch.workerAlias !== alias)) {
|
|
2534
2705
|
continue;
|
|
2535
2706
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
at: now.getTime(),
|
|
2539
|
-
});
|
|
2540
|
-
if (transition.state !== null) {
|
|
2541
|
-
state.progressWatches[index] = transition.state;
|
|
2542
|
-
changed += 1;
|
|
2543
|
-
}
|
|
2707
|
+
state.progressWatches[index] = recordProgressWatchActivity(watch, now.getTime());
|
|
2708
|
+
changed += 1;
|
|
2544
2709
|
}
|
|
2545
2710
|
return changed;
|
|
2546
2711
|
});
|
|
2547
2712
|
}
|
|
2548
|
-
async
|
|
2713
|
+
async endProgressWatch(conversationId) {
|
|
2549
2714
|
return this.mutate(async (state, now) => {
|
|
2550
|
-
|
|
2715
|
+
if (!CONVERSATION_ID_PATTERN.test(conversationId)) {
|
|
2716
|
+
throw new BridgeError("INVALID_PROGRESS_WATCH", "The progress-watch conversation token is malformed.");
|
|
2717
|
+
}
|
|
2718
|
+
const index = state.progressWatches.findIndex((watch) => watch.conversationId === conversationId);
|
|
2551
2719
|
const watch = state.progressWatches[index];
|
|
2552
2720
|
if (watch === undefined)
|
|
2553
2721
|
return false;
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
? { type: "endpoint_retired", at: now.getTime() }
|
|
2562
|
-
: input.outcome === "disabled"
|
|
2563
|
-
? { type: "disabled", at: now.getTime() }
|
|
2564
|
-
: undefined;
|
|
2565
|
-
if (event === undefined) {
|
|
2566
|
-
throw new BridgeError("INVALID_PROGRESS_WATCH_SETTLEMENT", "Unresponsive settlement is owned only by the watch deadline machine.");
|
|
2567
|
-
}
|
|
2568
|
-
const transition = transitionProgressWatch(watch, event);
|
|
2569
|
-
this.requireProgressWatchJournalCapacity(state, 1);
|
|
2722
|
+
this.appendProgressWatchEvents(state, [
|
|
2723
|
+
progressWatchJournalEvent(watch, now.toISOString(), {
|
|
2724
|
+
kind: "settled",
|
|
2725
|
+
actor: "operator",
|
|
2726
|
+
reason: "untracked",
|
|
2727
|
+
}),
|
|
2728
|
+
]);
|
|
2570
2729
|
state.progressWatches.splice(index, 1);
|
|
2571
|
-
|
|
2572
|
-
kind: input.outcome,
|
|
2573
|
-
timestamp: now.toISOString(),
|
|
2574
|
-
});
|
|
2575
|
-
return transition.state === null;
|
|
2730
|
+
return true;
|
|
2576
2731
|
});
|
|
2577
2732
|
}
|
|
2578
2733
|
/** Advance every due watch once under one durable store mutation. */
|
|
2579
2734
|
async advanceDueProgressWatches() {
|
|
2580
2735
|
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);
|
|
2587
2736
|
const actions = [];
|
|
2588
2737
|
const retained = [];
|
|
2738
|
+
const events = [];
|
|
2589
2739
|
for (const watch of state.progressWatches) {
|
|
2590
2740
|
if (Date.parse(watch.nextActionAt) > now.getTime()) {
|
|
2591
2741
|
retained.push(watch);
|
|
2592
2742
|
continue;
|
|
2593
2743
|
}
|
|
2594
|
-
const
|
|
2595
|
-
|
|
2596
|
-
// A nudge is only reserved here. Its state transition and journal row
|
|
2597
|
-
// commit atomically with the ordinary queued body.
|
|
2598
|
-
if (nudge !== undefined)
|
|
2744
|
+
const due = this.inspectProgressWatchDue(state, watch, now);
|
|
2745
|
+
if (due.kind === "not_due" || due.kind === "nudge") {
|
|
2599
2746
|
retained.push(watch);
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
type: "settled",
|
|
2619
|
-
conversationId: watch.conversationId,
|
|
2620
|
-
ownerAlias: watch.ownerAlias,
|
|
2621
|
-
workerAlias: watch.workerAlias,
|
|
2622
|
-
outcome: effect.outcome,
|
|
2623
|
-
});
|
|
2624
|
-
}
|
|
2747
|
+
}
|
|
2748
|
+
else if (due.kind === "rescheduled") {
|
|
2749
|
+
retained.push(due.watch);
|
|
2750
|
+
}
|
|
2751
|
+
if (due.kind === "nudge") {
|
|
2752
|
+
actions.push({
|
|
2753
|
+
conversationId: watch.conversationId,
|
|
2754
|
+
ownerAlias: watch.ownerAlias,
|
|
2755
|
+
workerAlias: watch.workerAlias,
|
|
2756
|
+
nudgeNumber: due.nudgeNumber,
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
else if (due.kind === "settled") {
|
|
2760
|
+
events.push(progressWatchJournalEvent(watch, now.toISOString(), {
|
|
2761
|
+
kind: "settled",
|
|
2762
|
+
actor: "gateway",
|
|
2763
|
+
reason: "idle_timeout",
|
|
2764
|
+
}));
|
|
2625
2765
|
}
|
|
2626
2766
|
}
|
|
2767
|
+
this.appendProgressWatchEvents(state, events);
|
|
2627
2768
|
state.progressWatches = retained;
|
|
2628
2769
|
return actions;
|
|
2629
2770
|
});
|
|
@@ -2633,21 +2774,17 @@ export class GatewayStore {
|
|
|
2633
2774
|
? watch.nextActionAt
|
|
2634
2775
|
: earliest, undefined));
|
|
2635
2776
|
}
|
|
2636
|
-
async deferProgressWatchNudge(conversationId,
|
|
2777
|
+
async deferProgressWatchNudge(conversationId, nudgeNumber) {
|
|
2637
2778
|
return this.mutate(async (state, now) => {
|
|
2638
2779
|
const index = state.progressWatches.findIndex((watch) => watch.conversationId === conversationId);
|
|
2639
2780
|
const watch = state.progressWatches[index];
|
|
2640
2781
|
if (watch === undefined)
|
|
2641
2782
|
return false;
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2644
|
-
type: "nudge_deferred",
|
|
2645
|
-
at: now.getTime(),
|
|
2646
|
-
retryAt: now.getTime() + boundedDelay,
|
|
2647
|
-
});
|
|
2648
|
-
if (transition.state === null)
|
|
2783
|
+
const due = this.inspectProgressWatchDue(state, watch, now);
|
|
2784
|
+
if (due.kind !== "nudge" || due.nudgeNumber !== nudgeNumber) {
|
|
2649
2785
|
return false;
|
|
2650
|
-
|
|
2786
|
+
}
|
|
2787
|
+
state.progressWatches[index] = deferProgressWatchNudge(watch, now.getTime());
|
|
2651
2788
|
return true;
|
|
2652
2789
|
});
|
|
2653
2790
|
}
|
|
@@ -2686,10 +2823,9 @@ export class GatewayStore {
|
|
|
2686
2823
|
return this.mutate(async (state, now) => {
|
|
2687
2824
|
const { claude, codex } = this.requirePairRoutes(state, input, false);
|
|
2688
2825
|
const pair = this.requireExactPair(state, claude, codex);
|
|
2689
|
-
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => progressWatchMatchesPair(watch, pair)).length);
|
|
2690
2826
|
const plan = this.validateAffectedPairInFlightSettlements(state, pair, input.inFlightSettlements ?? []);
|
|
2691
|
-
const settlements = this.terminateAffectedPairMessages(state, pair, now, "PAIR_REMOVED", plan);
|
|
2692
2827
|
this.settleProgressWatchesForPair(state, pair, now);
|
|
2828
|
+
const settlements = this.terminateAffectedPairMessages(state, pair, now, "PAIR_REMOVED", plan);
|
|
2693
2829
|
state.pairs = state.pairs.filter((candidate) => candidate !== pair);
|
|
2694
2830
|
state.dedupe = state.dedupe.filter((record) => !pairMatchesMessage(pair, record));
|
|
2695
2831
|
return {
|
|
@@ -2848,6 +2984,10 @@ export class GatewayStore {
|
|
|
2848
2984
|
const metadata = state.queue[index];
|
|
2849
2985
|
if (!metadata)
|
|
2850
2986
|
return undefined;
|
|
2987
|
+
const queuedAhead = state.queue
|
|
2988
|
+
.slice(0, index)
|
|
2989
|
+
.filter((candidate) => candidate.targetAlias === metadata.targetAlias)
|
|
2990
|
+
.length;
|
|
2851
2991
|
state.queue.splice(index, 1);
|
|
2852
2992
|
state.accounting.queuedBytes -= metadata.bytes;
|
|
2853
2993
|
const target = state.routes.find((route) => route.alias === metadata.targetAlias);
|
|
@@ -2875,7 +3015,11 @@ export class GatewayStore {
|
|
|
2875
3015
|
...(metadata.steer === true ? { steer: true } : {}),
|
|
2876
3016
|
latencyMs: Math.max(0, now.getTime() - Date.parse(metadata.enqueuedAt)),
|
|
2877
3017
|
});
|
|
2878
|
-
return {
|
|
3018
|
+
return {
|
|
3019
|
+
...metadata,
|
|
3020
|
+
body,
|
|
3021
|
+
...(queuedAhead === 0 ? {} : { queuedAhead }),
|
|
3022
|
+
};
|
|
2879
3023
|
});
|
|
2880
3024
|
}
|
|
2881
3025
|
/** Controller-private shutdown inventory; full IDs never cross control output. */
|
|
@@ -3142,26 +3286,20 @@ export class GatewayStore {
|
|
|
3142
3286
|
conversationIdSuffix: watch.conversationId.slice(-8),
|
|
3143
3287
|
ownerAlias: watch.ownerAlias,
|
|
3144
3288
|
workerAlias: watch.workerAlias,
|
|
3145
|
-
phase: watch.phase,
|
|
3146
|
-
capability: watch.capability,
|
|
3147
3289
|
lastActivityAt: watch.lastActivityAt,
|
|
3148
3290
|
nextActionAt: watch.nextActionAt,
|
|
3149
|
-
idleMs: watch.idleMs,
|
|
3150
3291
|
nudgeCount: watch.nudgeCount,
|
|
3151
3292
|
}))
|
|
3152
3293
|
.sort((left, right) => `${left.nextActionAt}\0${left.ownerAlias}\0${left.workerAlias}`.localeCompare(`${right.nextActionAt}\0${right.ownerAlias}\0${right.workerAlias}`));
|
|
3153
|
-
const progressWatchEvents = state.progressWatchEvents
|
|
3154
|
-
.filter((event) => event.kind !== "activity")
|
|
3155
|
-
.map((event) => ({
|
|
3294
|
+
const progressWatchEvents = state.progressWatchEvents.map((event) => ({
|
|
3156
3295
|
sequence: event.sequence,
|
|
3157
3296
|
timestamp: event.timestamp,
|
|
3158
3297
|
conversationIdSuffix: event.conversationId.slice(-8),
|
|
3159
3298
|
ownerAlias: event.ownerAlias,
|
|
3160
3299
|
workerAlias: event.workerAlias,
|
|
3161
3300
|
kind: event.kind,
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
: { nudgeNumber: event.nudgeNumber }),
|
|
3301
|
+
actor: event.actor,
|
|
3302
|
+
...(event.kind === "settled" ? { reason: event.reason } : {}),
|
|
3165
3303
|
}));
|
|
3166
3304
|
const pressureBuckets = [
|
|
3167
3305
|
{ bucket: "under_1m", settled: 0, expired: 0 },
|
|
@@ -3382,8 +3520,7 @@ export class GatewayStore {
|
|
|
3382
3520
|
replaceCodexRouteForSuccession(state, route, identity, now, routeState, compatibility) {
|
|
3383
3521
|
const oldAlias = route.alias;
|
|
3384
3522
|
const affectedAliases = new Set([oldAlias, identity.alias]);
|
|
3385
|
-
this.
|
|
3386
|
-
affectedAliases.has(watch.workerAlias)).length);
|
|
3523
|
+
this.settleProgressWatchesForAliases(state, affectedAliases, now);
|
|
3387
3524
|
route.alias = identity.alias;
|
|
3388
3525
|
route.binding = { ...identity.binding };
|
|
3389
3526
|
route.registrationMode = "explicit_opt_in";
|
|
@@ -3408,7 +3545,6 @@ export class GatewayStore {
|
|
|
3408
3545
|
record.targetAlias !== identity.alias);
|
|
3409
3546
|
state.rateBuckets = state.rateBuckets.filter((bucket) => bucket.sourceAlias !== oldAlias &&
|
|
3410
3547
|
bucket.sourceAlias !== identity.alias);
|
|
3411
|
-
this.settleProgressWatchesForAliases(state, affectedAliases, now);
|
|
3412
3548
|
removePairsForAliases(state, affectedAliases);
|
|
3413
3549
|
}
|
|
3414
3550
|
assertAllowedIdentity(identity) {
|
|
@@ -3663,19 +3799,6 @@ export class GatewayStore {
|
|
|
3663
3799
|
input.progressWatchNudge !== undefined) {
|
|
3664
3800
|
throw new BridgeError("INVALID_PROGRESS_WATCH", "A queued message cannot be both endpoint activity and a controller nudge.");
|
|
3665
3801
|
}
|
|
3666
|
-
this.applyProgressWatchMessageActivity(state, now, input.progressWatch, sides);
|
|
3667
|
-
this.commitProgressWatchNudge(state, now, input.progressWatchNudge, sides);
|
|
3668
|
-
let supersededSettlement;
|
|
3669
|
-
if (superseded !== undefined) {
|
|
3670
|
-
state.queue.splice(oldestSteerIndex, 1);
|
|
3671
|
-
this.transientBodies.delete(superseded.messageId);
|
|
3672
|
-
state.accounting.queuedBytes -= superseded.bytes;
|
|
3673
|
-
const oldestTarget = state.routes.find((route) => route.alias === superseded.targetAlias);
|
|
3674
|
-
if (oldestTarget) {
|
|
3675
|
-
oldestTarget.queueDepth = Math.max(0, oldestTarget.queueDepth - 1);
|
|
3676
|
-
}
|
|
3677
|
-
supersededSettlement = this.finishMetadata(state, superseded, "cancelled", now, "STEER_QUEUE_SUPERSEDED");
|
|
3678
|
-
}
|
|
3679
3802
|
const opaque = this.randomId();
|
|
3680
3803
|
const messageId = `msg_${opaque}`;
|
|
3681
3804
|
if (!MESSAGE_ID_PATTERN.test(messageId)) {
|
|
@@ -3701,6 +3824,19 @@ export class GatewayStore {
|
|
|
3701
3824
|
: {}),
|
|
3702
3825
|
...(input.steer === true ? { steer: true } : {}),
|
|
3703
3826
|
};
|
|
3827
|
+
this.applyProgressWatchMessageActivity(state, now, input.progressWatch, sides);
|
|
3828
|
+
this.commitProgressWatchNudge(state, now, input.progressWatchNudge, sides);
|
|
3829
|
+
let supersededSettlement;
|
|
3830
|
+
if (superseded !== undefined) {
|
|
3831
|
+
state.queue.splice(oldestSteerIndex, 1);
|
|
3832
|
+
this.transientBodies.delete(superseded.messageId);
|
|
3833
|
+
state.accounting.queuedBytes -= superseded.bytes;
|
|
3834
|
+
const oldestTarget = state.routes.find((route) => route.alias === superseded.targetAlias);
|
|
3835
|
+
if (oldestTarget) {
|
|
3836
|
+
oldestTarget.queueDepth = Math.max(0, oldestTarget.queueDepth - 1);
|
|
3837
|
+
}
|
|
3838
|
+
supersededSettlement = this.finishMetadata(state, superseded, "cancelled", now, "STEER_QUEUE_SUPERSEDED");
|
|
3839
|
+
}
|
|
3704
3840
|
state.queue.push(metadata);
|
|
3705
3841
|
this.transientBodies.set(messageId, input.body);
|
|
3706
3842
|
state.accounting.accepted += 1;
|
|
@@ -3901,22 +4037,7 @@ export class GatewayStore {
|
|
|
3901
4037
|
const pair = sides.pair === true
|
|
3902
4038
|
? state.pairs.find((candidate) => pairMatchesMessage(candidate, sides))
|
|
3903
4039
|
: undefined;
|
|
3904
|
-
|
|
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
|
-
}
|
|
4040
|
+
const index = state.progressWatches.findIndex((watch) => watch.conversationId === activity.conversationId);
|
|
3920
4041
|
const existing = state.progressWatches[index];
|
|
3921
4042
|
if (existing !== undefined) {
|
|
3922
4043
|
if (pair === undefined ||
|
|
@@ -3929,32 +4050,22 @@ export class GatewayStore {
|
|
|
3929
4050
|
}
|
|
3930
4051
|
if (activity.openIdleMs !== undefined &&
|
|
3931
4052
|
activity.actorAlias !== existing.ownerAlias) {
|
|
3932
|
-
throw new BridgeError("PROGRESS_WATCH_OWNER_REQUIRED", "
|
|
4053
|
+
throw new BridgeError("PROGRESS_WATCH_OWNER_REQUIRED", "Tracking options persist from the original TRACK; a repeated TRACK from the exact owner refreshes activity without changing them.");
|
|
3933
4054
|
}
|
|
3934
4055
|
if (activity.completionSignal === true) {
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
4056
|
+
this.appendProgressWatchEvents(state, [
|
|
4057
|
+
progressWatchJournalEvent(existing, now.toISOString(), {
|
|
4058
|
+
kind: "settled",
|
|
4059
|
+
actor: activity.actorAlias === existing.ownerAlias
|
|
4060
|
+
? "owner"
|
|
4061
|
+
: "worker",
|
|
4062
|
+
reason: "done",
|
|
4063
|
+
}),
|
|
4064
|
+
]);
|
|
3942
4065
|
state.progressWatches.splice(index, 1);
|
|
3943
|
-
this.appendProgressWatchEvent(state, existing, {
|
|
3944
|
-
kind: activity.actorAlias === existing.ownerAlias
|
|
3945
|
-
? "done"
|
|
3946
|
-
: "worker_reported_complete",
|
|
3947
|
-
timestamp: now.toISOString(),
|
|
3948
|
-
});
|
|
3949
4066
|
return;
|
|
3950
4067
|
}
|
|
3951
|
-
|
|
3952
|
-
type: "activity",
|
|
3953
|
-
at: now.getTime(),
|
|
3954
|
-
});
|
|
3955
|
-
if (transition.state !== null) {
|
|
3956
|
-
state.progressWatches[index] = transition.state;
|
|
3957
|
-
}
|
|
4068
|
+
state.progressWatches[index] = recordProgressWatchActivity(existing, now.getTime());
|
|
3958
4069
|
return;
|
|
3959
4070
|
}
|
|
3960
4071
|
if (activity.openIdleMs === undefined)
|
|
@@ -3965,49 +4076,29 @@ export class GatewayStore {
|
|
|
3965
4076
|
if (pair === undefined) {
|
|
3966
4077
|
throw new BridgeError("PROGRESS_WATCH_EDGE_REQUIRED", "Progress tracking requires one exact paired consent edge.");
|
|
3967
4078
|
}
|
|
3968
|
-
const
|
|
3969
|
-
const worker = state.routes.find((route) => route.alias === sides.targetAlias);
|
|
3970
|
-
if (owner === undefined || worker === undefined) {
|
|
3971
|
-
throw new BridgeError("PROGRESS_WATCH_EDGE_REQUIRED", "Progress tracking requires both exact paired routes to be registered.");
|
|
3972
|
-
}
|
|
3973
|
-
const watch = createProgressWatchMachine({
|
|
4079
|
+
const watch = createProgressWatch({
|
|
3974
4080
|
conversationId: activity.conversationId,
|
|
3975
|
-
ownerAlias:
|
|
3976
|
-
workerAlias:
|
|
3977
|
-
ownerLease:
|
|
3978
|
-
|
|
4081
|
+
ownerAlias: sides.sourceAlias,
|
|
4082
|
+
workerAlias: sides.targetAlias,
|
|
4083
|
+
ownerLease: sides.sourceAlias === pair.claudeAlias
|
|
4084
|
+
? pair.claudeOwnerLease
|
|
4085
|
+
: pair.codexOwnerLease,
|
|
4086
|
+
workerLease: sides.targetAlias === pair.claudeAlias
|
|
4087
|
+
? pair.claudeOwnerLease
|
|
4088
|
+
: pair.codexOwnerLease,
|
|
3979
4089
|
idleMs: activity.openIdleMs,
|
|
3980
4090
|
at: now.getTime(),
|
|
3981
4091
|
});
|
|
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
4092
|
this.installProgressWatch(state, watch, pair, now);
|
|
3986
4093
|
}
|
|
3987
|
-
|
|
4094
|
+
inspectProgressWatchDue(state, watch, now) {
|
|
3988
4095
|
const owner = state.routes.find((route) => route.alias === watch.ownerAlias &&
|
|
3989
4096
|
route.binding.ownerLease === watch.ownerLease);
|
|
3990
4097
|
const worker = state.routes.find((route) => route.alias === watch.workerAlias &&
|
|
3991
4098
|
route.binding.ownerLease === watch.workerLease);
|
|
3992
|
-
|
|
3993
|
-
worker !== undefined &&
|
|
3994
|
-
state.pairs.some((pair) => ((pair.claudeAlias === owner.alias &&
|
|
3995
|
-
pair.codexAlias === worker.alias) ||
|
|
3996
|
-
(pair.claudeAlias === worker.alias &&
|
|
3997
|
-
pair.codexAlias === owner.alias)) &&
|
|
3998
|
-
pair.claudeOwnerLease ===
|
|
3999
|
-
(owner.binding.provider === "claude"
|
|
4000
|
-
? owner.binding.ownerLease
|
|
4001
|
-
: worker.binding.ownerLease) &&
|
|
4002
|
-
pair.codexOwnerLease ===
|
|
4003
|
-
(owner.binding.provider === "codex"
|
|
4004
|
-
? owner.binding.ownerLease
|
|
4005
|
-
: worker.binding.ownerLease));
|
|
4006
|
-
return transitionProgressWatch(watch, {
|
|
4007
|
-
type: "due",
|
|
4099
|
+
return inspectProgressWatchDue(watch, {
|
|
4008
4100
|
at: now.getTime(),
|
|
4009
4101
|
bothIdle: owner?.state === "idle" && worker?.state === "idle",
|
|
4010
|
-
endpointsPresent: pairPresent,
|
|
4011
4102
|
});
|
|
4012
4103
|
}
|
|
4013
4104
|
commitProgressWatchNudge(state, now, nudge, sides) {
|
|
@@ -4020,17 +4111,12 @@ export class GatewayStore {
|
|
|
4020
4111
|
sides.targetAlias !== watch.workerAlias) {
|
|
4021
4112
|
throw new BridgeError("PROGRESS_WATCH_OWNERSHIP_MISMATCH", "The controller nudge no longer matches its exact watch edge.");
|
|
4022
4113
|
}
|
|
4023
|
-
const
|
|
4024
|
-
if (
|
|
4025
|
-
!transition.effects.some((effect) => effect.type === "send_nudge" &&
|
|
4026
|
-
effect.nudgeNumber === nudge.nudgeNumber)) {
|
|
4114
|
+
const due = this.inspectProgressWatchDue(state, watch, now);
|
|
4115
|
+
if (due.kind !== "nudge" || due.nudgeNumber !== nudge.nudgeNumber) {
|
|
4027
4116
|
throw new BridgeError("PROGRESS_WATCH_NUDGE_NOT_DUE", "The requested progress-watch nudge is not currently due.", true);
|
|
4028
4117
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
this.appendProgressWatchEvent(state, transition.state, {
|
|
4032
|
-
kind: "nudge",
|
|
4033
|
-
timestamp: now.toISOString(),
|
|
4118
|
+
state.progressWatches[index] = commitProgressWatchNudge(watch, {
|
|
4119
|
+
at: now.getTime(),
|
|
4034
4120
|
nudgeNumber: nudge.nudgeNumber,
|
|
4035
4121
|
});
|
|
4036
4122
|
}
|
|
@@ -4045,118 +4131,75 @@ export class GatewayStore {
|
|
|
4045
4131
|
state.progressWatches[replacedIndex].ownerAlias !== watch.ownerAlias) {
|
|
4046
4132
|
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
4133
|
}
|
|
4048
|
-
this.requireProgressWatchJournalCapacity(state, replacedIndex < 0 ? 1 : 2);
|
|
4049
4134
|
if (replacedIndex >= 0) {
|
|
4050
4135
|
const replaced = state.progressWatches[replacedIndex];
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4136
|
+
this.appendProgressWatchEvents(state, [
|
|
4137
|
+
progressWatchJournalEvent(replaced, now.toISOString(), {
|
|
4138
|
+
kind: "replaced",
|
|
4139
|
+
actor: "owner",
|
|
4140
|
+
}),
|
|
4141
|
+
]);
|
|
4142
|
+
state.progressWatches[replacedIndex] = watch;
|
|
4143
|
+
return;
|
|
4056
4144
|
}
|
|
4145
|
+
this.appendProgressWatchEvents(state, [
|
|
4146
|
+
progressWatchJournalEvent(watch, now.toISOString(), {
|
|
4147
|
+
kind: "opened",
|
|
4148
|
+
actor: "owner",
|
|
4149
|
+
}),
|
|
4150
|
+
]);
|
|
4057
4151
|
state.progressWatches.push(watch);
|
|
4058
|
-
this.appendProgressWatchEvent(state, watch, {
|
|
4059
|
-
kind: "opened",
|
|
4060
|
-
timestamp: now.toISOString(),
|
|
4061
|
-
});
|
|
4062
4152
|
}
|
|
4063
|
-
|
|
4064
|
-
if (
|
|
4065
|
-
additions < 0 ||
|
|
4066
|
-
state.watchSequence > Number.MAX_SAFE_INTEGER - additions) {
|
|
4153
|
+
appendProgressWatchEvents(state, events) {
|
|
4154
|
+
if (state.watchSequence > Number.MAX_SAFE_INTEGER - events.length) {
|
|
4067
4155
|
throw new BridgeError("PROGRESS_WATCH_SEQUENCE_EXHAUSTED", "The bounded progress-watch journal sequence is exhausted.");
|
|
4068
4156
|
}
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
conversationId: watch.conversationId,
|
|
4077
|
-
ownerAlias: watch.ownerAlias,
|
|
4078
|
-
workerAlias: watch.workerAlias,
|
|
4079
|
-
kind: event.kind,
|
|
4080
|
-
...(event.nudgeNumber === undefined
|
|
4081
|
-
? {}
|
|
4082
|
-
: { nudgeNumber: event.nudgeNumber }),
|
|
4083
|
-
});
|
|
4157
|
+
for (const event of events) {
|
|
4158
|
+
state.watchSequence += 1;
|
|
4159
|
+
state.progressWatchEvents.push({
|
|
4160
|
+
sequence: state.watchSequence,
|
|
4161
|
+
...event,
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4084
4164
|
while (state.progressWatchEvents.length > this.config.limits.eventCapacity) {
|
|
4085
4165
|
state.progressWatchEvents.shift();
|
|
4086
4166
|
}
|
|
4087
4167
|
}
|
|
4088
|
-
settleProgressWatchesForAliases(state, aliases, now) {
|
|
4089
|
-
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => aliases.has(watch.ownerAlias) || aliases.has(watch.workerAlias)).length);
|
|
4168
|
+
settleProgressWatchesForAliases(state, aliases, now, actor = "gateway") {
|
|
4090
4169
|
const retained = [];
|
|
4170
|
+
const events = [];
|
|
4091
4171
|
for (const watch of state.progressWatches) {
|
|
4092
4172
|
if (!aliases.has(watch.ownerAlias) &&
|
|
4093
4173
|
!aliases.has(watch.workerAlias)) {
|
|
4094
4174
|
retained.push(watch);
|
|
4095
4175
|
continue;
|
|
4096
4176
|
}
|
|
4097
|
-
|
|
4098
|
-
kind: "
|
|
4099
|
-
|
|
4100
|
-
|
|
4177
|
+
events.push(progressWatchJournalEvent(watch, now.toISOString(), {
|
|
4178
|
+
kind: "settled",
|
|
4179
|
+
actor,
|
|
4180
|
+
reason: "endpoint_retired",
|
|
4181
|
+
}));
|
|
4101
4182
|
}
|
|
4183
|
+
this.appendProgressWatchEvents(state, events);
|
|
4102
4184
|
state.progressWatches = retained;
|
|
4103
4185
|
}
|
|
4104
4186
|
settleProgressWatchesForPair(state, pair, now) {
|
|
4105
|
-
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => progressWatchMatchesPair(watch, pair)).length);
|
|
4106
4187
|
const retained = [];
|
|
4188
|
+
const events = [];
|
|
4107
4189
|
for (const watch of state.progressWatches) {
|
|
4108
4190
|
if (!progressWatchMatchesPair(watch, pair)) {
|
|
4109
4191
|
retained.push(watch);
|
|
4110
4192
|
continue;
|
|
4111
4193
|
}
|
|
4112
|
-
|
|
4113
|
-
kind: "
|
|
4114
|
-
|
|
4115
|
-
|
|
4194
|
+
events.push(progressWatchJournalEvent(watch, now.toISOString(), {
|
|
4195
|
+
kind: "settled",
|
|
4196
|
+
actor: "operator",
|
|
4197
|
+
reason: "pair_removed",
|
|
4198
|
+
}));
|
|
4116
4199
|
}
|
|
4200
|
+
this.appendProgressWatchEvents(state, events);
|
|
4117
4201
|
state.progressWatches = retained;
|
|
4118
4202
|
}
|
|
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
|
-
}
|
|
4160
4203
|
finishMetadata(state, metadata, deliveryState, now, safeErrorCode) {
|
|
4161
4204
|
const counterKey = deliveryState;
|
|
4162
4205
|
state.accounting[counterKey] += 1;
|
|
@@ -4306,11 +4349,9 @@ export class GatewayStore {
|
|
|
4306
4349
|
const state = this.requireState();
|
|
4307
4350
|
const eventCutoff = now.getTime() - this.config.limits.eventTtlMs;
|
|
4308
4351
|
state.events = state.events
|
|
4309
|
-
.filter((event) => Date.parse(event.timestamp) > eventCutoff)
|
|
4310
|
-
.slice(-this.config.limits.eventCapacity);
|
|
4352
|
+
.filter((event) => Date.parse(event.timestamp) > eventCutoff);
|
|
4311
4353
|
state.progressWatchEvents = state.progressWatchEvents
|
|
4312
|
-
.filter((event) => Date.parse(event.timestamp) > eventCutoff)
|
|
4313
|
-
.slice(-this.config.limits.eventCapacity);
|
|
4354
|
+
.filter((event) => Date.parse(event.timestamp) > eventCutoff);
|
|
4314
4355
|
state.dedupe = state.dedupe
|
|
4315
4356
|
.filter((record) => Date.parse(record.expiresAt) > now.getTime())
|
|
4316
4357
|
.slice(-this.config.limits.dedupeCapacity);
|
|
@@ -4322,37 +4363,13 @@ export class GatewayStore {
|
|
|
4322
4363
|
const state = this.requireState();
|
|
4323
4364
|
this.recoverCodexSuccessionAfterRestart(state, now);
|
|
4324
4365
|
if (this.config.trackingEnabled === false) {
|
|
4325
|
-
this.
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
});
|
|
4331
|
-
}
|
|
4366
|
+
this.appendProgressWatchEvents(state, state.progressWatches.map((watch) => progressWatchJournalEvent(watch, now.toISOString(), {
|
|
4367
|
+
kind: "settled",
|
|
4368
|
+
actor: "gateway",
|
|
4369
|
+
reason: "tracking_disabled",
|
|
4370
|
+
})));
|
|
4332
4371
|
state.progressWatches = [];
|
|
4333
4372
|
}
|
|
4334
|
-
else {
|
|
4335
|
-
this.requireProgressWatchJournalCapacity(state, state.progressWatches.filter((watch) => !watch.degradedNoticeSent).length);
|
|
4336
|
-
}
|
|
4337
|
-
for (let index = 0; index < state.progressWatches.length; index += 1) {
|
|
4338
|
-
const watch = state.progressWatches[index];
|
|
4339
|
-
if (watch === undefined)
|
|
4340
|
-
continue;
|
|
4341
|
-
const transition = transitionProgressWatch(watch, {
|
|
4342
|
-
type: "restart",
|
|
4343
|
-
at: now.getTime(),
|
|
4344
|
-
conversationCapabilityRestored: false,
|
|
4345
|
-
});
|
|
4346
|
-
if (transition.state !== null) {
|
|
4347
|
-
state.progressWatches[index] = transition.state;
|
|
4348
|
-
}
|
|
4349
|
-
if (transition.effects.some((effect) => effect.type === "record_capability_degraded")) {
|
|
4350
|
-
this.appendProgressWatchEvent(state, transition.state ?? watch, {
|
|
4351
|
-
kind: "capability_degraded",
|
|
4352
|
-
timestamp: now.toISOString(),
|
|
4353
|
-
});
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
4373
|
for (const connector of state.connectors) {
|
|
4357
4374
|
connector.health = "offline";
|
|
4358
4375
|
connector.compatibility = "expired";
|
|
@@ -4417,9 +4434,6 @@ export class GatewayStore {
|
|
|
4417
4434
|
aggregateHealth(connectors) {
|
|
4418
4435
|
if (connectors.length === 0)
|
|
4419
4436
|
return "offline";
|
|
4420
|
-
if (connectors.some((connector) => connector.health === "incompatible")) {
|
|
4421
|
-
return "incompatible";
|
|
4422
|
-
}
|
|
4423
4437
|
if (connectors.some((connector) => connector.health === "degraded")) {
|
|
4424
4438
|
return "degraded";
|
|
4425
4439
|
}
|
|
@@ -4636,7 +4650,7 @@ export class GatewayStore {
|
|
|
4636
4650
|
// Never remove a lock that cannot be proven to belong to this instance.
|
|
4637
4651
|
}
|
|
4638
4652
|
}
|
|
4639
|
-
async loadStateFile() {
|
|
4653
|
+
async loadStateFile(now) {
|
|
4640
4654
|
let body;
|
|
4641
4655
|
try {
|
|
4642
4656
|
body = await this.readPrivateFile(this.stateFilePath, GATEWAY_MAX_STATE_FILE_BYTES);
|
|
@@ -4663,7 +4677,7 @@ export class GatewayStore {
|
|
|
4663
4677
|
parsed = migratePreCodexOrphanRemovalJournal(parsed);
|
|
4664
4678
|
parsed = migrateLegacyHopCounts(parsed);
|
|
4665
4679
|
parsed = migrateLegacyCompatibilityCertifications(parsed);
|
|
4666
|
-
parsed =
|
|
4680
|
+
parsed = migrateV14ProgressWatchState(parsed, now.toISOString());
|
|
4667
4681
|
if (!isPersistedState(parsed)) {
|
|
4668
4682
|
throw new BridgeError("CORRUPT_GATEWAY_STATE", "The gateway controller state failed strict schema validation.");
|
|
4669
4683
|
}
|
|
@@ -4674,8 +4688,6 @@ export class GatewayStore {
|
|
|
4674
4688
|
parsed.rateBuckets.length > this.config.limits.maxRoutes ||
|
|
4675
4689
|
parsed.progressWatches.length >
|
|
4676
4690
|
(this.config.limits.maxWatches ?? PROGRESS_WATCH_DEFAULT_CAPACITY) ||
|
|
4677
|
-
parsed.progressWatchEvents.length > this.config.limits.eventCapacity ||
|
|
4678
|
-
parsed.events.length > this.config.limits.eventCapacity ||
|
|
4679
4691
|
parsed.dedupe.length > this.config.limits.dedupeCapacity ||
|
|
4680
4692
|
parsed.queue.length > this.config.limits.maxQueueMessages ||
|
|
4681
4693
|
parsed.inFlight.length > this.config.limits.maxInFlightMessages ||
|
|
@@ -4742,7 +4754,7 @@ export class GatewayStore {
|
|
|
4742
4754
|
throw error;
|
|
4743
4755
|
let installed;
|
|
4744
4756
|
try {
|
|
4745
|
-
installed = await this.loadStateFile();
|
|
4757
|
+
installed = await this.loadStateFile(this.now());
|
|
4746
4758
|
}
|
|
4747
4759
|
catch {
|
|
4748
4760
|
// Rename crossed the commit point. Keep the intended in-memory
|