recallnest 2.3.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -0
- package/README.md +49 -38
- package/README_CN.md +46 -31
- 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/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 +25 -4
- package/src/__tests__/access-tracker-novelty.test.ts +304 -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__/api-server-security.test.ts +102 -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__/belief-history.test.ts +220 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1328 -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 +116 -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 +945 -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 +418 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +430 -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 +262 -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 +129 -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 +780 -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__/freshness.test.ts +186 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +283 -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-client-pivot.test.ts +628 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +189 -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 +282 -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__/noise-prototype-bank.test.ts +280 -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 +333 -0
- package/src/__tests__/pivot-apply.test.ts +373 -0
- package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
- package/src/__tests__/pivot-distill.test.ts +1178 -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 +282 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -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-gate.test.ts +266 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.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-eval.test.ts +163 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -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 +654 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-read-consistency.test.ts +122 -0
- package/src/__tests__/store-schema-migration.test.ts +87 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +201 -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-terminal-roster.test.ts +127 -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__/ui-server-security.test.ts +93 -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 +90 -34
- package/src/activity-counter.ts +46 -18
- package/src/adaptive-retrieval.ts +11 -0
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +36 -9
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +142 -32
- package/src/cli.ts +268 -41
- package/src/confidence-tracker.ts +36 -18
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +255 -23
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +96 -7
- package/src/decay-engine.ts +59 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +13 -4
- package/src/dream-pipeline.ts +473 -41
- package/src/env-config.ts +118 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +402 -14
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +308 -12
- package/src/kg-extractor.ts +13 -3
- package/src/kg-store.ts +236 -44
- package/src/llm-client.ts +106 -11
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +87 -1800
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +954 -0
- package/src/mcp-tools-core.ts +637 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +26 -3
- package/src/memory-evolution.ts +73 -1
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +129 -4
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +46 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +207 -5
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/query-expander.ts +90 -9
- package/src/recall-gate.ts +305 -0
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +354 -157
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +78 -6
- 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 +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +304 -48
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +321 -57
- package/src/term-registry.ts +35 -0
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +9 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/version.ts +19 -0
- 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 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
package/src/kg-store.ts
CHANGED
|
@@ -19,15 +19,25 @@ export interface KGTriple {
|
|
|
19
19
|
subject: string;
|
|
20
20
|
predicate: string;
|
|
21
21
|
object: string;
|
|
22
|
-
/** Extraction confidence 0-1 */
|
|
22
|
+
/** Extraction confidence 0-1 (max across all extractions of this triple) */
|
|
23
23
|
confidence: number;
|
|
24
|
-
/**
|
|
24
|
+
/** First source memory that contributed this triple (read-path compat) */
|
|
25
25
|
source_memory_id: string;
|
|
26
26
|
/** Original text snippet used for extraction */
|
|
27
27
|
source_text: string;
|
|
28
|
+
/** Last time this triple was (re-)extracted */
|
|
28
29
|
timestamp: number;
|
|
30
|
+
/** Number of distinct source memories that mentioned this fact (frequency = importance signal) */
|
|
31
|
+
mention_count: number;
|
|
32
|
+
/** Timestamp of the first extraction of this triple */
|
|
33
|
+
first_seen: number;
|
|
34
|
+
/** JSON array of contributing source memory ids, capped at SOURCE_IDS_CAP */
|
|
35
|
+
source_memory_ids: string;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/** Caller-facing input: counting fields are managed internally by the store. */
|
|
39
|
+
export type KGTripleInput = Omit<KGTriple, "id" | "timestamp" | "mention_count" | "first_seen" | "source_memory_ids">;
|
|
40
|
+
|
|
31
41
|
export interface KGStoreConfig {
|
|
32
42
|
/** Reuse the same LanceDB connection path as MemoryStore */
|
|
33
43
|
dbPath: string;
|
|
@@ -56,6 +66,25 @@ function escapeSql(value: string): string {
|
|
|
56
66
|
return value.replace(/'/g, "''");
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Cap on the source_memory_ids list (row-bloat guard). mention_count itself is
|
|
71
|
+
* NOT capped: once the list is full, a returning 201st+ source can no longer be
|
|
72
|
+
* recognized and would double-count on re-extraction. Accepted error — a fact
|
|
73
|
+
* mentioned by 200+ distinct memories is extreme head-of-distribution either way.
|
|
74
|
+
*/
|
|
75
|
+
const SOURCE_IDS_CAP = 200;
|
|
76
|
+
|
|
77
|
+
/** Parse the JSON source id list, falling back to the legacy single-id column. */
|
|
78
|
+
export function parseSourceIds(raw: string | undefined | null, fallbackId?: string): string[] {
|
|
79
|
+
if (raw) {
|
|
80
|
+
try {
|
|
81
|
+
const arr = JSON.parse(raw);
|
|
82
|
+
if (Array.isArray(arr)) return arr.filter((x): x is string => typeof x === "string");
|
|
83
|
+
} catch { /* malformed — fall back below */ }
|
|
84
|
+
}
|
|
85
|
+
return fallbackId ? [fallbackId] : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
// ============================================================================
|
|
60
89
|
// KG Store
|
|
61
90
|
// ============================================================================
|
|
@@ -64,9 +93,19 @@ export class KGStore {
|
|
|
64
93
|
private db: LanceDB.Connection | null = null;
|
|
65
94
|
private table: LanceDB.Table | null = null;
|
|
66
95
|
private initPromise: Promise<void> | null = null;
|
|
96
|
+
/** In-process write serialization: concurrent workers (e.g. backfill
|
|
97
|
+
* --concurrency) would otherwise race read-modify-write AND collide on
|
|
98
|
+
* LanceDB optimistic commits. Cross-process races stay best-effort. */
|
|
99
|
+
private writeChain: Promise<unknown> = Promise.resolve();
|
|
67
100
|
|
|
68
101
|
constructor(private readonly config: KGStoreConfig) {}
|
|
69
102
|
|
|
103
|
+
private withWriteLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
104
|
+
const run = this.writeChain.then(fn, fn);
|
|
105
|
+
this.writeChain = run.catch(() => {});
|
|
106
|
+
return run;
|
|
107
|
+
}
|
|
108
|
+
|
|
70
109
|
// --------------------------------------------------------------------------
|
|
71
110
|
// Initialization
|
|
72
111
|
// --------------------------------------------------------------------------
|
|
@@ -101,6 +140,9 @@ export class KGStore {
|
|
|
101
140
|
source_memory_id: "",
|
|
102
141
|
source_text: "",
|
|
103
142
|
timestamp: 0,
|
|
143
|
+
mention_count: 0,
|
|
144
|
+
first_seen: 0,
|
|
145
|
+
source_memory_ids: "[]",
|
|
104
146
|
};
|
|
105
147
|
|
|
106
148
|
try {
|
|
@@ -115,6 +157,31 @@ export class KGStore {
|
|
|
115
157
|
}
|
|
116
158
|
}
|
|
117
159
|
|
|
160
|
+
// Defensive migration for pre-mention-count tables. Production has never
|
|
161
|
+
// created this table (KG mode was never enabled), so this only guards
|
|
162
|
+
// stale dev/test databases. Best-effort: on failure we log and continue —
|
|
163
|
+
// rowToTriple() still reads legacy rows via fallbacks.
|
|
164
|
+
try {
|
|
165
|
+
const schema = await table.schema();
|
|
166
|
+
const fields = new Set(schema.fields.map((f) => f.name));
|
|
167
|
+
const missing: Array<{ name: string; valueSql: string }> = [];
|
|
168
|
+
if (!fields.has("mention_count")) missing.push({ name: "mention_count", valueSql: "1" });
|
|
169
|
+
// Constant 0 (not a column reference — SQL-dialect-proof); rowToTriple treats
|
|
170
|
+
// first_seen<=0 as "unknown" and falls back to the row's timestamp.
|
|
171
|
+
if (!fields.has("first_seen")) missing.push({ name: "first_seen", valueSql: "0" });
|
|
172
|
+
if (!fields.has("source_memory_ids")) {
|
|
173
|
+
missing.push({ name: "source_memory_ids", valueSql: `concat('["', source_memory_id, '"]')` });
|
|
174
|
+
}
|
|
175
|
+
if (missing.length > 0) {
|
|
176
|
+
await table.addColumns(missing);
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(
|
|
180
|
+
"[recallnest] KG schema migration check failed (legacy rows still readable via fallbacks):",
|
|
181
|
+
err instanceof Error ? err.message : String(err),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
118
185
|
// Create FTS index on subject for entity lookup (optional)
|
|
119
186
|
try {
|
|
120
187
|
const indices = await table.listIndices();
|
|
@@ -138,53 +205,93 @@ export class KGStore {
|
|
|
138
205
|
// Write
|
|
139
206
|
// --------------------------------------------------------------------------
|
|
140
207
|
|
|
141
|
-
async createTriple(triple:
|
|
142
|
-
await this.
|
|
143
|
-
|
|
144
|
-
const full: KGTriple = {
|
|
145
|
-
...triple,
|
|
146
|
-
id: tripleId(triple.scope, triple.subject, triple.predicate, triple.object),
|
|
147
|
-
timestamp: Date.now(),
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Upsert: delete old then add new (LanceDB has no native upsert)
|
|
151
|
-
try {
|
|
152
|
-
await this.table!.delete(`id = '${escapeSql(full.id)}'`);
|
|
153
|
-
} catch {
|
|
154
|
-
// OK if not found
|
|
155
|
-
}
|
|
156
|
-
await this.table!.add([full]);
|
|
157
|
-
return full;
|
|
208
|
+
async createTriple(triple: KGTripleInput): Promise<KGTriple> {
|
|
209
|
+
const [result] = await this.createTriples([triple]);
|
|
210
|
+
return result;
|
|
158
211
|
}
|
|
159
212
|
|
|
160
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Upsert triples with mention counting (Memori-style: a repeated fact does
|
|
215
|
+
* not create a new row — its mention_count grows and timestamp refreshes).
|
|
216
|
+
*
|
|
217
|
+
* Counting semantics:
|
|
218
|
+
* - mention_count = number of DISTINCT source memories that contributed the triple
|
|
219
|
+
* - the same source memory re-contributing (e.g. memory re-edit) refreshes
|
|
220
|
+
* timestamp and confidence (max) but does NOT increment the count
|
|
221
|
+
* - first_seen is preserved from the first extraction
|
|
222
|
+
*
|
|
223
|
+
* Concurrency: read-modify-write — two processes racing on the same triple id
|
|
224
|
+
* may lose one count increment (accepted: the count is a best-effort signal).
|
|
225
|
+
* Row loss is NOT possible: the final write is an atomic mergeInsert, the same
|
|
226
|
+
* primitive the main memories table adopted in the 2026-07-02 surgery.
|
|
227
|
+
*/
|
|
228
|
+
async createTriples(triples: KGTripleInput[]): Promise<KGTriple[]> {
|
|
161
229
|
if (triples.length === 0) return [];
|
|
162
230
|
await this.ensureInitialized();
|
|
231
|
+
return this.withWriteLock(() => this.createTriplesLocked(triples));
|
|
232
|
+
}
|
|
163
233
|
|
|
234
|
+
private async createTriplesLocked(triples: KGTripleInput[]): Promise<KGTriple[]> {
|
|
164
235
|
const now = Date.now();
|
|
165
|
-
const fullTriples: KGTriple[] = triples.map((t) => ({
|
|
166
|
-
...t,
|
|
167
|
-
id: tripleId(t.scope, t.subject, t.predicate, t.object),
|
|
168
|
-
timestamp: now,
|
|
169
|
-
}));
|
|
170
|
-
|
|
171
|
-
// Deduplicate by id (same triple in batch)
|
|
172
|
-
const seen = new Set<string>();
|
|
173
|
-
const deduped = fullTriples.filter((t) => {
|
|
174
|
-
if (seen.has(t.id)) return false;
|
|
175
|
-
seen.add(t.id);
|
|
176
|
-
return true;
|
|
177
|
-
});
|
|
178
236
|
|
|
179
|
-
//
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
237
|
+
// 1. Aggregate within batch: same triple id → union of contributing sources
|
|
238
|
+
const byId = new Map<string, { input: KGTripleInput; sources: string[]; confidence: number }>();
|
|
239
|
+
for (const t of triples) {
|
|
240
|
+
const id = tripleId(t.scope, t.subject, t.predicate, t.object);
|
|
241
|
+
const agg = byId.get(id);
|
|
242
|
+
if (agg) {
|
|
243
|
+
if (!agg.sources.includes(t.source_memory_id)) agg.sources.push(t.source_memory_id);
|
|
244
|
+
agg.confidence = Math.max(agg.confidence, t.confidence);
|
|
245
|
+
} else {
|
|
246
|
+
byId.set(id, { input: t, sources: [t.source_memory_id], confidence: t.confidence });
|
|
247
|
+
}
|
|
185
248
|
}
|
|
186
|
-
|
|
187
|
-
|
|
249
|
+
|
|
250
|
+
// 2. Fetch existing rows for these ids
|
|
251
|
+
const idList = [...byId.keys()].map((id) => `'${escapeSql(id)}'`).join(", ");
|
|
252
|
+
const existingRows = await this.table!.query().where(`id IN (${idList})`).limit(byId.size).toArray();
|
|
253
|
+
const existingById = new Map<string, KGTriple>(
|
|
254
|
+
existingRows.map((r) => [r.id as string, rowToTriple(r)]),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// 3. Merge counts in memory
|
|
258
|
+
const toWrite: KGTriple[] = [];
|
|
259
|
+
for (const [id, agg] of byId) {
|
|
260
|
+
const prev = existingById.get(id);
|
|
261
|
+
if (prev) {
|
|
262
|
+
const knownIds = parseSourceIds(prev.source_memory_ids, prev.source_memory_id);
|
|
263
|
+
const knownSet = new Set(knownIds);
|
|
264
|
+
let count = prev.mention_count;
|
|
265
|
+
for (const s of agg.sources) {
|
|
266
|
+
if (knownSet.has(s)) continue;
|
|
267
|
+
count++;
|
|
268
|
+
knownSet.add(s);
|
|
269
|
+
if (knownIds.length < SOURCE_IDS_CAP) knownIds.push(s);
|
|
270
|
+
}
|
|
271
|
+
toWrite.push({
|
|
272
|
+
...prev,
|
|
273
|
+
confidence: Math.max(prev.confidence, agg.confidence),
|
|
274
|
+
timestamp: now,
|
|
275
|
+
mention_count: count,
|
|
276
|
+
source_memory_ids: JSON.stringify(knownIds),
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
toWrite.push({
|
|
280
|
+
...agg.input,
|
|
281
|
+
id,
|
|
282
|
+
confidence: agg.confidence,
|
|
283
|
+
source_memory_id: agg.sources[0],
|
|
284
|
+
timestamp: now,
|
|
285
|
+
mention_count: agg.sources.length,
|
|
286
|
+
first_seen: now,
|
|
287
|
+
source_memory_ids: JSON.stringify(agg.sources.slice(0, SOURCE_IDS_CAP)),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 4. Atomic idempotent write
|
|
293
|
+
await this.table!.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(toWrite);
|
|
294
|
+
return toWrite;
|
|
188
295
|
}
|
|
189
296
|
|
|
190
297
|
// --------------------------------------------------------------------------
|
|
@@ -272,6 +379,35 @@ export class KGStore {
|
|
|
272
379
|
return results;
|
|
273
380
|
}
|
|
274
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Batch reverse lookup: which triples did each of these memories contribute?
|
|
384
|
+
* Used by the consolidation engine as a second merge-evidence source.
|
|
385
|
+
* A multi-source triple appears under every contributing memory id.
|
|
386
|
+
*/
|
|
387
|
+
async getTriplesBySourceMemories(memoryIds: string[]): Promise<Map<string, KGTriple[]>> {
|
|
388
|
+
const result = new Map<string, KGTriple[]>();
|
|
389
|
+
if (memoryIds.length === 0) return result;
|
|
390
|
+
await this.ensureInitialized();
|
|
391
|
+
|
|
392
|
+
const conds = memoryIds.map((id) => {
|
|
393
|
+
const safe = escapeSql(id);
|
|
394
|
+
return `source_memory_id = '${safe}' OR source_memory_ids LIKE '%"${safe}"%'`;
|
|
395
|
+
});
|
|
396
|
+
const rows = await this.table!.query().where(conds.join(" OR ")).limit(100000).toArray();
|
|
397
|
+
|
|
398
|
+
for (const row of rows) {
|
|
399
|
+
const t = rowToTriple(row);
|
|
400
|
+
const contributors = parseSourceIds(t.source_memory_ids, t.source_memory_id);
|
|
401
|
+
for (const mid of memoryIds) {
|
|
402
|
+
if (!contributors.includes(mid)) continue; // LIKE prefilter false positive guard
|
|
403
|
+
const list = result.get(mid);
|
|
404
|
+
if (list) list.push(t);
|
|
405
|
+
else result.set(mid, [t]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
275
411
|
// --------------------------------------------------------------------------
|
|
276
412
|
// Read — Entity listing
|
|
277
413
|
// --------------------------------------------------------------------------
|
|
@@ -311,10 +447,57 @@ export class KGStore {
|
|
|
311
447
|
// Delete
|
|
312
448
|
// --------------------------------------------------------------------------
|
|
313
449
|
|
|
314
|
-
/**
|
|
450
|
+
/**
|
|
451
|
+
* Remove a source memory's contribution from the KG (forget path).
|
|
452
|
+
*
|
|
453
|
+
* Multi-source semantics: a triple evidenced by OTHER memories survives with
|
|
454
|
+
* its count decremented — forgetting one memory must not erase a fact that
|
|
455
|
+
* other memories still prove. Only when the last evidence is removed does the
|
|
456
|
+
* row get deleted. If the forgotten memory supplied the stored source_text
|
|
457
|
+
* snippet, the snippet is cleared (no forgotten-content residue).
|
|
458
|
+
*/
|
|
315
459
|
async deleteBySource(sourceMemoryId: string): Promise<void> {
|
|
316
460
|
await this.ensureInitialized();
|
|
317
|
-
|
|
461
|
+
const safe = escapeSql(sourceMemoryId);
|
|
462
|
+
|
|
463
|
+
// LIKE on the JSON array is an exact match thanks to the quotes:
|
|
464
|
+
// '%"mem-1"%' never matches "mem-12". source_memory_id covers legacy rows.
|
|
465
|
+
const rows = await this.table!.query()
|
|
466
|
+
.where(`source_memory_id = '${safe}' OR source_memory_ids LIKE '%"${safe}"%'`)
|
|
467
|
+
.limit(100000)
|
|
468
|
+
.toArray();
|
|
469
|
+
if (rows.length === 0) return;
|
|
470
|
+
|
|
471
|
+
const toDelete: string[] = [];
|
|
472
|
+
const toUpdate: KGTriple[] = [];
|
|
473
|
+
for (const row of rows) {
|
|
474
|
+
const t = rowToTriple(row);
|
|
475
|
+
const ids = parseSourceIds(t.source_memory_ids, t.source_memory_id);
|
|
476
|
+
if (!ids.includes(sourceMemoryId)) continue; // LIKE prefilter false positive — exact check in JS
|
|
477
|
+
const remaining = ids.filter((s) => s !== sourceMemoryId);
|
|
478
|
+
if (remaining.length === 0) {
|
|
479
|
+
toDelete.push(t.id);
|
|
480
|
+
} else {
|
|
481
|
+
toUpdate.push({
|
|
482
|
+
...t,
|
|
483
|
+
// count never drops below the remaining evidence count (cap-era divergence guard)
|
|
484
|
+
mention_count: Math.max(t.mention_count - 1, remaining.length),
|
|
485
|
+
source_memory_id: remaining[0],
|
|
486
|
+
source_memory_ids: JSON.stringify(remaining),
|
|
487
|
+
source_text: t.source_memory_id === sourceMemoryId ? "" : t.source_text,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
await this.withWriteLock(async () => {
|
|
493
|
+
if (toUpdate.length > 0) {
|
|
494
|
+
await this.table!.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(toUpdate);
|
|
495
|
+
}
|
|
496
|
+
if (toDelete.length > 0) {
|
|
497
|
+
const idList = toDelete.map((id) => `'${escapeSql(id)}'`).join(", ");
|
|
498
|
+
await this.table!.delete(`id IN (${idList})`);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
318
501
|
}
|
|
319
502
|
|
|
320
503
|
/** Delete all triples in scope */
|
|
@@ -340,6 +523,7 @@ export class KGStore {
|
|
|
340
523
|
// ============================================================================
|
|
341
524
|
|
|
342
525
|
function rowToTriple(row: Record<string, unknown>): KGTriple {
|
|
526
|
+
const sourceMemoryId = row.source_memory_id as string;
|
|
343
527
|
return {
|
|
344
528
|
id: row.id as string,
|
|
345
529
|
scope: (row.scope as string) ?? "global",
|
|
@@ -347,8 +531,16 @@ function rowToTriple(row: Record<string, unknown>): KGTriple {
|
|
|
347
531
|
predicate: row.predicate as string,
|
|
348
532
|
object: row.object as string,
|
|
349
533
|
confidence: Number(row.confidence),
|
|
350
|
-
source_memory_id:
|
|
534
|
+
source_memory_id: sourceMemoryId,
|
|
351
535
|
source_text: row.source_text as string,
|
|
352
536
|
timestamp: Number(row.timestamp),
|
|
537
|
+
// Legacy-row fallbacks (pre-mention-count schema; first_seen<=0 = migrated placeholder)
|
|
538
|
+
mention_count: row.mention_count != null ? Number(row.mention_count) : 1,
|
|
539
|
+
first_seen:
|
|
540
|
+
row.first_seen != null && Number(row.first_seen) > 0 ? Number(row.first_seen) : Number(row.timestamp),
|
|
541
|
+
source_memory_ids:
|
|
542
|
+
typeof row.source_memory_ids === "string" && row.source_memory_ids.length > 0
|
|
543
|
+
? row.source_memory_ids
|
|
544
|
+
: JSON.stringify(sourceMemoryId ? [sourceMemoryId] : []),
|
|
353
545
|
};
|
|
354
546
|
}
|
package/src/llm-client.ts
CHANGED
|
@@ -106,12 +106,57 @@ export interface LLMConfig {
|
|
|
106
106
|
apiKey: string;
|
|
107
107
|
model: string;
|
|
108
108
|
baseURL: string;
|
|
109
|
+
/** SDK-level retries. Undefined preserves the OpenAI SDK default. */
|
|
110
|
+
maxRetries?: number;
|
|
109
111
|
/** Request timeout in ms (default: 15000) */
|
|
110
112
|
timeoutMs?: number;
|
|
111
113
|
/** Temperature (default: 0.1 for consistency) */
|
|
112
114
|
temperature?: number;
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
export type DetailedChatTokenLimit =
|
|
118
|
+
| { parameter: "max_tokens"; value: number }
|
|
119
|
+
| { parameter: "max_completion_tokens"; value: number };
|
|
120
|
+
|
|
121
|
+
export interface DetailedChatOptions {
|
|
122
|
+
/** Per-request temperature override. Defaults to the client configuration. */
|
|
123
|
+
temperature?: number;
|
|
124
|
+
/** OpenAI-compatible JSON object response mode. */
|
|
125
|
+
responseFormat?: { type: "json_object" };
|
|
126
|
+
/** Provider extension used by Qwen-compatible endpoints. */
|
|
127
|
+
enableThinking?: boolean;
|
|
128
|
+
/** Explicitly records both the token-limit field name and its value. */
|
|
129
|
+
tokenLimit?: DetailedChatTokenLimit;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface DetailedChatUsage {
|
|
133
|
+
promptTokens: number;
|
|
134
|
+
completionTokens: number;
|
|
135
|
+
totalTokens: number;
|
|
136
|
+
cachedPromptTokens?: number;
|
|
137
|
+
reasoningTokens?: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface DetailedChatResult {
|
|
141
|
+
content: string | null;
|
|
142
|
+
finishReason: string | null;
|
|
143
|
+
responseModel: string;
|
|
144
|
+
usage: DetailedChatUsage | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface OpenAICompatibleLongChatRequest {
|
|
148
|
+
model: string;
|
|
149
|
+
messages: [
|
|
150
|
+
{ role: "system"; content: string },
|
|
151
|
+
{ role: "user"; content: string },
|
|
152
|
+
];
|
|
153
|
+
temperature: number;
|
|
154
|
+
max_tokens?: number;
|
|
155
|
+
max_completion_tokens?: number;
|
|
156
|
+
response_format?: { type: "json_object" };
|
|
157
|
+
enable_thinking?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
115
160
|
export interface DedupDecision {
|
|
116
161
|
action: "CREATE" | "MERGE" | "SKIP";
|
|
117
162
|
reason: string;
|
|
@@ -190,9 +235,13 @@ export class LLMClient {
|
|
|
190
235
|
|
|
191
236
|
constructor(config: LLMConfig, breakerConfig?: Partial<CircuitBreakerConfig>) {
|
|
192
237
|
const apiKey = resolveEnvVars(config.apiKey);
|
|
238
|
+
if (config.maxRetries !== undefined && (!Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
|
|
239
|
+
throw new Error("LLM maxRetries must be a non-negative integer");
|
|
240
|
+
}
|
|
193
241
|
this.client = new OpenAI({
|
|
194
242
|
apiKey,
|
|
195
243
|
baseURL: config.baseURL,
|
|
244
|
+
...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),
|
|
196
245
|
});
|
|
197
246
|
this.model = config.model;
|
|
198
247
|
this.temperature = config.temperature ?? 0.1;
|
|
@@ -696,29 +745,68 @@ export class LLMClient {
|
|
|
696
745
|
}
|
|
697
746
|
}
|
|
698
747
|
|
|
699
|
-
async
|
|
748
|
+
async chatLongDetailed(
|
|
749
|
+
system: string,
|
|
750
|
+
user: string,
|
|
751
|
+
options: DetailedChatOptions = {},
|
|
752
|
+
): Promise<DetailedChatResult | null> {
|
|
700
753
|
if (!this.breaker.canAttempt()) {
|
|
701
754
|
return null;
|
|
702
755
|
}
|
|
703
756
|
|
|
757
|
+
const tokenLimit = options.tokenLimit ?? { parameter: "max_tokens", value: 2000 };
|
|
758
|
+
if (!Number.isInteger(tokenLimit.value) || tokenLimit.value <= 0) {
|
|
759
|
+
throw new Error("Detailed chat token limit must be a positive integer");
|
|
760
|
+
}
|
|
761
|
+
const temperature = options.temperature ?? this.temperature;
|
|
762
|
+
if (!Number.isFinite(temperature)) {
|
|
763
|
+
throw new Error("Detailed chat temperature must be finite");
|
|
764
|
+
}
|
|
765
|
+
const request: OpenAICompatibleLongChatRequest = {
|
|
766
|
+
model: this.model,
|
|
767
|
+
messages: [
|
|
768
|
+
{ role: "system", content: system },
|
|
769
|
+
{ role: "user", content: user },
|
|
770
|
+
],
|
|
771
|
+
temperature,
|
|
772
|
+
};
|
|
773
|
+
if (tokenLimit.parameter === "max_tokens") {
|
|
774
|
+
request.max_tokens = tokenLimit.value;
|
|
775
|
+
} else {
|
|
776
|
+
request.max_completion_tokens = tokenLimit.value;
|
|
777
|
+
}
|
|
778
|
+
if (options.responseFormat) request.response_format = options.responseFormat;
|
|
779
|
+
if (options.enableThinking !== undefined) request.enable_thinking = options.enableThinking;
|
|
780
|
+
|
|
704
781
|
const controller = new AbortController();
|
|
705
782
|
const timeout = setTimeout(() => controller.abort(), this.timeoutMs * 2);
|
|
706
783
|
|
|
707
784
|
try {
|
|
708
785
|
const response = await this.client.chat.completions.create(
|
|
709
|
-
|
|
710
|
-
model: this.model,
|
|
711
|
-
messages: [
|
|
712
|
-
{ role: "system", content: system },
|
|
713
|
-
{ role: "user", content: user },
|
|
714
|
-
],
|
|
715
|
-
temperature: this.temperature,
|
|
716
|
-
max_tokens: maxTokens,
|
|
717
|
-
},
|
|
786
|
+
request,
|
|
718
787
|
{ signal: controller.signal },
|
|
719
788
|
);
|
|
720
789
|
|
|
721
|
-
const
|
|
790
|
+
const choice = response.choices[0];
|
|
791
|
+
const usage = response.usage
|
|
792
|
+
? {
|
|
793
|
+
promptTokens: response.usage.prompt_tokens,
|
|
794
|
+
completionTokens: response.usage.completion_tokens,
|
|
795
|
+
totalTokens: response.usage.total_tokens,
|
|
796
|
+
...(response.usage.prompt_tokens_details?.cached_tokens !== undefined
|
|
797
|
+
? { cachedPromptTokens: response.usage.prompt_tokens_details.cached_tokens }
|
|
798
|
+
: {}),
|
|
799
|
+
...(response.usage.completion_tokens_details?.reasoning_tokens !== undefined
|
|
800
|
+
? { reasoningTokens: response.usage.completion_tokens_details.reasoning_tokens }
|
|
801
|
+
: {}),
|
|
802
|
+
}
|
|
803
|
+
: null;
|
|
804
|
+
const result: DetailedChatResult = {
|
|
805
|
+
content: choice?.message?.content?.trim() || null,
|
|
806
|
+
finishReason: choice?.finish_reason ?? null,
|
|
807
|
+
responseModel: response.model,
|
|
808
|
+
usage,
|
|
809
|
+
};
|
|
722
810
|
this.breaker.recordSuccess();
|
|
723
811
|
return result;
|
|
724
812
|
} catch (err) {
|
|
@@ -729,6 +817,13 @@ export class LLMClient {
|
|
|
729
817
|
}
|
|
730
818
|
}
|
|
731
819
|
|
|
820
|
+
async chatLong(system: string, user: string, maxTokens = 2000): Promise<string | null> {
|
|
821
|
+
const result = await this.chatLongDetailed(system, user, {
|
|
822
|
+
tokenLimit: { parameter: "max_tokens", value: maxTokens },
|
|
823
|
+
});
|
|
824
|
+
return result?.content ?? null;
|
|
825
|
+
}
|
|
826
|
+
|
|
732
827
|
// --------------------------------------------------------------------------
|
|
733
828
|
// Constructive Retrieval — public wrappers for reconstruction pipeline
|
|
734
829
|
// --------------------------------------------------------------------------
|
package/src/llm-consolidation.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { LLMClient } from "./llm-client.js";
|
|
|
16
16
|
import type { MemoryEntry } from "./store.js";
|
|
17
17
|
import { createVersionGroup } from "./version-manager.js";
|
|
18
18
|
import { logInfo, logWarn } from "./stderr-log.js";
|
|
19
|
+
import * as envConfig from "./env-config.js";
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// Configuration
|
|
@@ -25,7 +26,7 @@ import { logInfo, logWarn } from "./stderr-log.js";
|
|
|
25
26
|
* Check if LLM-driven consolidation is enabled.
|
|
26
27
|
*/
|
|
27
28
|
export function isLLMConsolidationEnabled(): boolean {
|
|
28
|
-
return
|
|
29
|
+
return envConfig.llmConsolidation();
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
// ============================================================================
|