@remnic/core 9.29.0 → 9.29.1
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/dist/access-admin-ops-surface.js +7 -7
- package/dist/access-authorization-probe.js +8 -8
- package/dist/access-boundary.js +7 -7
- package/dist/access-cli.js +27 -27
- package/dist/access-http.js +12 -12
- package/dist/access-identity-continuity-surface.js +7 -7
- package/dist/access-lcm-surface.js +7 -7
- package/dist/access-mcp.js +10 -10
- package/dist/access-namespace-preflight.js +7 -7
- package/dist/access-observe-write-surface.js +7 -7
- package/dist/access-operations-batch.js +8 -8
- package/dist/access-operations.js +9 -9
- package/dist/access-recall-concurrency.js +7 -7
- package/dist/access-recall-response.js +7 -7
- package/dist/access-recall-surface.js +7 -7
- package/dist/access-schema.d.ts +7 -7
- package/dist/access-service.js +7 -7
- package/dist/briefing.js +2 -2
- package/dist/causal-consolidation.js +3 -3
- package/dist/{chunk-2HNUMF5V.js → chunk-2AXMQWCN.js} +2 -2
- package/dist/{chunk-NBDCZWP4.js → chunk-2D7FEUCK.js} +2 -2
- package/dist/{chunk-Q6VFVAKZ.js → chunk-2GXM5N5A.js} +2 -2
- package/dist/{chunk-SG265CVF.js → chunk-35DL7IOI.js} +5 -5
- package/dist/{chunk-EVX44I7C.js → chunk-3JNDOAQ2.js} +2 -2
- package/dist/{chunk-QAOGGNE3.js → chunk-5QDRGWT2.js} +2 -2
- package/dist/{chunk-P3MH72CI.js → chunk-5ZCUDWD4.js} +2 -2
- package/dist/{chunk-VVIW3PTA.js → chunk-7L45JFPW.js} +3 -3
- package/dist/{chunk-ESBQWC3U.js → chunk-CW4AGY3G.js} +2 -2
- package/dist/{chunk-ZXAGJFHJ.js → chunk-E5OBR6CJ.js} +2 -2
- package/dist/{chunk-QMBNMPQM.js → chunk-FUYEVEX7.js} +15 -15
- package/dist/{chunk-KUJQHXPH.js → chunk-FZRQZW4H.js} +2 -2
- package/dist/{chunk-IMA2EWLV.js → chunk-GAWOEQEQ.js} +2 -2
- package/dist/{chunk-4PBVXX7L.js → chunk-HKJCMOT6.js} +2 -2
- package/dist/{chunk-CMRQ34J5.js → chunk-IAEAFLF2.js} +2 -2
- package/dist/{chunk-YGHMSDLK.js → chunk-IGPEJVBY.js} +2 -2
- package/dist/{chunk-FCDWWCLX.js → chunk-JMQUBRV6.js} +88 -25
- package/dist/chunk-JMQUBRV6.js.map +1 -0
- package/dist/{chunk-WZ7UB7XQ.js → chunk-JP3YPDAW.js} +13 -13
- package/dist/{chunk-GGUBDXRR.js → chunk-OP6F6L3S.js} +2 -2
- package/dist/{chunk-JDZRYH2N.js → chunk-OYJKSZIQ.js} +2 -2
- package/dist/{chunk-2XC73SHY.js → chunk-PBUZNGMX.js} +5 -5
- package/dist/{chunk-WONS4RZ2.js → chunk-PTSNLEXK.js} +2 -2
- package/dist/{chunk-XEQ7OT7U.js → chunk-QDKK63Y7.js} +2 -2
- package/dist/{chunk-ZGOQTOYA.js → chunk-UBKEQOTD.js} +2 -2
- package/dist/{chunk-T3GCXULH.js → chunk-VSURVXKY.js} +6 -6
- package/dist/{chunk-CSMCNOPH.js → chunk-WRFBW2TT.js} +3 -3
- package/dist/{chunk-G6LBWZRF.js → chunk-WU7TTVOD.js} +5 -5
- package/dist/cli.js +21 -21
- package/dist/compounding/engine.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +2 -2
- package/dist/connectors/index.js +2 -2
- package/dist/corpus-watermark.js +4 -4
- package/dist/entity-retrieval.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +27 -27
- package/dist/maintenance/memory-governance.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +3 -3
- package/dist/namespaces/migrate.js +3 -3
- package/dist/namespaces/storage.js +2 -2
- package/dist/operator-doctor-corpus.js +5 -5
- package/dist/operator-toolkit.js +8 -8
- package/dist/orchestration/maintenance.js +7 -7
- package/dist/orchestrator.js +27 -27
- package/dist/schemas.d.ts +22 -22
- package/dist/semantic-consolidation.js +3 -3
- package/dist/semantic-rule-promotion.js +2 -2
- package/dist/semantic-rule-verifier.js +2 -2
- package/dist/storage.js +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +2 -2
- package/package.json +2 -2
- package/src/storage/entity-canonical-id-migration.ts +151 -23
- package/dist/chunk-FCDWWCLX.js.map +0 -1
- /package/dist/{chunk-2HNUMF5V.js.map → chunk-2AXMQWCN.js.map} +0 -0
- /package/dist/{chunk-NBDCZWP4.js.map → chunk-2D7FEUCK.js.map} +0 -0
- /package/dist/{chunk-Q6VFVAKZ.js.map → chunk-2GXM5N5A.js.map} +0 -0
- /package/dist/{chunk-SG265CVF.js.map → chunk-35DL7IOI.js.map} +0 -0
- /package/dist/{chunk-EVX44I7C.js.map → chunk-3JNDOAQ2.js.map} +0 -0
- /package/dist/{chunk-QAOGGNE3.js.map → chunk-5QDRGWT2.js.map} +0 -0
- /package/dist/{chunk-P3MH72CI.js.map → chunk-5ZCUDWD4.js.map} +0 -0
- /package/dist/{chunk-VVIW3PTA.js.map → chunk-7L45JFPW.js.map} +0 -0
- /package/dist/{chunk-ESBQWC3U.js.map → chunk-CW4AGY3G.js.map} +0 -0
- /package/dist/{chunk-ZXAGJFHJ.js.map → chunk-E5OBR6CJ.js.map} +0 -0
- /package/dist/{chunk-QMBNMPQM.js.map → chunk-FUYEVEX7.js.map} +0 -0
- /package/dist/{chunk-KUJQHXPH.js.map → chunk-FZRQZW4H.js.map} +0 -0
- /package/dist/{chunk-IMA2EWLV.js.map → chunk-GAWOEQEQ.js.map} +0 -0
- /package/dist/{chunk-4PBVXX7L.js.map → chunk-HKJCMOT6.js.map} +0 -0
- /package/dist/{chunk-CMRQ34J5.js.map → chunk-IAEAFLF2.js.map} +0 -0
- /package/dist/{chunk-YGHMSDLK.js.map → chunk-IGPEJVBY.js.map} +0 -0
- /package/dist/{chunk-WZ7UB7XQ.js.map → chunk-JP3YPDAW.js.map} +0 -0
- /package/dist/{chunk-GGUBDXRR.js.map → chunk-OP6F6L3S.js.map} +0 -0
- /package/dist/{chunk-JDZRYH2N.js.map → chunk-OYJKSZIQ.js.map} +0 -0
- /package/dist/{chunk-2XC73SHY.js.map → chunk-PBUZNGMX.js.map} +0 -0
- /package/dist/{chunk-WONS4RZ2.js.map → chunk-PTSNLEXK.js.map} +0 -0
- /package/dist/{chunk-XEQ7OT7U.js.map → chunk-QDKK63Y7.js.map} +0 -0
- /package/dist/{chunk-ZGOQTOYA.js.map → chunk-UBKEQOTD.js.map} +0 -0
- /package/dist/{chunk-T3GCXULH.js.map → chunk-VSURVXKY.js.map} +0 -0
- /package/dist/{chunk-CSMCNOPH.js.map → chunk-WRFBW2TT.js.map} +0 -0
- /package/dist/{chunk-G6LBWZRF.js.map → chunk-WU7TTVOD.js.map} +0 -0
|
@@ -15,6 +15,7 @@ import { readFileSync } from "node:fs";
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import { computeSupersessionKey, normalizeSupersessionKey } from "../temporal-supersession.js";
|
|
17
17
|
import type { EntityFile, MemoryFile } from "../types.js";
|
|
18
|
+
import { log } from "../logger.js";
|
|
18
19
|
import { isErrnoCode } from "../utils/errno.js";
|
|
19
20
|
import { withHeldFileLock } from "../utils/serialize-mutations.js";
|
|
20
21
|
import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
|
|
@@ -69,6 +70,14 @@ const EntityCanonicalIdMigrationStateSchema = z.object({
|
|
|
69
70
|
version: z.literal(ENTITY_CANONICAL_ID_MIGRATION_VERSION),
|
|
70
71
|
complete: z.boolean(),
|
|
71
72
|
mappings: z.record(z.string().min(1)),
|
|
73
|
+
/**
|
|
74
|
+
* Pairs the migration refused to resolve, PARKED rather than dropped.
|
|
75
|
+
* Deleting the record would strand the collision: once an operator removes
|
|
76
|
+
* the legacy file, no later scan can rediscover the mapping, so references
|
|
77
|
+
* still naming the legacy id would never be rewritten. Optional so a journal
|
|
78
|
+
* written by an older build still parses.
|
|
79
|
+
*/
|
|
80
|
+
blocked: z.record(z.string().min(1)).optional(),
|
|
72
81
|
});
|
|
73
82
|
|
|
74
83
|
type EntityCanonicalIdMigrationState = z.infer<typeof EntityCanonicalIdMigrationStateSchema>;
|
|
@@ -348,12 +357,28 @@ async function readSafeEntityEntries(entitiesDir: string): Promise<string[]> {
|
|
|
348
357
|
return entries;
|
|
349
358
|
}
|
|
350
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Legacy ids the migration cannot resolve on its own, mapped to the canonical
|
|
362
|
+
* id they collide with. Two entity files describing one entity is an ORDINARY
|
|
363
|
+
* operator state, and picking a winner would silently drop whichever side lost.
|
|
364
|
+
* So the pair is skipped and reported: reads of the legacy id keep working
|
|
365
|
+
* exactly as they did before the migration ran.
|
|
366
|
+
*/
|
|
367
|
+
export type BlockedEntityPairs = Map<string, string>;
|
|
368
|
+
|
|
351
369
|
async function discoverMappings(
|
|
352
370
|
deps: EntityCanonicalIdMigrationDependencies,
|
|
353
371
|
knownMappings: Readonly<Record<string, string>> = {},
|
|
372
|
+
blocked: BlockedEntityPairs = new Map(),
|
|
354
373
|
): Promise<Record<string, string>> {
|
|
355
374
|
const mappings: Record<string, string> = {};
|
|
356
|
-
const canonicalOwners = new Map<string, string>();
|
|
375
|
+
const canonicalOwners = new Map<string, string[]>();
|
|
376
|
+
// Blocks describe THIS scan of the directory, so they are rebuilt per pass.
|
|
377
|
+
// Accumulating them across a run would let a collision another writer has
|
|
378
|
+
// since resolved keep pruning its now-valid mapping: the entity file gets
|
|
379
|
+
// renamed while the pruned mapping skips the reference rewrite, stranding
|
|
380
|
+
// memories on a filename that no longer exists.
|
|
381
|
+
blocked.clear();
|
|
357
382
|
const entityEntries = await readSafeEntityEntries(deps.entitiesDir);
|
|
358
383
|
for (const entry of entityEntries) {
|
|
359
384
|
if (!entry.endsWith(".md")) continue;
|
|
@@ -386,18 +411,23 @@ async function discoverMappings(
|
|
|
386
411
|
if (canonicalExists) {
|
|
387
412
|
const canonicalContent = await deps.readStorageSecureFile(canonicalPath);
|
|
388
413
|
if (entityContent !== canonicalContent) {
|
|
389
|
-
|
|
414
|
+
blocked.set(legacyId, canonicalId);
|
|
415
|
+
continue;
|
|
390
416
|
}
|
|
391
417
|
}
|
|
392
|
-
|
|
393
|
-
if (previousOwner !== undefined && previousOwner !== legacyId) {
|
|
394
|
-
throw new Error(
|
|
395
|
-
`Cannot migrate legacy entity ids ${previousOwner} and ${legacyId}: both normalize to ${canonicalId}.`,
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
canonicalOwners.set(canonicalId, legacyId);
|
|
418
|
+
canonicalOwners.set(canonicalId, [...(canonicalOwners.get(canonicalId) ?? []), legacyId]);
|
|
399
419
|
mappings[legacyId] = canonicalId;
|
|
400
420
|
}
|
|
421
|
+
// A canonical id claimed by more than one legacy file has no non-arbitrary
|
|
422
|
+
// winner, and readdir order is not stable (§6) - so the "winner" would differ
|
|
423
|
+
// between hosts reading identical data. Block every claimant instead.
|
|
424
|
+
for (const [canonicalId, claimants] of canonicalOwners) {
|
|
425
|
+
if (claimants.length < 2) continue;
|
|
426
|
+
for (const legacyId of claimants) {
|
|
427
|
+
blocked.set(legacyId, canonicalId);
|
|
428
|
+
delete mappings[legacyId];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
401
431
|
return mappings;
|
|
402
432
|
}
|
|
403
433
|
|
|
@@ -645,7 +675,19 @@ function mergeDiscoveredMappings(
|
|
|
645
675
|
for (const [legacyId, canonicalId] of Object.entries(discovered)) {
|
|
646
676
|
const previousMapping = merged[legacyId];
|
|
647
677
|
if (previousMapping !== undefined && previousMapping !== canonicalId) {
|
|
648
|
-
|
|
678
|
+
// A -> B collapses to A -> C once B -> C exists, so a rescan that still
|
|
679
|
+
// reads A -> B off disk is reporting the same chain, not a new target.
|
|
680
|
+
// Only a target the chain cannot reach is a real conflict.
|
|
681
|
+
let hop: string | undefined = canonicalId;
|
|
682
|
+
const seen = new Set<string>();
|
|
683
|
+
while (hop !== undefined && hop !== previousMapping && !seen.has(hop)) {
|
|
684
|
+
seen.add(hop);
|
|
685
|
+
hop = merged[hop];
|
|
686
|
+
}
|
|
687
|
+
if (hop !== previousMapping) {
|
|
688
|
+
throw new Error(`Legacy entity id ${legacyId} changed canonical target during migration.`);
|
|
689
|
+
}
|
|
690
|
+
continue;
|
|
649
691
|
}
|
|
650
692
|
if (previousMapping === undefined && wouldCreateMappingCycle(merged, legacyId, canonicalId)) continue;
|
|
651
693
|
const previousOwner = canonicalOwners.get(canonicalId);
|
|
@@ -681,6 +723,8 @@ function collapseMappings(mappings: Readonly<Record<string, string>>): Record<st
|
|
|
681
723
|
type EntityFileMigrationResult = {
|
|
682
724
|
deferred: boolean;
|
|
683
725
|
progressed: boolean;
|
|
726
|
+
/** The pair collided on content; skipped and reported, never migrated. */
|
|
727
|
+
blocked?: boolean;
|
|
684
728
|
};
|
|
685
729
|
|
|
686
730
|
async function migrateEntityFilePair(
|
|
@@ -711,7 +755,9 @@ async function migrateEntityFilePair(
|
|
|
711
755
|
deps.isEncryptedStorageFile(canonicalPath),
|
|
712
756
|
]);
|
|
713
757
|
if (legacyContent !== canonicalContent) {
|
|
714
|
-
|
|
758
|
+
// A mapping persisted by an older build, or a file changed since
|
|
759
|
+
// discovery. Same verdict as discovery: skip the pair, keep both files.
|
|
760
|
+
return { deferred: false, progressed: false, blocked: true };
|
|
715
761
|
}
|
|
716
762
|
if (legacyEncrypted && !canonicalEncrypted) {
|
|
717
763
|
if (!(await refreshMigrationLock()) || !(await refreshEntityLock())) {
|
|
@@ -792,6 +838,77 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
792
838
|
mappings: {},
|
|
793
839
|
};
|
|
794
840
|
}
|
|
841
|
+
const blocked: BlockedEntityPairs = new Map();
|
|
842
|
+
// ONE aggregated line per run, not one per pair per restart: this used to
|
|
843
|
+
// abort the daemon, so the operator needs the whole list and the remedy.
|
|
844
|
+
const finish = (): Promise<string | undefined> | string | undefined => {
|
|
845
|
+
if (blocked.size > 0) {
|
|
846
|
+
const pairs = [...blocked].map(([legacyId, canonicalId]) => `${legacyId} -> ${canonicalId}`).join(", ");
|
|
847
|
+
log.warn(
|
|
848
|
+
`entity canonical-id migration skipped ${blocked.size} unresolvable pair(s): ${pairs}. `
|
|
849
|
+
+ "Both files were kept and the legacy id still resolves; merge or delete one side to migrate it.",
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return deps.readMigrationFingerprint?.();
|
|
853
|
+
};
|
|
854
|
+
/**
|
|
855
|
+
* Drop blocked pairs from the working set. Discovery only refuses to
|
|
856
|
+
* ADD them; a mapping persisted by an earlier run would otherwise still
|
|
857
|
+
* be applied - migrating one claimant and rewriting every reference onto
|
|
858
|
+
* the canonical file, i.e. picking the winner this fix exists to avoid,
|
|
859
|
+
* off stale state rather than what is on disk.
|
|
860
|
+
*/
|
|
861
|
+
/** @returns true when a park was promoted back into the mapping set. */
|
|
862
|
+
const pruneBlocked = async (): Promise<boolean> => {
|
|
863
|
+
const current = state;
|
|
864
|
+
if (!current) return false;
|
|
865
|
+
// THIS scan's verdict wins over the journal: normalization can change
|
|
866
|
+
// between runs, so a stale parked target would otherwise be promoted
|
|
867
|
+
// later and rewrite references onto an id that does not exist.
|
|
868
|
+
let revived = false;
|
|
869
|
+
const parked: Record<string, string> = { ...(current.blocked ?? {}) };
|
|
870
|
+
const active: Record<string, string> = {};
|
|
871
|
+
for (const [legacyId, canonicalId] of Object.entries(current.mappings)) {
|
|
872
|
+
if (!blocked.has(legacyId)) active[legacyId] = canonicalId;
|
|
873
|
+
}
|
|
874
|
+
for (const [legacyId, canonicalId] of blocked) parked[legacyId] = canonicalId;
|
|
875
|
+
// A parked pair whose legacy file is gone was resolved by keeping the
|
|
876
|
+
// canonical side. The rename is moot; the reference rewrite is not, and
|
|
877
|
+
// this record is the only thing that can still perform it.
|
|
878
|
+
// A park survives ONLY while this scan re-establishes the collision.
|
|
879
|
+
// Anything else - the scan found a real migration, or found the source
|
|
880
|
+
// needs none (already canonical under a newer normalization) - makes
|
|
881
|
+
// the record obsolete, and keeping it lets a later deletion promote a
|
|
882
|
+
// target the entity never occupied.
|
|
883
|
+
for (const [legacyId, canonicalId] of Object.entries(parked)) {
|
|
884
|
+
if (blocked.has(legacyId)) continue;
|
|
885
|
+
delete parked[legacyId];
|
|
886
|
+
if (active[legacyId] !== undefined) continue;
|
|
887
|
+
const legacyPath = deps.resolveEntityFilePath(legacyId);
|
|
888
|
+
if (legacyPath !== null && (await fileExists(legacyPath))) continue;
|
|
889
|
+
// Source gone: the rename is moot, but references still name it and
|
|
890
|
+
// this record is the only thing that can still rewrite them.
|
|
891
|
+
active[legacyId] = canonicalId;
|
|
892
|
+
revived = true;
|
|
893
|
+
}
|
|
894
|
+
// Reviving a whole chain at once yields A -> B -> C, and each rewrite
|
|
895
|
+
// surface makes a different number of passes over it - so collapse
|
|
896
|
+
// transitively before anything consumes the set.
|
|
897
|
+
const collapsed = collapseMappings(active);
|
|
898
|
+
// Parked targets follow ACTIVE moves only: hopping over a still-blocked
|
|
899
|
+
// link would jump to an id the migration has not been allowed to reach.
|
|
900
|
+
for (const [legacyId, canonicalId] of Object.entries(parked)) {
|
|
901
|
+
const moved = collapsed[canonicalId];
|
|
902
|
+
if (moved !== undefined && moved !== legacyId) parked[legacyId] = moved;
|
|
903
|
+
}
|
|
904
|
+
const same = (a: Record<string, string>, b: Record<string, string>): boolean =>
|
|
905
|
+
Object.keys(a).length === Object.keys(b).length
|
|
906
|
+
&& Object.entries(a).every(([key, value]) => b[key] === value);
|
|
907
|
+
if (same(collapsed, current.mappings) && same(parked, current.blocked ?? {})) return revived;
|
|
908
|
+
state = { ...current, mappings: collapsed, blocked: parked };
|
|
909
|
+
await writeState(deps, state);
|
|
910
|
+
return revived;
|
|
911
|
+
};
|
|
795
912
|
const previousMappings = state.mappings;
|
|
796
913
|
const collapsedMappings = collapseMappings(previousMappings);
|
|
797
914
|
const mappingsChanged =
|
|
@@ -804,11 +921,12 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
804
921
|
if (!(await lock.refresh())) throw new Error("Lost entity canonical-id migration lock.");
|
|
805
922
|
const discoveredBefore = rejectMappingCycles(
|
|
806
923
|
state.mappings,
|
|
807
|
-
await discoverMappings(deps, state.mappings),
|
|
924
|
+
await discoverMappings(deps, state.mappings, blocked),
|
|
808
925
|
);
|
|
926
|
+
await pruneBlocked();
|
|
809
927
|
const mergedBefore = collapseMappings(mergeDiscoveredMappings(state.mappings, discoveredBefore));
|
|
810
928
|
if (state.complete && Object.keys(discoveredBefore).length === 0 && !mappingsChanged) {
|
|
811
|
-
if (Object.keys(state.mappings).length === 0) return
|
|
929
|
+
if (Object.keys(state.mappings).length === 0) return finish();
|
|
812
930
|
const rewroteColdMemory = await rewriteKnownReferences(
|
|
813
931
|
deps,
|
|
814
932
|
state.mappings,
|
|
@@ -818,7 +936,7 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
818
936
|
deps.invalidateAllMemoriesCache();
|
|
819
937
|
if (rewroteColdMemory) deps.invalidateColdMemoriesCache();
|
|
820
938
|
deps.bumpMemoryStatusVersion();
|
|
821
|
-
return
|
|
939
|
+
return finish();
|
|
822
940
|
}
|
|
823
941
|
if (
|
|
824
942
|
state.complete
|
|
@@ -832,11 +950,16 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
832
950
|
if (!(await lock.refresh())) throw new Error("Lost entity canonical-id migration lock.");
|
|
833
951
|
const discoveredAfter = rejectMappingCycles(
|
|
834
952
|
state.mappings,
|
|
835
|
-
await discoverMappings(deps, state.mappings),
|
|
953
|
+
await discoverMappings(deps, state.mappings, blocked),
|
|
836
954
|
);
|
|
837
|
-
|
|
955
|
+
// Reconcile before declaring completion: this rescan can resolve a
|
|
956
|
+
// park (another writer removed the legacy file), and completing
|
|
957
|
+
// without promoting it strands its references behind a fingerprint
|
|
958
|
+
// that already reflects the deletion - so no later run revisits it.
|
|
959
|
+
const revivedInRescan = await pruneBlocked();
|
|
960
|
+
if (Object.keys(discoveredAfter).length === 0 && !revivedInRescan) {
|
|
838
961
|
await writeState(deps, { ...state, complete: true });
|
|
839
|
-
return
|
|
962
|
+
return finish();
|
|
840
963
|
}
|
|
841
964
|
state = {
|
|
842
965
|
...state,
|
|
@@ -857,9 +980,8 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
857
980
|
canonicalId,
|
|
858
981
|
() => lock.refresh(),
|
|
859
982
|
);
|
|
860
|
-
if (result.
|
|
861
|
-
|
|
862
|
-
}
|
|
983
|
+
if (result.blocked) blocked.set(legacyId, canonicalId);
|
|
984
|
+
else if (result.deferred) deferredMappings.push([legacyId, canonicalId]);
|
|
863
985
|
progressed ||= result.progressed;
|
|
864
986
|
}
|
|
865
987
|
if (deferredMappings.length === 0) break;
|
|
@@ -871,6 +993,7 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
871
993
|
}
|
|
872
994
|
pendingMappings = deferredMappings;
|
|
873
995
|
}
|
|
996
|
+
await pruneBlocked();
|
|
874
997
|
|
|
875
998
|
const rewroteColdMemory = await rewriteKnownReferences(
|
|
876
999
|
deps,
|
|
@@ -885,11 +1008,16 @@ export async function migrateLegacyEntityCanonicalIds(
|
|
|
885
1008
|
if (!(await lock.refresh())) throw new Error("Lost entity canonical-id migration lock.");
|
|
886
1009
|
const discoveredAfter = rejectMappingCycles(
|
|
887
1010
|
state.mappings,
|
|
888
|
-
await discoverMappings(deps, state.mappings),
|
|
1011
|
+
await discoverMappings(deps, state.mappings, blocked),
|
|
889
1012
|
);
|
|
890
|
-
|
|
1013
|
+
// A park resolved after the rewrite pass is promoted here, and a
|
|
1014
|
+
// promotion still has to be APPLIED. Completing on `discoveredAfter`
|
|
1015
|
+
// alone fingerprints the post-deletion corpus, so the runner never
|
|
1016
|
+
// retries and those references stay stranded.
|
|
1017
|
+
const revivedAfterRewrite = await pruneBlocked();
|
|
1018
|
+
if (Object.keys(discoveredAfter).length === 0 && !revivedAfterRewrite) {
|
|
891
1019
|
await writeState(deps, { ...state, complete: true });
|
|
892
|
-
return
|
|
1020
|
+
return finish();
|
|
893
1021
|
}
|
|
894
1022
|
state = {
|
|
895
1023
|
...state,
|