recallnest 2.6.1 → 3.0.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/CHANGELOG.md +292 -0
- package/README.md +280 -195
- package/README_CN.md +266 -199
- package/config.json.example +8 -1
- package/docs/api-reference.md +1 -1
- package/integrations/README.md +1 -1
- package/integrations/agy/README.md +73 -0
- package/integrations/agy/setup.sh +75 -0
- package/integrations/setup-all.sh +1 -1
- package/package.json +7 -5
- package/src/access-tracker.ts +10 -1
- package/src/activity-counter.ts +69 -11
- package/src/age-label.ts +29 -0
- package/src/audit-log.ts +35 -0
- package/src/auto-gc.ts +97 -30
- package/src/capture-engine.ts +26 -4
- package/src/cli.ts +224 -10
- package/src/consolidation-engine.ts +317 -123
- package/src/context-composer-stable.ts +5 -1
- package/src/data-checkup.ts +17 -3
- package/src/dream-pipeline.ts +90 -12
- package/src/embedder.ts +44 -5
- package/src/env-config.ts +71 -0
- package/src/forget-engine.ts +8 -2
- package/src/gateway-server.ts +272 -0
- package/src/ingest.ts +248 -22
- package/src/kg-extractor.ts +17 -0
- package/src/llm-client.ts +86 -25
- package/src/mcp-server.ts +2 -1
- package/src/mcp-tools-advanced.ts +7 -2
- package/src/mcp-tools-core.ts +11 -1
- package/src/mcp-tools-governance.ts +45 -3
- package/src/memory-evolution.ts +16 -0
- package/src/memory-output.ts +47 -8
- package/src/memory-promotion.ts +280 -0
- package/src/memory-store-port.ts +32 -3
- package/src/memory-utility.ts +314 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/recall-ledger.ts +76 -0
- package/src/retriever.ts +82 -9
- package/src/runtime-config.ts +9 -0
- package/src/scope-policy.ts +22 -1
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +13 -4
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +218 -1
- package/src/store.ts +231 -71
- package/src/synthesis-contract.ts +286 -0
- package/src/version-manager.ts +83 -0
- package/src/workflow-observation-schema.ts +26 -0
- package/integrations/gemini-cli/README.md +0 -57
- package/integrations/gemini-cli/setup.sh +0 -64
- package/src/__tests__/access-tracker-novelty.test.ts +0 -304
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -945
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -430
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -276
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -780
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config.test.ts +0 -151
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -189
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -250
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -205
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -839
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
- /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
package/src/store.ts
CHANGED
|
@@ -9,11 +9,12 @@ import { dirname, join } from "node:path";
|
|
|
9
9
|
import { logWarn } from "./stderr-log.js";
|
|
10
10
|
import { readConsistencyInterval as envReadConsistencyInterval } from "./env-config.js";
|
|
11
11
|
import type { DurableMemoryCategory } from "./memory-schema.js";
|
|
12
|
-
import { matchesScopeFilter } from "./scope-policy.js";
|
|
12
|
+
import { matchesScopeFilter, type ScopeMatchMode } from "./scope-policy.js";
|
|
13
13
|
import { detectEmotionIfEnabled } from "./emotion-detector.js";
|
|
14
14
|
import { withWriteLock } from "./distill-lock.js";
|
|
15
15
|
import { incrementWriteCount } from "./activity-counter.js";
|
|
16
16
|
import type { MemoryStorePort, MemoryStoreStats, MemoryStoreUpdate } from "./memory-store-port.js";
|
|
17
|
+
import { clearVersionGroupMetadata, planSingletonVersionGroupRepairs } from "./version-manager.js";
|
|
17
18
|
|
|
18
19
|
// ============================================================================
|
|
19
20
|
// Types
|
|
@@ -222,6 +223,27 @@ export function validateStoragePath(dbPath: string): string {
|
|
|
222
223
|
|
|
223
224
|
const TABLE_NAME = "memories";
|
|
224
225
|
|
|
226
|
+
/**
|
|
227
|
+
* 把 scopeFilter 编译成 SQL 条件。**store 里所有 scope 过滤的唯一出处**
|
|
228
|
+
* (2026-08-16 收敛,此前同一条规则在 6 个方法里各抄了一遍)。
|
|
229
|
+
*
|
|
230
|
+
* `mode` 见 scope-policy.ts 的 ScopeMatchMode:默认 `family` = 历史行为(有冒号精确 /
|
|
231
|
+
* 无冒号前缀),`exact` = 调用方明说给的是具体 scope 名、一律精确。
|
|
232
|
+
* 应用层双检 `matchesScopeFilter(row, filter, mode)` 必须传同一个 mode,否则 SQL 收紧了
|
|
233
|
+
* 应用层又放行(或反之),两层判据打架。
|
|
234
|
+
*/
|
|
235
|
+
function scopeWhereClause(scopeFilter: string[], mode: ScopeMatchMode = "family"): string {
|
|
236
|
+
return scopeFilter
|
|
237
|
+
.map(scope => {
|
|
238
|
+
const safe = escapeSqlLiteral(scope);
|
|
239
|
+
if (mode === "exact") return `scope = '${safe}'`;
|
|
240
|
+
return scope.includes(":")
|
|
241
|
+
? `scope = '${safe}'`
|
|
242
|
+
: `scope LIKE '${safe}%'`;
|
|
243
|
+
})
|
|
244
|
+
.join(" OR ");
|
|
245
|
+
}
|
|
246
|
+
|
|
225
247
|
export class MemoryStore implements MemoryStorePort {
|
|
226
248
|
private db: LanceDB.Connection | null = null;
|
|
227
249
|
private table: LanceDB.Table | null = null;
|
|
@@ -467,7 +489,7 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
467
489
|
const statsPath = this.activityStatsPath();
|
|
468
490
|
const countByScope = new Map<string, number>();
|
|
469
491
|
for (const e of toWrite) countByScope.set(e.scope, (countByScope.get(e.scope) ?? 0) + 1);
|
|
470
|
-
for (const [scope, n] of countByScope) incrementWriteCount(scope, n, { statsPath });
|
|
492
|
+
for (const [scope, n] of countByScope) await incrementWriteCount(scope, n, { statsPath });
|
|
471
493
|
} catch { /* activity tracking is best-effort */ }
|
|
472
494
|
return toWrite.length;
|
|
473
495
|
}
|
|
@@ -497,7 +519,7 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
497
519
|
}, { expireMs: 30_000 });
|
|
498
520
|
// Per-scope write count drives dream activation. Best-effort — never block a write.
|
|
499
521
|
try {
|
|
500
|
-
incrementWriteCount(entry.scope, 1, { statsPath: this.activityStatsPath() });
|
|
522
|
+
await incrementWriteCount(entry.scope, 1, { statsPath: this.activityStatsPath() });
|
|
501
523
|
} catch { /* activity tracking is best-effort */ }
|
|
502
524
|
return entry;
|
|
503
525
|
}
|
|
@@ -554,7 +576,13 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
554
576
|
return res.length > 0;
|
|
555
577
|
}
|
|
556
578
|
|
|
557
|
-
async vectorSearch(
|
|
579
|
+
async vectorSearch(
|
|
580
|
+
vector: number[],
|
|
581
|
+
limit = 5,
|
|
582
|
+
minScore = 0.3,
|
|
583
|
+
scopeFilter?: string[],
|
|
584
|
+
scopeMatch: ScopeMatchMode = "family",
|
|
585
|
+
): Promise<MemorySearchResult[]> {
|
|
558
586
|
await this.ensureInitialized();
|
|
559
587
|
|
|
560
588
|
const safeLimit = clampInt(limit, 1, 20);
|
|
@@ -562,19 +590,11 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
562
590
|
|
|
563
591
|
let query = this.table!.vectorSearch(vector).distanceType('cosine').limit(fetchLimit);
|
|
564
592
|
|
|
565
|
-
// Apply scope filter if provided
|
|
566
|
-
//
|
|
593
|
+
// Apply scope filter if provided. Default mode "family" = 历史行为(含冒号精确 /
|
|
594
|
+
// 无冒号前缀);exact 由调用方显式声明。**必须下推到 SQL,不能事后在应用层滤**——
|
|
595
|
+
// top-k 在收窄前就被兄弟 scope 占满,滤完只剩残缺候选(2026-08-16 互审 C1)。
|
|
567
596
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
568
|
-
|
|
569
|
-
.map(scope => {
|
|
570
|
-
const safe = escapeSqlLiteral(scope);
|
|
571
|
-
// If scope contains ":", treat as exact match; otherwise prefix match
|
|
572
|
-
return scope.includes(":")
|
|
573
|
-
? `scope = '${safe}'`
|
|
574
|
-
: `scope LIKE '${safe}%'`;
|
|
575
|
-
})
|
|
576
|
-
.join(" OR ");
|
|
577
|
-
query = query.where(`(${scopeConditions})`);
|
|
597
|
+
query = query.where(`(${scopeWhereClause(scopeFilter, scopeMatch)})`);
|
|
578
598
|
}
|
|
579
599
|
|
|
580
600
|
const results = await query.toArray();
|
|
@@ -588,8 +608,8 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
588
608
|
|
|
589
609
|
const rowScope = (row.scope as string | undefined) ?? "";
|
|
590
610
|
|
|
591
|
-
// Double-check scope filter in application layer (
|
|
592
|
-
if (!matchesScopeFilter(rowScope, scopeFilter)) {
|
|
611
|
+
// Double-check scope filter in application layer (same mode as the SQL above)
|
|
612
|
+
if (!matchesScopeFilter(rowScope, scopeFilter, scopeMatch)) {
|
|
593
613
|
continue;
|
|
594
614
|
}
|
|
595
615
|
|
|
@@ -626,17 +646,9 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
626
646
|
// Use FTS query type explicitly
|
|
627
647
|
let searchQuery = this.table!.search(query, "fts").limit(safeLimit);
|
|
628
648
|
|
|
629
|
-
// Apply scope filter if provided (
|
|
649
|
+
// Apply scope filter if provided (family mode, same as vectorSearch's default)
|
|
630
650
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
631
|
-
|
|
632
|
-
.map(scope => {
|
|
633
|
-
const safe = escapeSqlLiteral(scope);
|
|
634
|
-
return scope.includes(":")
|
|
635
|
-
? `scope = '${safe}'`
|
|
636
|
-
: `scope LIKE '${safe}%'`;
|
|
637
|
-
})
|
|
638
|
-
.join(" OR ");
|
|
639
|
-
searchQuery = searchQuery.where(`(${scopeConditions})`);
|
|
651
|
+
searchQuery = searchQuery.where(`(${scopeWhereClause(scopeFilter)})`);
|
|
640
652
|
}
|
|
641
653
|
|
|
642
654
|
const results = await searchQuery.toArray();
|
|
@@ -725,7 +737,13 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
725
737
|
return true;
|
|
726
738
|
}
|
|
727
739
|
|
|
728
|
-
async list(
|
|
740
|
+
async list(
|
|
741
|
+
scopeFilter?: string[],
|
|
742
|
+
category?: string,
|
|
743
|
+
limit = 20,
|
|
744
|
+
offset = 0,
|
|
745
|
+
scopeMatch: ScopeMatchMode = "family",
|
|
746
|
+
): Promise<MemoryEntry[]> {
|
|
729
747
|
await this.ensureInitialized();
|
|
730
748
|
|
|
731
749
|
let query = this.table!.query();
|
|
@@ -734,15 +752,9 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
734
752
|
const conditions: string[] = [];
|
|
735
753
|
|
|
736
754
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
return scope.includes(":")
|
|
741
|
-
? `scope = '${safe}'`
|
|
742
|
-
: `scope LIKE '${safe}%'`;
|
|
743
|
-
})
|
|
744
|
-
.join(" OR ");
|
|
745
|
-
conditions.push(`((${scopeConditions}))`);
|
|
755
|
+
// 收窄必须在 SQL 里:limit 是在这之后下推的,事后过滤等于先截断再筛,
|
|
756
|
+
// 目标 scope 实际拿到多少行不可控(2026-08-16 互审 C1/K3)。
|
|
757
|
+
conditions.push(`((${scopeWhereClause(scopeFilter, scopeMatch)}))`);
|
|
746
758
|
}
|
|
747
759
|
|
|
748
760
|
if (category) {
|
|
@@ -801,15 +813,7 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
801
813
|
|
|
802
814
|
const conditions: string[] = [];
|
|
803
815
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
804
|
-
|
|
805
|
-
.map(scope => {
|
|
806
|
-
const safe = escapeSqlLiteral(scope);
|
|
807
|
-
return scope.includes(":")
|
|
808
|
-
? `scope = '${safe}'`
|
|
809
|
-
: `scope LIKE '${safe}%'`;
|
|
810
|
-
})
|
|
811
|
-
.join(" OR ");
|
|
812
|
-
conditions.push(`((${scopeConditions}))`);
|
|
816
|
+
conditions.push(`((${scopeWhereClause(scopeFilter)}))`);
|
|
813
817
|
}
|
|
814
818
|
if (category) {
|
|
815
819
|
conditions.push(`category = '${escapeSqlLiteral(category)}'`);
|
|
@@ -962,21 +966,13 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
962
966
|
return result;
|
|
963
967
|
}
|
|
964
968
|
|
|
965
|
-
async stats(scopeFilter?: string[]): Promise<MemoryStoreStats> {
|
|
969
|
+
async stats(scopeFilter?: string[], scopeMatch: ScopeMatchMode = "family"): Promise<MemoryStoreStats> {
|
|
966
970
|
await this.ensureInitialized();
|
|
967
971
|
|
|
968
972
|
let query = this.table!.query();
|
|
969
973
|
|
|
970
974
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
971
|
-
|
|
972
|
-
.map(scope => {
|
|
973
|
-
const safe = escapeSqlLiteral(scope);
|
|
974
|
-
return scope.includes(":")
|
|
975
|
-
? `scope = '${safe}'`
|
|
976
|
-
: `scope LIKE '${safe}%'`;
|
|
977
|
-
})
|
|
978
|
-
.join(" OR ");
|
|
979
|
-
query = query.where(`((${scopeConditions}))`);
|
|
975
|
+
query = query.where(`((${scopeWhereClause(scopeFilter, scopeMatch)}))`);
|
|
980
976
|
}
|
|
981
977
|
|
|
982
978
|
const results = await query.select(["scope", "category"]).toArray();
|
|
@@ -1110,11 +1106,17 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
1110
1106
|
// Atomic upsert via mergeInsert — replaces the previous delete+add two-step,
|
|
1111
1107
|
// which could lose the row entirely on a crash between steps and exposed a
|
|
1112
1108
|
// window where concurrent reads saw the id momentarily vanish.
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1109
|
+
//
|
|
1110
|
+
// 2026-08-14: 包进 store-write 锁与 upsert/storeBatch 对齐 —— 此前 update 是唯一
|
|
1111
|
+
// 裸奔的写路径(dream 的 3a/3b/auto-gc 全走它),与其他进程的 mergeInsert 并发时
|
|
1112
|
+
// 靠 LanceDB 乐观重试硬扛,是 commit conflict 的隐性源之一。
|
|
1113
|
+
await withWriteLock("store-write", async () => {
|
|
1114
|
+
await this.table!
|
|
1115
|
+
.mergeInsert("id")
|
|
1116
|
+
.whenMatchedUpdateAll()
|
|
1117
|
+
.whenNotMatchedInsertAll()
|
|
1118
|
+
.execute([updated]);
|
|
1119
|
+
}, { expireMs: 30_000 });
|
|
1118
1120
|
|
|
1119
1121
|
return updated;
|
|
1120
1122
|
}
|
|
@@ -1195,21 +1197,179 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
1195
1197
|
return this.metadataPatchQueues.size;
|
|
1196
1198
|
}
|
|
1197
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Batched metadata read-modify-write: one store-write lock acquisition, one
|
|
1202
|
+
* mergeInsert commit per batch of up to 200 rows — versus one commit per row
|
|
1203
|
+
* through update()/patchMetadata().
|
|
1204
|
+
*
|
|
1205
|
+
* 为什么存在(2026-08-14 dream 写爆库根治):LanceDB 每次写 commit 生成一个新
|
|
1206
|
+
* manifest 版本文件,manifest 大小 ∝ 全表 fragment 数(实测 348-436KB/个)。dream
|
|
1207
|
+
* 的 3a/3b/auto-gc 全部逐条 update,一轮 --auto 写出 ~6600 个 commit ≈ 2GB+ 的
|
|
1208
|
+
* _versions 增量。本方法把"一批相关行的 metadata 变更"折成单次 commit。
|
|
1209
|
+
*
|
|
1210
|
+
* 语义要点:
|
|
1211
|
+
* - patchFn 在 **锁内、以库里最新行为底座** 应用 —— 不接受调用方预先物化的完整
|
|
1212
|
+
* metadata 串。这是互审 R1 的 P0 修正:3b 持有的是 3a 之前 gather 的旧条目,
|
|
1213
|
+
* 预物化串批量写会把 3a 刚提交的 version_group/sourceMemories 再抹一遍
|
|
1214
|
+
* (与它要修的 Bug-1 同病,只是从循环内搬到阶段间)。
|
|
1215
|
+
* - patches 按数组序在同一临界区内依次应用:同批的 patchFn 之间可通过调用方
|
|
1216
|
+
* 闭包传递决策(3a 靠它让 canonical 与 members 共享同一个 groupId)。
|
|
1217
|
+
* - 单条 patchFn 抛错或 id 不存在/越权:跳过该条继续(stderr 记一行),不炸整批 ——
|
|
1218
|
+
* 与 dream"单 scope 失败不阻断其他"的分级一致。返回成功写入数。
|
|
1219
|
+
* - 与 patchMetadata 的 per-id 队列并存:本方法自身持 store-write 锁做批内原子,
|
|
1220
|
+
* 跨进程窗口由锁消除;进程内与单条 patchMetadata 的交错等同于现状并发水平。
|
|
1221
|
+
*/
|
|
1222
|
+
async patchMetadataBatch(
|
|
1223
|
+
patches: Array<{
|
|
1224
|
+
id: string;
|
|
1225
|
+
patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>;
|
|
1226
|
+
}>,
|
|
1227
|
+
scopeFilter?: string[],
|
|
1228
|
+
): Promise<number> {
|
|
1229
|
+
await this.ensureInitialized();
|
|
1230
|
+
if (patches.length === 0) return 0;
|
|
1231
|
+
|
|
1232
|
+
let written = 0;
|
|
1233
|
+
// 分批:避免 OR 链过长(抄 getVectors 的范式,批更保守——每批一个 commit 本身就是收益)
|
|
1234
|
+
const BATCH = 200;
|
|
1235
|
+
for (let i = 0; i < patches.length; i += BATCH) {
|
|
1236
|
+
const batch = patches.slice(i, i + BATCH);
|
|
1237
|
+
written += await withWriteLock("store-write", async () => {
|
|
1238
|
+
// 锁内读最新行
|
|
1239
|
+
const conditions = batch.map(p => `id = '${escapeSqlLiteral(p.id)}'`).join(" OR ");
|
|
1240
|
+
const rows = await this.table!.query().where(conditions).limit(batch.length).toArray();
|
|
1241
|
+
const rowById = new Map<string, Record<string, unknown>>();
|
|
1242
|
+
for (const row of rows) rowById.set(row.id as string, row as Record<string, unknown>);
|
|
1243
|
+
|
|
1244
|
+
const toWrite: MemoryEntry[] = [];
|
|
1245
|
+
for (const { id, patchFn } of batch) {
|
|
1246
|
+
const row = rowById.get(id);
|
|
1247
|
+
if (!row) continue; // 行不存在:跳过(dream 处理途中被并发删除属正常)
|
|
1248
|
+
const rowScope = (row.scope as string | undefined) ?? "";
|
|
1249
|
+
if (!matchesScopeFilter(rowScope, scopeFilter)) continue;
|
|
1250
|
+
let meta: Record<string, unknown>;
|
|
1251
|
+
try {
|
|
1252
|
+
const parsed: unknown = JSON.parse((row.metadata as string) || "{}");
|
|
1253
|
+
meta = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1254
|
+
? (parsed as Record<string, unknown>)
|
|
1255
|
+
: {};
|
|
1256
|
+
} catch {
|
|
1257
|
+
meta = {};
|
|
1258
|
+
}
|
|
1259
|
+
const entry: MemoryEntry = {
|
|
1260
|
+
id: row.id as string,
|
|
1261
|
+
text: row.text as string,
|
|
1262
|
+
// null 向量防护同 update():历史病行 → [],metadata-only 批量写不被绊倒
|
|
1263
|
+
vector: row.vector ? Array.from(row.vector as Iterable<number>) : [],
|
|
1264
|
+
category: row.category as MemoryEntry["category"],
|
|
1265
|
+
scope: rowScope,
|
|
1266
|
+
importance: Number(row.importance),
|
|
1267
|
+
timestamp: Number(row.timestamp),
|
|
1268
|
+
metadata: (row.metadata as string) || "{}",
|
|
1269
|
+
language: (row.language as string) || "en",
|
|
1270
|
+
fts_text: (row.fts_text as string) || (row.text as string),
|
|
1271
|
+
};
|
|
1272
|
+
try {
|
|
1273
|
+
const patched = patchFn(meta, entry);
|
|
1274
|
+
toWrite.push({ ...entry, metadata: JSON.stringify(patched) });
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
console.error(`[patchMetadataBatch] patchFn failed for ${id}, skipping: ${err instanceof Error ? err.message : String(err)}`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (toWrite.length === 0) return 0;
|
|
1280
|
+
await this.table!.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(toWrite);
|
|
1281
|
+
return toWrite.length;
|
|
1282
|
+
}, { expireMs: 30_000 });
|
|
1283
|
+
}
|
|
1284
|
+
return written;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Dissolve version groups that have only one surviving member.
|
|
1289
|
+
*
|
|
1290
|
+
* The membership scan and metadata rewrite share the global store-write lock.
|
|
1291
|
+
* Keeping both inside one critical section matters: planning during auto-GC and
|
|
1292
|
+
* patching later could otherwise dismantle a group that gained a new member in
|
|
1293
|
+
* between. Hard deletes that bypass the store may still create a new singleton
|
|
1294
|
+
* after this pass; the next scheduled pass repairs it.
|
|
1295
|
+
*/
|
|
1296
|
+
async repairSingletonVersionGroups(): Promise<number> {
|
|
1297
|
+
await this.ensureInitialized();
|
|
1298
|
+
|
|
1299
|
+
return withWriteLock("store-write", async () => {
|
|
1300
|
+
const membershipRows = await this.table!.query()
|
|
1301
|
+
.select(["id", "metadata"])
|
|
1302
|
+
.toArray();
|
|
1303
|
+
const repairs = planSingletonVersionGroupRepairs(
|
|
1304
|
+
membershipRows.map(row => ({
|
|
1305
|
+
id: row.id as string,
|
|
1306
|
+
metadata: (row.metadata as string) || "{}",
|
|
1307
|
+
})),
|
|
1308
|
+
);
|
|
1309
|
+
if (repairs.length === 0) return 0;
|
|
1310
|
+
|
|
1311
|
+
let repaired = 0;
|
|
1312
|
+
const BATCH = 200;
|
|
1313
|
+
for (let i = 0; i < repairs.length; i += BATCH) {
|
|
1314
|
+
const batch = repairs.slice(i, i + BATCH);
|
|
1315
|
+
const expectedById = new Map(batch.map(item => [item.id, item.groupId]));
|
|
1316
|
+
const conditions = batch
|
|
1317
|
+
.map(item => `id = '${escapeSqlLiteral(item.id)}'`)
|
|
1318
|
+
.join(" OR ");
|
|
1319
|
+
const rows = await this.table!.query()
|
|
1320
|
+
.where(conditions)
|
|
1321
|
+
.limit(batch.length)
|
|
1322
|
+
.toArray();
|
|
1323
|
+
|
|
1324
|
+
const toWrite: MemoryEntry[] = [];
|
|
1325
|
+
for (const row of rows) {
|
|
1326
|
+
const id = row.id as string;
|
|
1327
|
+
const expectedGroupId = expectedById.get(id);
|
|
1328
|
+
if (!expectedGroupId) continue;
|
|
1329
|
+
let meta: Record<string, unknown>;
|
|
1330
|
+
try {
|
|
1331
|
+
const parsed: unknown = JSON.parse((row.metadata as string) || "{}");
|
|
1332
|
+
meta = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1333
|
+
? (parsed as Record<string, unknown>)
|
|
1334
|
+
: {};
|
|
1335
|
+
} catch {
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
// The group id is rechecked against the locked, current row.
|
|
1339
|
+
if (meta.version_group !== expectedGroupId) continue;
|
|
1340
|
+
clearVersionGroupMetadata(meta, expectedGroupId);
|
|
1341
|
+
toWrite.push({
|
|
1342
|
+
id,
|
|
1343
|
+
text: row.text as string,
|
|
1344
|
+
vector: row.vector ? Array.from(row.vector as Iterable<number>) : [],
|
|
1345
|
+
category: row.category as MemoryEntry["category"],
|
|
1346
|
+
scope: (row.scope as string | undefined) ?? "",
|
|
1347
|
+
importance: Number(row.importance),
|
|
1348
|
+
timestamp: Number(row.timestamp),
|
|
1349
|
+
metadata: JSON.stringify(meta),
|
|
1350
|
+
language: (row.language as string) || "en",
|
|
1351
|
+
fts_text: (row.fts_text as string) || (row.text as string),
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
if (toWrite.length === 0) continue;
|
|
1355
|
+
// Matched rows only: a concurrent hard delete must never be resurrected.
|
|
1356
|
+
const result = await this.table!.mergeInsert("id").whenMatchedUpdateAll().execute(toWrite);
|
|
1357
|
+
repaired += result.numUpdatedRows;
|
|
1358
|
+
}
|
|
1359
|
+
return repaired;
|
|
1360
|
+
}, { expireMs: 30 * 60_000 });
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1198
1363
|
async bulkDelete(scopeFilter: string[], beforeTimestamp?: number): Promise<number> {
|
|
1199
1364
|
await this.ensureInitialized();
|
|
1200
1365
|
|
|
1201
1366
|
const conditions: string[] = [];
|
|
1202
1367
|
|
|
1203
1368
|
if (scopeFilter.length > 0) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
? `scope = '${safe}'`
|
|
1209
|
-
: `scope LIKE '${safe}%'`;
|
|
1210
|
-
})
|
|
1211
|
-
.join(" OR ");
|
|
1212
|
-
conditions.push(`(${scopeConditions})`);
|
|
1369
|
+
// family 模式(历史行为)。⚠️ 这是**破坏性**操作走前缀:`bulkDelete(["memory"])`
|
|
1370
|
+
// 会连 `memory:pivot` 一起删。当前无生产 caller;要加 caller 请先显式传 exact
|
|
1371
|
+
// (同 forgetByScope 的处理,2026-08-16)。
|
|
1372
|
+
conditions.push(`(${scopeWhereClause(scopeFilter)})`);
|
|
1213
1373
|
}
|
|
1214
1374
|
|
|
1215
1375
|
if (beforeTimestamp) {
|