@remnic/core 9.3.748 → 9.3.749
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-boundary.d.ts +2 -2
- package/dist/access-cli.js +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-operations.d.ts +2 -2
- package/dist/{access-service-CH1aqqb1.d.ts → access-service-5-EVpt3v.d.ts} +1 -1
- package/dist/access-service.d.ts +2 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-ZWGXZ7ID.js → chunk-PNUOQYRJ.js} +560 -457
- package/dist/chunk-PNUOQYRJ.js.map +1 -0
- package/dist/{cli-CE5olss2.d.ts → cli-DuWWjaKT.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-CIOqdNzF.d.ts → orchestrator-CZI6hO_R.d.ts} +7 -56
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +1 -1
- package/package.json +2 -2
- package/src/orchestration/persistence-index.ts +497 -0
- package/src/orchestrator.ts +69 -365
- package/dist/chunk-ZWGXZ7ID.js.map +0 -1
package/src/orchestrator.ts
CHANGED
|
@@ -114,6 +114,7 @@ import { RecallSearchPipelineCoordinator } from "./orchestration/recall-search-p
|
|
|
114
114
|
import { TurnIngestionCoordinator } from "./orchestration/turn-ingestion.js";
|
|
115
115
|
import { RecallIntrospectionCoordinator } from "./orchestration/recall-introspection.js";
|
|
116
116
|
import { OrchestratorInitCoordinator } from "./orchestration/orchestrator-init.js";
|
|
117
|
+
import { PersistenceIndexCoordinator } from "./orchestration/persistence-index.js";
|
|
117
118
|
export { hasIdentityRecoveryIntent, resolveEffectiveIdentityInjectionMode } from "./orchestration/recall-result-formatter.js";
|
|
118
119
|
import {
|
|
119
120
|
GraphRecallCoordinator,
|
|
@@ -2549,17 +2550,20 @@ export class Orchestrator {
|
|
|
2549
2550
|
targetStorage: StorageManager,
|
|
2550
2551
|
content: string,
|
|
2551
2552
|
): Promise<boolean> {
|
|
2552
|
-
|
|
2553
|
-
|
|
2553
|
+
return this.persistenceIndexCoordinator.hasContentHashDedup(
|
|
2554
|
+
targetStorage,
|
|
2555
|
+
content,
|
|
2556
|
+
);
|
|
2554
2557
|
}
|
|
2555
2558
|
|
|
2556
2559
|
private async addContentHashDedup(
|
|
2557
2560
|
targetStorage: StorageManager,
|
|
2558
2561
|
content: string,
|
|
2559
2562
|
): Promise<void> {
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
+
return this.persistenceIndexCoordinator.addContentHashDedup(
|
|
2564
|
+
targetStorage,
|
|
2565
|
+
content,
|
|
2566
|
+
);
|
|
2563
2567
|
}
|
|
2564
2568
|
|
|
2565
2569
|
private async removeContentHashForMemory(
|
|
@@ -2567,50 +2571,13 @@ export class Orchestrator {
|
|
|
2567
2571
|
memory: MemoryFile,
|
|
2568
2572
|
context: string,
|
|
2569
2573
|
): Promise<void> {
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
index.removeByHash(memory.frontmatter.contentHash);
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
log.warn(
|
|
2579
|
-
`[${context}] removing hash for legacy memory ${memory.frontmatter.id ?? "(unknown)"} via content fallback - no contentHash in frontmatter`,
|
|
2574
|
+
return this.persistenceIndexCoordinator.removeContentHashForMemory(
|
|
2575
|
+
targetStorage,
|
|
2576
|
+
memory,
|
|
2577
|
+
context,
|
|
2580
2578
|
);
|
|
2581
|
-
index.remove(memory.content);
|
|
2582
2579
|
}
|
|
2583
2580
|
|
|
2584
|
-
/**
|
|
2585
|
-
* Issue #1671 — backfill bi-temporal bounds onto an existing promoted/deduped
|
|
2586
|
-
* copy that was written BEFORE the source fact carried a resolved
|
|
2587
|
-
* `invalid_at`/`observedAt`/`eventTimeSource`.
|
|
2588
|
-
*
|
|
2589
|
-
* On re-extraction/backfill, a fact may now carry a resolved end bound (e.g.
|
|
2590
|
-
* "until June 2025") that the existing copy lacks because it was promoted
|
|
2591
|
-
* before bi-temporal wiring existed. Without this backfill, recall keeps
|
|
2592
|
-
* surfacing an expired fact even though the source copy now expires correctly.
|
|
2593
|
-
*
|
|
2594
|
-
* Finds the active fact in `targetStorage` matching `dedupContent`, then
|
|
2595
|
-
* patches the temporal frontmatter the existing copy is missing. Best-effort
|
|
2596
|
-
* / fail-open — any I/O error is logged and swallowed so the dedup
|
|
2597
|
-
* short-circuit is never blocked by a backfill failure.
|
|
2598
|
-
*
|
|
2599
|
-
* Matching: the stored `frontmatter.contentHash` is compared against
|
|
2600
|
-
* `ContentHashIndex.computeHash(dedupContent)` first (the exact hash the
|
|
2601
|
-
* content-hash index uses), then falls back to stripping citations and
|
|
2602
|
-
* comparing normalized bodies. This handles inline-attribution deployments
|
|
2603
|
-
* where the persisted body carries a citation marker the dedup key does not.
|
|
2604
|
-
*
|
|
2605
|
-
* I/O gate: only triggers when `invalidAt` is present (the end bound that
|
|
2606
|
-
* actually changes recall behavior by expiring the fact). `observedAt` and
|
|
2607
|
-
* `eventTimeSource` alone don'\''t expire a fact, so backfilling them without
|
|
2608
|
-
* an end bound would cause a full readAllMemories scan on every dedup hit
|
|
2609
|
-
* under biTemporal for no recall benefit.
|
|
2610
|
-
*
|
|
2611
|
-
* Only patches fields the existing copy LACKS — never overwrites a bound the
|
|
2612
|
-
* copy already carries.
|
|
2613
|
-
*/
|
|
2614
2581
|
private async backfillTemporalBoundsOnDedupHit(
|
|
2615
2582
|
targetStorage: StorageManager,
|
|
2616
2583
|
dedupContent: string,
|
|
@@ -2626,130 +2593,17 @@ export class Orchestrator {
|
|
|
2626
2593
|
},
|
|
2627
2594
|
entityRef?: string,
|
|
2628
2595
|
): Promise<void> {
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
// run a full readAllMemories on every bi-temporal dedup hit for no benefit
|
|
2636
|
-
// (review cursor PRRT_OvHk / codex PRRT_OvHxVH). observedAt and
|
|
2637
|
-
// eventTimeSource alone never change recall.
|
|
2638
|
-
const hasExtractedStart =
|
|
2639
|
-
bounds.validFrom !== undefined && bounds.eventTimeSource === "extracted";
|
|
2640
|
-
if (!bounds.invalidAt && !hasExtractedStart) return;
|
|
2641
|
-
try {
|
|
2642
|
-
const incomingHash = ContentHashIndex.computeHash(dedupContent);
|
|
2643
|
-
const normalizedIncoming = ContentHashIndex.normalizeContent(dedupContent);
|
|
2644
|
-
// Normalize the entity for same-entity scoping when provided — two
|
|
2645
|
-
// entities can share identical fact text, and patching a different
|
|
2646
|
-
// entity'\''s fact would corrupt its temporal bounds (cursor review).
|
|
2647
|
-
const incomingEntityNorm = entityRef
|
|
2648
|
-
? normalizeSupersessionKey(entityRef)
|
|
2649
|
-
: undefined;
|
|
2650
|
-
const all = await targetStorage.readAllMemories();
|
|
2651
|
-
const existing = all.find((m) => {
|
|
2652
|
-
if (m.frontmatter.category !== "fact") return false;
|
|
2653
|
-
if ((m.frontmatter.status ?? "active") !== "active") return false;
|
|
2654
|
-
// Same-entity guard: reject only when the stored fact carries a
|
|
2655
|
-
// DIFFERENT entity (two entities can share identical fact text, so
|
|
2656
|
-
// patching the other entity's copy would corrupt its bounds — codex
|
|
2657
|
-
// P2 PRRT_OvB4A). Legacy facts written before entity linkage (no
|
|
2658
|
-
// entityRef) have no entity to conflict with, so they stay eligible
|
|
2659
|
-
// for backfill (cursor PRRT_OvKnV: the guard must NOT silently
|
|
2660
|
-
// no-op for older promoted copies that predate entity linkage).
|
|
2661
|
-
if (
|
|
2662
|
-
incomingEntityNorm &&
|
|
2663
|
-
m.frontmatter.entityRef &&
|
|
2664
|
-
normalizeSupersessionKey(m.frontmatter.entityRef) !== incomingEntityNorm
|
|
2665
|
-
) {
|
|
2666
|
-
return false;
|
|
2667
|
-
}
|
|
2668
|
-
// Prefer the stored contentHash (what the hash index actually keys
|
|
2669
|
-
// on) — it is computed from contentHashSource (the raw/enriched
|
|
2670
|
-
// body before citation), matching the dedupContent the caller passes.
|
|
2671
|
-
if (m.frontmatter.contentHash) {
|
|
2672
|
-
return m.frontmatter.contentHash === incomingHash;
|
|
2673
|
-
}
|
|
2674
|
-
// Legacy facts without a stored hash: strip citations then compare
|
|
2675
|
-
// normalized bodies so inline-attribution markers don'\''t prevent
|
|
2676
|
-
// a match.
|
|
2677
|
-
return (
|
|
2678
|
-
ContentHashIndex.normalizeContent(
|
|
2679
|
-
stripCitationForTemplate(m.content ?? "", this.config.inlineSourceAttributionFormat),
|
|
2680
|
-
) === normalizedIncoming
|
|
2681
|
-
);
|
|
2682
|
-
});
|
|
2683
|
-
if (!existing) return;
|
|
2684
|
-
// Build a patch containing ONLY the fields the existing copy lacks.
|
|
2685
|
-
const patch: Partial<MemoryFrontmatter> = {};
|
|
2686
|
-
const fm = existing.frontmatter;
|
|
2687
|
-
if (bounds.invalidAt && (!fm.invalid_at || fm.invalid_at.length === 0)) {
|
|
2688
|
-
patch.invalid_at = bounds.invalidAt;
|
|
2689
|
-
}
|
|
2690
|
-
if (bounds.observedAt && (!fm.observedAt || fm.observedAt.length === 0)) {
|
|
2691
|
-
patch.observedAt = bounds.observedAt;
|
|
2692
|
-
}
|
|
2693
|
-
if (
|
|
2694
|
-
bounds.eventTimeSource &&
|
|
2695
|
-
(!fm.eventTimeSource || fm.eventTimeSource.length === 0)
|
|
2696
|
-
) {
|
|
2697
|
-
patch.eventTimeSource = bounds.eventTimeSource;
|
|
2698
|
-
}
|
|
2699
|
-
// #1707 thread 2 — per-fact-anchored start bound. A re-extracted
|
|
2700
|
-
// duplicate whose event time resolves a real start bound must carry
|
|
2701
|
-
// that anchor onto the existing copy so as-of recall uses the corrected
|
|
2702
|
-
// valid_at instead of a stale batch-anchored value. Only an EXTRACTED
|
|
2703
|
-
// bound corrects; an "assumed" bound is just the ingestion anchor.
|
|
2704
|
-
//
|
|
2705
|
-
// No-clobber via equality, not provenance inference: exact-content dedup
|
|
2706
|
-
// re-extracts the SAME event-time expression, which #1670 per-fact
|
|
2707
|
-
// anchoring resolves deterministically to the same validFrom — so for
|
|
2708
|
-
// stable content the incoming validFrom EQUALS the copy's valid_at and
|
|
2709
|
-
// we skip the redundant write (the only no-clobber that holds without a
|
|
2710
|
-
// fragile provenance heuristic — review codex PRRT_Ov7LKC). When they
|
|
2711
|
-
// differ (a prior batch/assumed anchor, end-only assumed start, or a
|
|
2712
|
-
// non-deterministic re-resolution), the extracted validFrom is the
|
|
2713
|
-
// authoritative correction and overwrites. The eventTimeSource upgrade
|
|
2714
|
-
// below records that the start is now extracted-anchored.
|
|
2715
|
-
if (
|
|
2716
|
-
bounds.validFrom &&
|
|
2717
|
-
bounds.eventTimeSource === "extracted" &&
|
|
2718
|
-
fm.valid_at !== bounds.validFrom
|
|
2719
|
-
) {
|
|
2720
|
-
patch.valid_at = bounds.validFrom;
|
|
2721
|
-
// Mark the copy extracted-anchored in the SAME patch so its provenance
|
|
2722
|
-
// reflects the correction (review cursor PRRT_OvHM / codex PRRT_OvHxVD):
|
|
2723
|
-
// without this, a copy upgraded from "assumed" would keep "assumed"
|
|
2724
|
-
// provenance while carrying an extracted start. (The earlier
|
|
2725
|
-
// eventTimeSource block only fills an EMPTY source.)
|
|
2726
|
-
if (fm.eventTimeSource !== "extracted") {
|
|
2727
|
-
patch.eventTimeSource = "extracted";
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
if (Object.keys(patch).length === 0) return;
|
|
2731
|
-
const ok = await targetStorage.writeMemoryFrontmatter(existing, patch);
|
|
2732
|
-
if (ok) {
|
|
2733
|
-
log.debug(
|
|
2734
|
-
`bitemporal-backfill: patched ${Object.keys(patch).join(",")} onto existing fact ${fm.id ?? "(unknown)"} in ${targetStorage.dir}`,
|
|
2735
|
-
);
|
|
2736
|
-
}
|
|
2737
|
-
} catch (err) {
|
|
2738
|
-
log.warn(
|
|
2739
|
-
`bitemporal-backfill: failed open for ${targetStorage.dir}: ${err}`,
|
|
2740
|
-
);
|
|
2741
|
-
}
|
|
2596
|
+
return this.persistenceIndexCoordinator.backfillTemporalBoundsOnDedupHit(
|
|
2597
|
+
targetStorage,
|
|
2598
|
+
dedupContent,
|
|
2599
|
+
bounds,
|
|
2600
|
+
entityRef,
|
|
2601
|
+
);
|
|
2742
2602
|
}
|
|
2743
2603
|
|
|
2744
2604
|
private async saveContentHashIndexes(): Promise<void> {
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
for (const index of this.contentHashIndexesByStorageDir.values()) {
|
|
2748
|
-
indexes.add(index);
|
|
2749
|
-
}
|
|
2750
|
-
for (const index of indexes) {
|
|
2751
|
-
await index.save();
|
|
2752
|
-
}
|
|
2605
|
+
return this.persistenceIndexCoordinator.saveContentHashIndexes(
|
|
2606
|
+
);
|
|
2753
2607
|
}
|
|
2754
2608
|
|
|
2755
2609
|
constructor(config: PluginConfig) {
|
|
@@ -5146,6 +5000,31 @@ export class Orchestrator {
|
|
|
5146
5000
|
return this._orchestratorInitCoordinator;
|
|
5147
5001
|
}
|
|
5148
5002
|
|
|
5003
|
+
/**
|
|
5004
|
+
* Persistence-index coordinator (issue #1526 seam 23). Owns post-persist
|
|
5005
|
+
* bookkeeping (content-hash dedup, temporal indexes, graph edges,
|
|
5006
|
+
* semantic dedup lookup). Lazy + accessor-wired (late-binding rule).
|
|
5007
|
+
*/
|
|
5008
|
+
private _persistenceIndexCoordinator: PersistenceIndexCoordinator | undefined;
|
|
5009
|
+
|
|
5010
|
+
private get persistenceIndexCoordinator(): PersistenceIndexCoordinator {
|
|
5011
|
+
if (!this._persistenceIndexCoordinator) {
|
|
5012
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
5013
|
+
const self = this;
|
|
5014
|
+
this._persistenceIndexCoordinator = new PersistenceIndexCoordinator({
|
|
5015
|
+
get config() { return self.config; },
|
|
5016
|
+
get contentHashIndex() { return self.contentHashIndex; },
|
|
5017
|
+
contentHashIndexForStorage: (targetStorage) => self.contentHashIndexForStorage(targetStorage),
|
|
5018
|
+
get contentHashIndexesByStorageDir() { return self.contentHashIndexesByStorageDir; },
|
|
5019
|
+
get embeddingFallback() { return self.embeddingFallback; },
|
|
5020
|
+
graphIndexFor: (storage) => self.graphIndexFor(storage),
|
|
5021
|
+
readAllMemoriesForNamespaces: (namespaces) => self.readAllMemoriesForNamespaces(namespaces),
|
|
5022
|
+
semanticDedupScopeFor: (targetStorage) => self.semanticDedupScopeFor(targetStorage),
|
|
5023
|
+
});
|
|
5024
|
+
}
|
|
5025
|
+
return this._persistenceIndexCoordinator;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5149
5028
|
private async recallInternal(
|
|
5150
5029
|
prompt: string,
|
|
5151
5030
|
sessionKey?: string,
|
|
@@ -5593,22 +5472,12 @@ export class Orchestrator {
|
|
|
5593
5472
|
storage: StorageManager,
|
|
5594
5473
|
memoryId: string,
|
|
5595
5474
|
): Promise<void> {
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
const memory = await storage.getMemoryById(memoryId);
|
|
5599
|
-
if (!memory) return;
|
|
5600
|
-
await this.embeddingFallback.indexFile(
|
|
5475
|
+
return this.persistenceIndexCoordinator.indexPersistedMemory(
|
|
5476
|
+
storage,
|
|
5601
5477
|
memoryId,
|
|
5602
|
-
memory.content,
|
|
5603
|
-
memory.path,
|
|
5604
5478
|
);
|
|
5605
5479
|
}
|
|
5606
5480
|
|
|
5607
|
-
/**
|
|
5608
|
-
* Build a graph edge for a persisted memory (v8.2).
|
|
5609
|
-
* Shared helper used by both the chunked and non-chunked write paths to avoid duplication.
|
|
5610
|
-
* Fail-open: caller wraps in try/catch.
|
|
5611
|
-
*/
|
|
5612
5481
|
private async buildGraphEdge(
|
|
5613
5482
|
storage: StorageManager,
|
|
5614
5483
|
memoryRelPath: string,
|
|
@@ -5622,65 +5491,19 @@ export class Orchestrator {
|
|
|
5622
5491
|
fallbackCausalPredecessor: string | undefined,
|
|
5623
5492
|
graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.config),
|
|
5624
5493
|
): Promise<void> {
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
try {
|
|
5629
|
-
const allMems = allMemsForGraph ?? [];
|
|
5630
|
-
for (const m of allMems) {
|
|
5631
|
-
if (m.frontmatter.entityRef === entityRef) {
|
|
5632
|
-
const rel = path.relative(storage.dir, m.path);
|
|
5633
|
-
if (rel !== memoryRelPath) entitySiblings.push(rel);
|
|
5634
|
-
}
|
|
5635
|
-
}
|
|
5636
|
-
} catch {
|
|
5637
|
-
/* fail-open */
|
|
5638
|
-
}
|
|
5639
|
-
}
|
|
5640
|
-
// Recent thread memories for time graph
|
|
5641
|
-
const recentInThread: string[] = [];
|
|
5642
|
-
if (threadIdForEdge && threadEpisodeIdsForGraph?.length) {
|
|
5643
|
-
try {
|
|
5644
|
-
recentInThread.push(
|
|
5645
|
-
...resolveRecentThreadMemoryPaths({
|
|
5646
|
-
threadEpisodeIds: threadEpisodeIdsForGraph,
|
|
5647
|
-
currentMemoryId: memoryId,
|
|
5648
|
-
allMemsForGraph,
|
|
5649
|
-
pathById: memoryPathById,
|
|
5650
|
-
storageDir: storage.dir,
|
|
5651
|
-
maxRecent: 3,
|
|
5652
|
-
}),
|
|
5653
|
-
);
|
|
5654
|
-
} catch {
|
|
5655
|
-
/* fail-open */
|
|
5656
|
-
}
|
|
5657
|
-
}
|
|
5658
|
-
if (
|
|
5659
|
-
recentInThread.length === 0 &&
|
|
5660
|
-
graphCaps.graphWriteSessionAdjacency &&
|
|
5661
|
-
fallbackCausalPredecessor &&
|
|
5662
|
-
fallbackCausalPredecessor !== memoryRelPath
|
|
5663
|
-
) {
|
|
5664
|
-
recentInThread.push(fallbackCausalPredecessor);
|
|
5665
|
-
}
|
|
5666
|
-
const causalPredecessor =
|
|
5667
|
-
recentInThread[recentInThread.length - 1] ?? fallbackCausalPredecessor;
|
|
5668
|
-
await this.graphIndexFor(storage).onMemoryWritten({
|
|
5669
|
-
memoryPath: memoryRelPath,
|
|
5494
|
+
return this.persistenceIndexCoordinator.buildGraphEdge(
|
|
5495
|
+
storage,
|
|
5496
|
+
memoryRelPath,
|
|
5670
5497
|
entityRef,
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
causalGraph: graphCaps.causalGraph,
|
|
5681
|
-
multiGraphMemory: graphCaps.multiGraphMemory,
|
|
5682
|
-
},
|
|
5683
|
-
});
|
|
5498
|
+
memoryId,
|
|
5499
|
+
factContent,
|
|
5500
|
+
allMemsForGraph,
|
|
5501
|
+
memoryPathById,
|
|
5502
|
+
threadIdForEdge,
|
|
5503
|
+
threadEpisodeIdsForGraph,
|
|
5504
|
+
fallbackCausalPredecessor,
|
|
5505
|
+
graphCaps,
|
|
5506
|
+
);
|
|
5684
5507
|
}
|
|
5685
5508
|
|
|
5686
5509
|
private graphIndexFor(storage: StorageManager): GraphIndex {
|
|
@@ -5692,89 +5515,14 @@ export class Orchestrator {
|
|
|
5692
5515
|
return created;
|
|
5693
5516
|
}
|
|
5694
5517
|
|
|
5695
|
-
/**
|
|
5696
|
-
* Batch-update temporal and tag indexes after extraction (v8.1).
|
|
5697
|
-
* Reads each persisted memory's path + frontmatter and adds them to
|
|
5698
|
-
* state/index_time.json and state/index_tags.json.
|
|
5699
|
-
* Fail-open: any error is logged but does not abort extraction.
|
|
5700
|
-
*/
|
|
5701
5518
|
private async updateTemporalTagIndexes(
|
|
5702
5519
|
storage: StorageManager,
|
|
5703
5520
|
persistedIds: string[],
|
|
5704
5521
|
): Promise<void> {
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
// Enabling only parallelRetrievalEnabled without queryAwareIndexingEnabled would silently
|
|
5710
|
-
// produce an empty temporal index, leaving the temporal agent with no data to work from.
|
|
5711
|
-
if (
|
|
5712
|
-
!resolveIndexingCapabilities(this.config).queryAwareIndexing &&
|
|
5713
|
-
!caps.parallelRetrieval
|
|
5714
|
-
)
|
|
5715
|
-
return;
|
|
5716
|
-
// Check for missing indexes BEFORE the early-return so first-time enablement
|
|
5717
|
-
// can bootstrap the full corpus even when this extraction turn persisted nothing.
|
|
5718
|
-
const needsFullRebuild = !indexesExist(this.config.memoryDir);
|
|
5719
|
-
if (!needsFullRebuild && persistedIds.length === 0) return;
|
|
5720
|
-
try {
|
|
5721
|
-
// Read the corpus once to avoid N separate full-corpus scans.
|
|
5722
|
-
// On full rebuild with namespaces enabled, span all configured namespaces so
|
|
5723
|
-
// memories written to other namespaces before the index existed are also captured.
|
|
5724
|
-
const allMemories =
|
|
5725
|
-
needsFullRebuild && resolveNamespaceCapabilities(this.config).namespaces
|
|
5726
|
-
? await this.readAllMemoriesForNamespaces(
|
|
5727
|
-
Array.from(
|
|
5728
|
-
new Set<string>([
|
|
5729
|
-
this.config.defaultNamespace,
|
|
5730
|
-
this.config.sharedNamespace,
|
|
5731
|
-
...this.config.namespacePolicies.map((p) => p.name),
|
|
5732
|
-
]),
|
|
5733
|
-
),
|
|
5734
|
-
)
|
|
5735
|
-
: await storage.readAllMemories();
|
|
5736
|
-
|
|
5737
|
-
// Bootstrap: index only active (non-archived, non-superseded) memories.
|
|
5738
|
-
// Incremental: index only the newly persisted IDs.
|
|
5739
|
-
const pool = needsFullRebuild
|
|
5740
|
-
? allMemories.filter((m) => isActiveMemoryStatus(m.frontmatter.status))
|
|
5741
|
-
: (() => {
|
|
5742
|
-
const idSet = new Set(persistedIds);
|
|
5743
|
-
return allMemories.filter((m) => idSet.has(m.frontmatter.id));
|
|
5744
|
-
})();
|
|
5745
|
-
|
|
5746
|
-
const entries: Array<{
|
|
5747
|
-
path: string;
|
|
5748
|
-
createdAt: string;
|
|
5749
|
-
tags: string[];
|
|
5750
|
-
}> = [];
|
|
5751
|
-
for (const mem of pool) {
|
|
5752
|
-
if (mem.path && mem.frontmatter?.created) {
|
|
5753
|
-
entries.push({
|
|
5754
|
-
path: mem.path,
|
|
5755
|
-
createdAt: mem.frontmatter.created,
|
|
5756
|
-
tags: mem.frontmatter.tags ?? [],
|
|
5757
|
-
});
|
|
5758
|
-
}
|
|
5759
|
-
}
|
|
5760
|
-
if (needsFullRebuild) {
|
|
5761
|
-
// Always write empty indexes on full rebuild — even when the active pool
|
|
5762
|
-
// is empty (e.g. store contains only archived/superseded entries).
|
|
5763
|
-
// This marks bootstrap completion so indexesExist() returns true and
|
|
5764
|
-
// subsequent extractions skip the full-corpus scan.
|
|
5765
|
-
clearIndexes(this.config.memoryDir);
|
|
5766
|
-
if (entries.length > 0) {
|
|
5767
|
-
indexMemoriesBatch(this.config.memoryDir, entries);
|
|
5768
|
-
}
|
|
5769
|
-
log.info(
|
|
5770
|
-
`temporal-index: bootstrapped from ${entries.length} active memories`,
|
|
5771
|
-
);
|
|
5772
|
-
} else if (entries.length > 0) {
|
|
5773
|
-
indexMemoriesBatch(this.config.memoryDir, entries);
|
|
5774
|
-
}
|
|
5775
|
-
} catch (err) {
|
|
5776
|
-
log.debug(`temporal-index update failed (non-fatal): ${err}`);
|
|
5777
|
-
}
|
|
5522
|
+
return this.persistenceIndexCoordinator.updateTemporalTagIndexes(
|
|
5523
|
+
storage,
|
|
5524
|
+
persistedIds,
|
|
5525
|
+
);
|
|
5778
5526
|
}
|
|
5779
5527
|
|
|
5780
5528
|
/** IDs of facts persisted in the last extraction */
|
|
@@ -6198,60 +5946,16 @@ export class Orchestrator {
|
|
|
6198
5946
|
);
|
|
6199
5947
|
}
|
|
6200
5948
|
|
|
6201
|
-
/**
|
|
6202
|
-
* Issue #373 — nearest-neighbor lookup for the write-time semantic dedup
|
|
6203
|
-
* guard. Returns the top-K embedding hits against the currently indexed
|
|
6204
|
-
* memories, or an empty array when the embedding backend is unavailable.
|
|
6205
|
-
* Intentionally does NOT throw; `decideSemanticDedup` treats both "empty"
|
|
6206
|
-
* and "error" outcomes as fail-open (keep the candidate).
|
|
6207
|
-
*
|
|
6208
|
-
* PR #399 P1 fix: when namespaces are enabled the lookup must be scoped
|
|
6209
|
-
* to the SAME namespace as the fact being written. Otherwise a
|
|
6210
|
-
* high-similarity memory from another namespace can suppress a write in
|
|
6211
|
-
* the target namespace — cross-tenant data loss. Callers pass the target
|
|
6212
|
-
* storage so we can translate its root directory into the correct index
|
|
6213
|
-
* path prefix (and, for the legacy default-namespace layout at
|
|
6214
|
-
* `memoryDir` root, an exclusion list for `namespaces/*`).
|
|
6215
|
-
*/
|
|
6216
5949
|
async semanticDedupLookup(
|
|
6217
5950
|
content: string,
|
|
6218
5951
|
limit: number,
|
|
6219
5952
|
targetStorage: StorageManager,
|
|
6220
5953
|
): Promise<SemanticDedupHit[]> {
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
//
|
|
6227
|
-
// Contract after this fix:
|
|
6228
|
-
// • embeddingFallbackEnabled=false → throw (feature not configured;
|
|
6229
|
-
// caller treats this as backend_unavailable and fails open).
|
|
6230
|
-
// • isAvailable() returns false → throw (provider is reachable but
|
|
6231
|
-
// reports itself unavailable; distinct from empty index).
|
|
6232
|
-
// • search() throws → re-throw (network/provider error).
|
|
6233
|
-
// • search() returns [] → return [] (empty index, not a
|
|
6234
|
-
// backend failure; decideSemanticDedup reports no_candidates).
|
|
6235
|
-
if (!resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) {
|
|
6236
|
-
throw new Error("semantic dedup: embedding backend not configured");
|
|
6237
|
-
}
|
|
6238
|
-
if (!(await this.embeddingFallback.isAvailable())) {
|
|
6239
|
-
log.debug("semantic dedup: embedding backend unavailable, skipping");
|
|
6240
|
-
throw new Error("semantic dedup: embedding backend unavailable");
|
|
6241
|
-
}
|
|
6242
|
-
// search() may throw — let it propagate so decideSemanticDedup catches it
|
|
6243
|
-
// and returns reason="backend_unavailable". Pass throwOnTimeout:true so
|
|
6244
|
-
// EmbeddingTimeoutError is re-thrown here (Round 10 fix, Ui1J+Ui1L: the
|
|
6245
|
-
// recall-path caller searchEmbeddingFallback does NOT pass this flag,
|
|
6246
|
-
// keeping its fail-open [] contract on timeout).
|
|
6247
|
-
const scope = this.semanticDedupScopeFor(targetStorage);
|
|
6248
|
-
const hits = await this.embeddingFallback.search(content, limit, { ...scope, throwOnTimeout: true });
|
|
6249
|
-
if (!Array.isArray(hits) || hits.length === 0) return [];
|
|
6250
|
-
return hits.map((hit) => ({
|
|
6251
|
-
id: hit.id,
|
|
6252
|
-
score: hit.score,
|
|
6253
|
-
path: hit.path,
|
|
6254
|
-
}));
|
|
5954
|
+
return this.persistenceIndexCoordinator.semanticDedupLookup(
|
|
5955
|
+
content,
|
|
5956
|
+
limit,
|
|
5957
|
+
targetStorage,
|
|
5958
|
+
);
|
|
6255
5959
|
}
|
|
6256
5960
|
|
|
6257
5961
|
/**
|