recallnest 2.2.1 → 2.5.4
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 +150 -0
- package/README.md +46 -35
- package/README_CN.md +41 -26
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +199 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/connector-types.ts +57 -0
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +126 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +38 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +234 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/obsidian-connector.ts +282 -0
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +3 -2
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/source-heartbeat.ts +108 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -30
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GB-3: Source Heartbeat Tracking
|
|
3
|
+
*
|
|
4
|
+
* Every connector ingest writes a heartbeat record to data/source-heartbeat.json.
|
|
5
|
+
* data_checkup and doctor --ci read heartbeats to detect stale data sources.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { resolve, dirname } from "node:path";
|
|
10
|
+
import { metaDir } from "./compat.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface SourceHeartbeat {
|
|
17
|
+
source: string;
|
|
18
|
+
lastIngest: string; // ISO 8601
|
|
19
|
+
recordsIngested: number;
|
|
20
|
+
errors: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type HeartbeatFile = Record<string, SourceHeartbeat>;
|
|
24
|
+
|
|
25
|
+
export interface StaleSource {
|
|
26
|
+
source: string;
|
|
27
|
+
lastIngest: string;
|
|
28
|
+
daysSince: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Path
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const HEARTBEAT_PATH = resolve(metaDir(import.meta), "../data/source-heartbeat.json");
|
|
36
|
+
|
|
37
|
+
/** Visible for testing — override the default path. */
|
|
38
|
+
export function heartbeatPath(): string {
|
|
39
|
+
return HEARTBEAT_PATH;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Read / Write
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export function readHeartbeats(path?: string): HeartbeatFile {
|
|
47
|
+
const p = path ?? HEARTBEAT_PATH;
|
|
48
|
+
if (!existsSync(p)) return {};
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(p, "utf-8")) as HeartbeatFile;
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeHeartbeat(
|
|
57
|
+
source: string,
|
|
58
|
+
recordsIngested: number,
|
|
59
|
+
errors: string[],
|
|
60
|
+
path?: string,
|
|
61
|
+
): void {
|
|
62
|
+
const p = path ?? HEARTBEAT_PATH;
|
|
63
|
+
const dir = dirname(p);
|
|
64
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
const existing = readHeartbeats(p);
|
|
67
|
+
existing[source] = {
|
|
68
|
+
source,
|
|
69
|
+
lastIngest: new Date().toISOString(),
|
|
70
|
+
recordsIngested,
|
|
71
|
+
errors: errors.slice(0, 10), // cap stored errors
|
|
72
|
+
};
|
|
73
|
+
writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Staleness check
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
const MS_PER_DAY = 86_400_000;
|
|
81
|
+
|
|
82
|
+
export function checkSourceStaleness(maxDays = 7, path?: string): StaleSource[] {
|
|
83
|
+
const heartbeats = readHeartbeats(path);
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const stale: StaleSource[] = [];
|
|
86
|
+
|
|
87
|
+
for (const hb of Object.values(heartbeats)) {
|
|
88
|
+
const lastMs = Date.parse(hb.lastIngest);
|
|
89
|
+
if (Number.isNaN(lastMs)) continue;
|
|
90
|
+
const daysSince = Math.floor((now - lastMs) / MS_PER_DAY);
|
|
91
|
+
if (daysSince > maxDays) {
|
|
92
|
+
stale.push({ source: hb.source, lastIngest: hb.lastIngest, daysSince });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return stale.sort((a, b) => b.daysSince - a.daysSince);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Human-readable age string, e.g. "2h ago", "3d ago". */
|
|
100
|
+
export function formatAge(isoDate: string): string {
|
|
101
|
+
const ms = Date.now() - Date.parse(isoDate);
|
|
102
|
+
if (ms < 0) return "just now";
|
|
103
|
+
const hours = Math.floor(ms / 3_600_000);
|
|
104
|
+
if (hours < 1) return "just now";
|
|
105
|
+
if (hours < 24) return `${hours}h ago`;
|
|
106
|
+
const days = Math.floor(hours / 24);
|
|
107
|
+
return `${days}d ago`;
|
|
108
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
import type * as LanceDB from "@lancedb/lancedb";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { existsSync, accessSync, constants, mkdirSync, realpathSync, lstatSync } from "node:fs";
|
|
8
|
-
import { dirname } from "node:path";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
9
|
import { logWarn } from "./stderr-log.js";
|
|
10
10
|
import type { DurableMemoryCategory } from "./memory-schema.js";
|
|
11
11
|
import { matchesScopeFilter } from "./scope-policy.js";
|
|
12
12
|
import { detectEmotionIfEnabled } from "./emotion-detector.js";
|
|
13
|
+
import { withWriteLock } from "./distill-lock.js";
|
|
14
|
+
import { incrementWriteCount } from "./activity-counter.js";
|
|
15
|
+
import type { MemoryStorePort, MemoryStoreStats, MemoryStoreUpdate } from "./memory-store-port.js";
|
|
13
16
|
|
|
14
17
|
// ============================================================================
|
|
15
18
|
// Types
|
|
@@ -211,11 +214,13 @@ export function validateStoragePath(dbPath: string): string {
|
|
|
211
214
|
|
|
212
215
|
const TABLE_NAME = "memories";
|
|
213
216
|
|
|
214
|
-
export class MemoryStore {
|
|
217
|
+
export class MemoryStore implements MemoryStorePort {
|
|
215
218
|
private db: LanceDB.Connection | null = null;
|
|
216
219
|
private table: LanceDB.Table | null = null;
|
|
217
220
|
private initPromise: Promise<void> | null = null;
|
|
218
221
|
private ftsIndexCreated = false;
|
|
222
|
+
/** Per-id serial queues for patchMetadata; entries self-remove on settle. */
|
|
223
|
+
private readonly metadataPatchQueues = new Map<string, Promise<void>>();
|
|
219
224
|
|
|
220
225
|
constructor(private readonly config: StoreConfig) {}
|
|
221
226
|
|
|
@@ -375,7 +380,10 @@ export class MemoryStore {
|
|
|
375
380
|
}
|
|
376
381
|
|
|
377
382
|
try {
|
|
378
|
-
|
|
383
|
+
// P0-2: go through upsert (delete+add under the global store-write lock) rather
|
|
384
|
+
// than a bare table.add — with the deterministic id above, storing the same
|
|
385
|
+
// (scope,text) twice now yields one row, not a dup-id pair (fixes the append bug).
|
|
386
|
+
return await this.upsert(fullEntry);
|
|
379
387
|
} catch (err: any) {
|
|
380
388
|
const code = err.code || "";
|
|
381
389
|
const message = err.message || String(err);
|
|
@@ -383,20 +391,21 @@ export class MemoryStore {
|
|
|
383
391
|
`Failed to store memory in "${this.config.dbPath}": ${code} ${message}`
|
|
384
392
|
);
|
|
385
393
|
}
|
|
386
|
-
return fullEntry;
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
/**
|
|
390
397
|
* Batch store multiple entries at once — much faster than individual store() calls.
|
|
391
398
|
* LanceDB handles bulk inserts efficiently with a single index update.
|
|
392
399
|
*/
|
|
393
|
-
async storeBatch(entries: Omit<MemoryEntry, "id" | "timestamp">[]): Promise<number> {
|
|
400
|
+
async storeBatch(entries: (Omit<MemoryEntry, "id" | "timestamp"> & { id?: string })[]): Promise<number> {
|
|
394
401
|
if (entries.length === 0) return 0;
|
|
395
402
|
await this.ensureInitialized();
|
|
396
403
|
|
|
397
404
|
const fullEntries: MemoryEntry[] = entries.map(entry => ({
|
|
398
405
|
...entry,
|
|
399
|
-
|
|
406
|
+
// P0-2: honor a caller-supplied id (consolidation insight/pattern idempotency),
|
|
407
|
+
// else the deterministic content id — unified with store() (was: always recompute).
|
|
408
|
+
id: entry.id ?? deterministicId(entry.scope, entry.text),
|
|
400
409
|
timestamp: Date.now(),
|
|
401
410
|
metadata: entry.metadata || "{}",
|
|
402
411
|
language: entry.language || "en",
|
|
@@ -413,8 +422,20 @@ export class MemoryStore {
|
|
|
413
422
|
return e;
|
|
414
423
|
});
|
|
415
424
|
|
|
425
|
+
// P2: collapse in-batch duplicate ids (latest-wins) BEFORE writing. Two entries
|
|
426
|
+
// with the same (scope,text) → same deterministic id; adding both would create a
|
|
427
|
+
// fresh dup-id pair in one shot (delete-set was deduped but the add was not).
|
|
428
|
+
const dedupedById = new Map<string, MemoryEntry>();
|
|
429
|
+
for (const e of enrichedEntries) dedupedById.set(e.id, e); // later occurrence wins
|
|
430
|
+
const toWrite = [...dedupedById.values()];
|
|
431
|
+
|
|
416
432
|
try {
|
|
417
|
-
await
|
|
433
|
+
await withWriteLock("store-write", async () => {
|
|
434
|
+
// P0-1/P0-2/P1: atomic idempotent batch write via mergeInsert (no delete+add
|
|
435
|
+
// window a crash could tear). toWrite is already in-batch deduped by id above,
|
|
436
|
+
// so the merge key matches ≤1 existing row (given no pre-existing dup-id rows).
|
|
437
|
+
await this.table!.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(toWrite);
|
|
438
|
+
}, { expireMs: 30_000 });
|
|
418
439
|
} catch (err: any) {
|
|
419
440
|
const code = err.code || "";
|
|
420
441
|
const message = err.message || String(err);
|
|
@@ -422,7 +443,44 @@ export class MemoryStore {
|
|
|
422
443
|
`Failed to batch store ${entries.length} memories in "${this.config.dbPath}": ${code} ${message}`
|
|
423
444
|
);
|
|
424
445
|
}
|
|
425
|
-
|
|
446
|
+
// Per-scope write counts drive dream activation. Best-effort — never block writes.
|
|
447
|
+
try {
|
|
448
|
+
const statsPath = this.activityStatsPath();
|
|
449
|
+
const countByScope = new Map<string, number>();
|
|
450
|
+
for (const e of toWrite) countByScope.set(e.scope, (countByScope.get(e.scope) ?? 0) + 1);
|
|
451
|
+
for (const [scope, n] of countByScope) incrementWriteCount(scope, n, { statsPath });
|
|
452
|
+
} catch { /* activity tracking is best-effort */ }
|
|
453
|
+
return toWrite.length;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* P0-1/P0-2/P1: Idempotent, crash-atomic write of a fully-formed entry, serialized
|
|
458
|
+
* across processes.
|
|
459
|
+
*
|
|
460
|
+
* Uses mergeInsert("id") (same primitive as update()) so the write is atomic — no
|
|
461
|
+
* delete+add window that a crash between the two steps could tear, losing the row
|
|
462
|
+
* (P1). The global `store-write` lock still serializes concurrent merges across the
|
|
463
|
+
* 11 mcp-server processes so two whenNotMatched inserts can't both fire for the same
|
|
464
|
+
* id (the P0-1 race). "Same id overwrites" gives content-addressed idempotency to
|
|
465
|
+
* every store()/storeBatch() caller whose id is deterministicId(scope,text).
|
|
466
|
+
* Precondition: no pre-existing dup-id rows in the table (run the dedup migration
|
|
467
|
+
* scripts/cleanup-duplicate-ids.ts first) — else the merge key match is ambiguous.
|
|
468
|
+
*/
|
|
469
|
+
/** Path to this store's activity-counter stats file, beside the LanceDB dir. */
|
|
470
|
+
private activityStatsPath(): string {
|
|
471
|
+
return join(dirname(this.config.dbPath), "activity-stats.json");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async upsert(entry: MemoryEntry): Promise<MemoryEntry> {
|
|
475
|
+
await this.ensureInitialized();
|
|
476
|
+
await withWriteLock("store-write", async () => {
|
|
477
|
+
await this.table!.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([entry]);
|
|
478
|
+
}, { expireMs: 30_000 });
|
|
479
|
+
// Per-scope write count drives dream activation. Best-effort — never block a write.
|
|
480
|
+
try {
|
|
481
|
+
incrementWriteCount(entry.scope, 1, { statsPath: this.activityStatsPath() });
|
|
482
|
+
} catch { /* activity tracking is best-effort */ }
|
|
483
|
+
return entry;
|
|
426
484
|
}
|
|
427
485
|
|
|
428
486
|
/**
|
|
@@ -617,11 +675,19 @@ export class MemoryStore {
|
|
|
617
675
|
if (isFullId) {
|
|
618
676
|
candidates = await this.table!.query().where(`id = '${id}'`).limit(1).toArray();
|
|
619
677
|
} else {
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
678
|
+
// v2.5.2 (2026-05-27): Use SQL LIKE for prefix lookup instead of app-layer scan
|
|
679
|
+
// with limit(1000) — the old `.select().limit(1000).filter()` pattern silently
|
|
680
|
+
// missed entries beyond row 1000 in 90K+ libraries (Codex trio review finding,
|
|
681
|
+
// ref: ~/Desktop/codex-v2.5.1-fix-review-20260527.md, store.delete limit 漏洞).
|
|
682
|
+
// Now mirrors getById prefix behavior: LIKE with limit(2) detects ambiguity.
|
|
683
|
+
const safeId = escapeSqlLiteral(id);
|
|
684
|
+
candidates = await this.table!.query()
|
|
685
|
+
.select(["id", "scope"])
|
|
686
|
+
.where(`id LIKE '${safeId}%'`)
|
|
687
|
+
.limit(2)
|
|
688
|
+
.toArray();
|
|
623
689
|
if (candidates.length > 1) {
|
|
624
|
-
throw new Error(`Ambiguous prefix "${id}" matches ${candidates.length} memories. Use a longer prefix or full ID.`);
|
|
690
|
+
throw new Error(`Ambiguous prefix "${id}" matches ${candidates.length}+ memories. Use a longer prefix or full ID.`);
|
|
625
691
|
}
|
|
626
692
|
}
|
|
627
693
|
if (candidates.length === 0) {
|
|
@@ -688,6 +754,72 @@ export class MemoryStore {
|
|
|
688
754
|
.slice(offset, offset + limit);
|
|
689
755
|
}
|
|
690
756
|
|
|
757
|
+
/**
|
|
758
|
+
* True DB-level pagination — where/limit/offset are pushed down to LanceDB,
|
|
759
|
+
* so unlike list() this never materializes the full match set in JS. Use for
|
|
760
|
+
* full-corpus maintenance sweeps (auto-gc, promotion scan, lint/checkup).
|
|
761
|
+
*
|
|
762
|
+
* Caveats:
|
|
763
|
+
* - No orderBy support in the LanceDB query API: rows come back in fragment
|
|
764
|
+
* scan order, NOT timestamp order. Stable across pages for a read-only
|
|
765
|
+
* sweep, but concurrent writes rewrite fragments and may shift rows
|
|
766
|
+
* between pages (a missed row is picked up by the next sweep). Do not use
|
|
767
|
+
* for user-facing ordered listings — that's what list() is for.
|
|
768
|
+
* - includeVector loads the embedding column; only request it when the
|
|
769
|
+
* consumer actually needs vectors (they dominate row size).
|
|
770
|
+
*/
|
|
771
|
+
async listPage(opts: {
|
|
772
|
+
scopeFilter?: string[];
|
|
773
|
+
category?: string;
|
|
774
|
+
limit?: number;
|
|
775
|
+
offset?: number;
|
|
776
|
+
includeVector?: boolean;
|
|
777
|
+
} = {}): Promise<MemoryEntry[]> {
|
|
778
|
+
await this.ensureInitialized();
|
|
779
|
+
const { scopeFilter, category, limit = 1000, offset = 0, includeVector = false } = opts;
|
|
780
|
+
|
|
781
|
+
let query = this.table!.query();
|
|
782
|
+
|
|
783
|
+
const conditions: string[] = [];
|
|
784
|
+
if (scopeFilter && scopeFilter.length > 0) {
|
|
785
|
+
const scopeConditions = scopeFilter
|
|
786
|
+
.map(scope => {
|
|
787
|
+
const safe = escapeSqlLiteral(scope);
|
|
788
|
+
return scope.includes(":")
|
|
789
|
+
? `scope = '${safe}'`
|
|
790
|
+
: `scope LIKE '${safe}%'`;
|
|
791
|
+
})
|
|
792
|
+
.join(" OR ");
|
|
793
|
+
conditions.push(`((${scopeConditions}))`);
|
|
794
|
+
}
|
|
795
|
+
if (category) {
|
|
796
|
+
conditions.push(`category = '${escapeSqlLiteral(category)}'`);
|
|
797
|
+
}
|
|
798
|
+
if (conditions.length > 0) {
|
|
799
|
+
query = query.where(conditions.join(" AND "));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const columns = ["id", "text", "category", "scope", "importance", "timestamp", "metadata"];
|
|
803
|
+
if (includeVector) columns.push("vector");
|
|
804
|
+
|
|
805
|
+
const results = await query
|
|
806
|
+
.select(columns)
|
|
807
|
+
.limit(limit)
|
|
808
|
+
.offset(offset)
|
|
809
|
+
.toArray();
|
|
810
|
+
|
|
811
|
+
return results.map((row): MemoryEntry => ({
|
|
812
|
+
id: row.id as string,
|
|
813
|
+
text: row.text as string,
|
|
814
|
+
vector: includeVector ? Array.from(row.vector as Iterable<number>) : [],
|
|
815
|
+
category: row.category as MemoryEntry["category"],
|
|
816
|
+
scope: (row.scope as string | undefined) ?? "",
|
|
817
|
+
importance: Number(row.importance),
|
|
818
|
+
timestamp: Number(row.timestamp),
|
|
819
|
+
metadata: (row.metadata as string) || "{}",
|
|
820
|
+
}));
|
|
821
|
+
}
|
|
822
|
+
|
|
691
823
|
async get(id: string, scopeFilter?: string[]): Promise<MemoryEntry | null> {
|
|
692
824
|
await this.ensureInitialized();
|
|
693
825
|
|
|
@@ -741,18 +873,35 @@ export class MemoryStore {
|
|
|
741
873
|
}
|
|
742
874
|
|
|
743
875
|
/**
|
|
744
|
-
* Get a single entry by
|
|
745
|
-
* Lightweight read used by AccessTracker for metadata updates
|
|
876
|
+
* Get a single entry by ID (full UUID) or 8+ hex prefix.
|
|
877
|
+
* Lightweight read used by AccessTracker for metadata updates and by skill-engine
|
|
878
|
+
* for outcome recording.
|
|
879
|
+
*
|
|
880
|
+
* v2.5.1 (2026-05-27) — 加 prefix lookup support:和 update() / delete() 行为对齐,
|
|
881
|
+
* 让 agent 拿到 short id(如 store_skill 返回的 8 位前缀、人类粘贴的截断 id)也能
|
|
882
|
+
* resolve 到完整 entry。歧义 prefix(匹配 >1 条)返回 null 让 caller 处理。
|
|
883
|
+
* 现有 caller 全部传完整 UUID,向后兼容。
|
|
746
884
|
*/
|
|
747
885
|
async getById(id: string): Promise<MemoryEntry | null> {
|
|
748
886
|
await this.ensureInitialized();
|
|
887
|
+
|
|
888
|
+
// Support both full UUID and 8+ hex prefix (same pattern as update() / delete()).
|
|
889
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
890
|
+
const prefixRegex = /^[0-9a-f]{8,}$/i;
|
|
891
|
+
const isFullId = uuidRegex.test(id);
|
|
892
|
+
const isPrefix = !isFullId && prefixRegex.test(id);
|
|
893
|
+
if (!isFullId && !isPrefix) return null;
|
|
894
|
+
|
|
749
895
|
const safeId = escapeSqlLiteral(id);
|
|
896
|
+
const whereClause = isFullId ? `id = '${safeId}'` : `id LIKE '${safeId}%'`;
|
|
897
|
+
|
|
750
898
|
const rows = await this.table!.query()
|
|
751
899
|
.select(["id", "text", "vector", "category", "scope", "importance", "timestamp", "metadata"])
|
|
752
|
-
.where(
|
|
753
|
-
.limit(1)
|
|
900
|
+
.where(whereClause)
|
|
901
|
+
.limit(2) // detect ambiguous prefix (matches >1)
|
|
754
902
|
.toArray();
|
|
755
903
|
if (rows.length === 0) return null;
|
|
904
|
+
if (isPrefix && rows.length > 1) return null; // ambiguous prefix
|
|
756
905
|
const row = rows[0];
|
|
757
906
|
return {
|
|
758
907
|
id: row.id as string,
|
|
@@ -773,28 +922,28 @@ export class MemoryStore {
|
|
|
773
922
|
*/
|
|
774
923
|
async getVectors(ids: string[]): Promise<Map<string, number[]>> {
|
|
775
924
|
await this.ensureInitialized();
|
|
776
|
-
if (ids.length === 0) return new Map();
|
|
777
|
-
|
|
778
|
-
const conditions = ids.map(id => `id = '${escapeSqlLiteral(id)}'`).join(" OR ");
|
|
779
|
-
const rows = await this.table!.query()
|
|
780
|
-
.select(["id", "vector"])
|
|
781
|
-
.where(conditions)
|
|
782
|
-
.limit(ids.length)
|
|
783
|
-
.toArray();
|
|
784
|
-
|
|
785
925
|
const result = new Map<string, number[]>();
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
926
|
+
if (ids.length === 0) return result;
|
|
927
|
+
|
|
928
|
+
// 分批:避免 id 很多时 OR 链过长(如 data-checkup/memory-lint 一次补几千条向量)。
|
|
929
|
+
const BATCH = 500;
|
|
930
|
+
for (let i = 0; i < ids.length; i += BATCH) {
|
|
931
|
+
const batch = ids.slice(i, i + BATCH);
|
|
932
|
+
const conditions = batch.map(id => `id = '${escapeSqlLiteral(id)}'`).join(" OR ");
|
|
933
|
+
const rows = await this.table!.query()
|
|
934
|
+
.select(["id", "vector"])
|
|
935
|
+
.where(conditions)
|
|
936
|
+
.limit(batch.length)
|
|
937
|
+
.toArray();
|
|
938
|
+
for (const row of rows) {
|
|
939
|
+
const vec = Array.from(row.vector as Iterable<number>);
|
|
940
|
+
if (vec.length > 0) result.set(row.id as string, vec);
|
|
941
|
+
}
|
|
789
942
|
}
|
|
790
943
|
return result;
|
|
791
944
|
}
|
|
792
945
|
|
|
793
|
-
async stats(scopeFilter?: string[]): Promise<{
|
|
794
|
-
totalCount: number;
|
|
795
|
-
scopeCounts: Record<string, number>;
|
|
796
|
-
categoryCounts: Record<string, number>
|
|
797
|
-
}> {
|
|
946
|
+
async stats(scopeFilter?: string[]): Promise<MemoryStoreStats> {
|
|
798
947
|
await this.ensureInitialized();
|
|
799
948
|
|
|
800
949
|
let query = this.table!.query();
|
|
@@ -874,7 +1023,7 @@ export class MemoryStore {
|
|
|
874
1023
|
|
|
875
1024
|
async update(
|
|
876
1025
|
id: string,
|
|
877
|
-
updates:
|
|
1026
|
+
updates: MemoryStoreUpdate,
|
|
878
1027
|
scopeFilter?: string[]
|
|
879
1028
|
): Promise<MemoryEntry | null> {
|
|
880
1029
|
await this.ensureInitialized();
|
|
@@ -936,14 +1085,94 @@ export class MemoryStore {
|
|
|
936
1085
|
}
|
|
937
1086
|
}
|
|
938
1087
|
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
await this.table
|
|
1088
|
+
// Atomic upsert via mergeInsert — replaces the previous delete+add two-step,
|
|
1089
|
+
// which could lose the row entirely on a crash between steps and exposed a
|
|
1090
|
+
// window where concurrent reads saw the id momentarily vanish.
|
|
1091
|
+
await this.table!
|
|
1092
|
+
.mergeInsert("id")
|
|
1093
|
+
.whenMatchedUpdateAll()
|
|
1094
|
+
.whenNotMatchedInsertAll()
|
|
1095
|
+
.execute([updated]);
|
|
943
1096
|
|
|
944
1097
|
return updated;
|
|
945
1098
|
}
|
|
946
1099
|
|
|
1100
|
+
/**
|
|
1101
|
+
* Serialized read-modify-write for the metadata column.
|
|
1102
|
+
*
|
|
1103
|
+
* Every direct `getById → mutate metadata → update` round-trip can lose
|
|
1104
|
+
* concurrent increments (last write wins). This method funnels metadata
|
|
1105
|
+
* patches for the same id through a per-id promise queue so each patch
|
|
1106
|
+
* reads the result of the previous one.
|
|
1107
|
+
*
|
|
1108
|
+
* patchFn receives the parsed metadata object plus the freshly-read entry
|
|
1109
|
+
* (for callers that need importance/timestamp context) and returns the new
|
|
1110
|
+
* metadata object. A throwing patchFn abandons that patch — the error
|
|
1111
|
+
* propagates to the caller, later queued patches still run.
|
|
1112
|
+
*
|
|
1113
|
+
* Scope of guarantee: in-process only. Cross-process writers (multiple
|
|
1114
|
+
* mcp-server instances) and the remaining direct update() callers
|
|
1115
|
+
* (consolidation/capture/forget/prospective/conflict engines, auto-gc)
|
|
1116
|
+
* are NOT serialized by this queue. Long-term direction: make this the
|
|
1117
|
+
* only legal metadata write path.
|
|
1118
|
+
*/
|
|
1119
|
+
async patchMetadata(
|
|
1120
|
+
id: string,
|
|
1121
|
+
patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>,
|
|
1122
|
+
scopeFilter?: string[],
|
|
1123
|
+
): Promise<MemoryEntry | null> {
|
|
1124
|
+
await this.ensureInitialized();
|
|
1125
|
+
|
|
1126
|
+
// Queue key must be the resolved full UUID — otherwise a prefix caller and
|
|
1127
|
+
// a full-id caller targeting the same row would race on separate queues.
|
|
1128
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1129
|
+
let fullId: string;
|
|
1130
|
+
if (uuidRegex.test(id)) {
|
|
1131
|
+
fullId = id;
|
|
1132
|
+
} else {
|
|
1133
|
+
const resolved = await this.getById(id);
|
|
1134
|
+
if (!resolved) return null;
|
|
1135
|
+
fullId = resolved.id;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const run = async (): Promise<MemoryEntry | null> => {
|
|
1139
|
+
const entry = await this.getById(fullId);
|
|
1140
|
+
if (!entry) return null;
|
|
1141
|
+
let meta: Record<string, unknown>;
|
|
1142
|
+
try {
|
|
1143
|
+
const parsed: unknown = JSON.parse(entry.metadata || "{}");
|
|
1144
|
+
meta = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1145
|
+
? (parsed as Record<string, unknown>)
|
|
1146
|
+
: {};
|
|
1147
|
+
} catch {
|
|
1148
|
+
meta = {};
|
|
1149
|
+
}
|
|
1150
|
+
const patched = patchFn(meta, entry);
|
|
1151
|
+
return this.update(fullId, { metadata: JSON.stringify(patched) }, scopeFilter);
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
const prev = this.metadataPatchQueues.get(fullId) ?? Promise.resolve();
|
|
1155
|
+
// Isolate prior failures so one rejected patch can't wedge the queue.
|
|
1156
|
+
const result = prev.then(run, run);
|
|
1157
|
+
// The stored tail settles regardless of outcome; the caller keeps the real
|
|
1158
|
+
// rejection via `result`. Only the current tail removes itself.
|
|
1159
|
+
const tail = result.then(
|
|
1160
|
+
() => undefined,
|
|
1161
|
+
() => undefined,
|
|
1162
|
+
).then(() => {
|
|
1163
|
+
if (this.metadataPatchQueues.get(fullId) === tail) {
|
|
1164
|
+
this.metadataPatchQueues.delete(fullId);
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
this.metadataPatchQueues.set(fullId, tail);
|
|
1168
|
+
return result;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/** Number of ids with in-flight metadata patches (test/diagnostic hook). */
|
|
1172
|
+
get pendingMetadataPatchCount(): number {
|
|
1173
|
+
return this.metadataPatchQueues.size;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
947
1176
|
async bulkDelete(scopeFilter: string[], beforeTimestamp?: number): Promise<number> {
|
|
948
1177
|
await this.ensureInitialized();
|
|
949
1178
|
|
|
@@ -59,7 +59,9 @@ const RULES: CompressionRule[] = [
|
|
|
59
59
|
|
|
60
60
|
// Patterns that identify tool output blocks in conversation text.
|
|
61
61
|
// Claude Code / Codex format: command on one line, output follows.
|
|
62
|
-
|
|
62
|
+
// 末项用 (?![\s\S]) 表示「字符串绝对结尾」。不能用 \z(JS 无此锚点,会被当字面字符 z →
|
|
63
|
+
// 在输出里第一个 z 处误截断),也不能用 $(gm 模式下匹配每行尾,惰性 [\s\S]*? 会在第一行就停)。
|
|
64
|
+
const TOOL_OUTPUT_BLOCK = /^(?:\$\s+|❯\s+|>\s+)(.+)\n([\s\S]*?)(?=\n(?:\$\s+|❯\s+|>\s+)|\n\n[A-Z]|\n##|(?![\s\S]))/gm;
|
|
63
65
|
|
|
64
66
|
// Base64 image data (screenshots, etc.)
|
|
65
67
|
const BASE64_BLOCK = /(?:data:image\/[^;]+;base64,)?[A-Za-z0-9+/]{500,}={0,2}/g;
|
package/src/ui-server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsse
|
|
|
12
12
|
import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
|
|
13
13
|
import { createComponentResolver, loadConfig, loadDotEnv } from "./runtime-config.js";
|
|
14
14
|
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
15
|
+
import * as envConfig from "./env-config.js";
|
|
15
16
|
const config = (loadDotEnv(), loadConfig());
|
|
16
17
|
const getComponents = createComponentResolver(config);
|
|
17
18
|
|
|
@@ -186,7 +187,7 @@ async function handleSearch(mode: "search" | "explain" | "distill", body: Record
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
const server = Bun.serve({
|
|
189
|
-
port: readLimit(
|
|
190
|
+
port: readLimit(envConfig.uiPortRaw(), 4317, 1, 65535),
|
|
190
191
|
async fetch(request) {
|
|
191
192
|
try {
|
|
192
193
|
const url = new URL(request.url);
|