@memory-river/core 0.2.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.
Files changed (86) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +222 -0
  3. package/README.zh-TW.md +186 -0
  4. package/dist/api.d.ts +100 -0
  5. package/dist/api.js +156 -0
  6. package/dist/cognition/causal-attribution.d.ts +36 -0
  7. package/dist/cognition/causal-attribution.js +239 -0
  8. package/dist/cognition/causal-engine.d.ts +105 -0
  9. package/dist/cognition/causal-engine.js +150 -0
  10. package/dist/cognition/conflict-detector.d.ts +39 -0
  11. package/dist/cognition/conflict-detector.js +193 -0
  12. package/dist/cognition/global-working-memory.d.ts +53 -0
  13. package/dist/cognition/global-working-memory.js +211 -0
  14. package/dist/cognition/hooks-engine.d.ts +99 -0
  15. package/dist/cognition/hooks-engine.js +672 -0
  16. package/dist/cognition/ralph-core.d.ts +28 -0
  17. package/dist/cognition/ralph-core.js +104 -0
  18. package/dist/distill/concentrator-adapter.d.ts +167 -0
  19. package/dist/distill/concentrator-adapter.js +1876 -0
  20. package/dist/engine.d.ts +402 -0
  21. package/dist/engine.js +2254 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +3 -0
  24. package/dist/lifecycle/cleanup-engine.d.ts +80 -0
  25. package/dist/lifecycle/cleanup-engine.js +162 -0
  26. package/dist/lifecycle/cleanup-state.d.ts +34 -0
  27. package/dist/lifecycle/cleanup-state.js +50 -0
  28. package/dist/lifecycle/night-consolidation.d.ts +102 -0
  29. package/dist/lifecycle/night-consolidation.js +640 -0
  30. package/dist/lifecycle/night-recovery.d.ts +40 -0
  31. package/dist/lifecycle/night-recovery.js +107 -0
  32. package/dist/paths.d.ts +17 -0
  33. package/dist/paths.js +16 -0
  34. package/dist/pipeline/capsule-bridge.d.ts +35 -0
  35. package/dist/pipeline/capsule-bridge.js +86 -0
  36. package/dist/pipeline/compact-request.d.ts +30 -0
  37. package/dist/pipeline/compact-request.js +66 -0
  38. package/dist/pipeline/inbox-watcher.d.ts +112 -0
  39. package/dist/pipeline/inbox-watcher.js +1039 -0
  40. package/dist/ports.d.ts +29 -0
  41. package/dist/ports.js +1 -0
  42. package/dist/providers/embedder-v5.d.ts +46 -0
  43. package/dist/providers/embedder-v5.js +155 -0
  44. package/dist/providers/ollama-embedding.d.ts +25 -0
  45. package/dist/providers/ollama-embedding.js +166 -0
  46. package/dist/retrieval/abstractness-judge.d.ts +14 -0
  47. package/dist/retrieval/abstractness-judge.js +87 -0
  48. package/dist/retrieval/coverage-selection.d.ts +3 -0
  49. package/dist/retrieval/coverage-selection.js +53 -0
  50. package/dist/retrieval/cross-encoder-gate.d.ts +40 -0
  51. package/dist/retrieval/cross-encoder-gate.js +239 -0
  52. package/dist/retrieval/retriever-v4.d.ts +78 -0
  53. package/dist/retrieval/retriever-v4.js +1200 -0
  54. package/dist/skills/validate.d.ts +6 -0
  55. package/dist/skills/validate.js +69 -0
  56. package/dist/storage.d.ts +19 -0
  57. package/dist/storage.js +54 -0
  58. package/dist/store/aux-table-maintenance.d.ts +5 -0
  59. package/dist/store/aux-table-maintenance.js +64 -0
  60. package/dist/store/graph-enumerator.d.ts +21 -0
  61. package/dist/store/graph-enumerator.js +185 -0
  62. package/dist/store/graph-store.d.ts +107 -0
  63. package/dist/store/graph-store.js +478 -0
  64. package/dist/store/status-manager.d.ts +44 -0
  65. package/dist/store/status-manager.js +235 -0
  66. package/dist/store/store-v4.d.ts +339 -0
  67. package/dist/store/store-v4.js +2871 -0
  68. package/dist/transcript/keyword-search.d.ts +9 -0
  69. package/dist/transcript/keyword-search.js +67 -0
  70. package/dist/transcript/rehydrate-keyword.d.ts +6 -0
  71. package/dist/transcript/rehydrate-keyword.js +29 -0
  72. package/dist/transcript/rehydrate.d.ts +33 -0
  73. package/dist/transcript/rehydrate.js +285 -0
  74. package/dist/transcript/transcript-archive.d.ts +46 -0
  75. package/dist/transcript/transcript-archive.js +516 -0
  76. package/dist/types.d.ts +409 -0
  77. package/dist/types.js +104 -0
  78. package/dist/util/bounded-map.d.ts +1 -0
  79. package/dist/util/bounded-map.js +8 -0
  80. package/dist/util/rate-limiter.d.ts +12 -0
  81. package/dist/util/rate-limiter.js +54 -0
  82. package/dist/util/session-identity.d.ts +65 -0
  83. package/dist/util/session-identity.js +227 -0
  84. package/dist/util/util-hash.d.ts +1 -0
  85. package/dist/util/util-hash.js +4 -0
  86. package/package.json +59 -0
package/dist/api.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { type StorageMode } from './storage.js';
2
+ import type { EmbeddingProvider, LlmClient, Logger, Notifier, SessionFileAccess } from './ports.js';
3
+ import { type TranscriptEntry } from './transcript/rehydrate.js';
4
+ import { type ArchiveSnapshotResult } from './transcript/transcript-archive.js';
5
+ import { type ContextMessage, type EnumerationPlan, type MemoryEntry, type MemorySearchResult, type PluginConfig, type SkillCapsuleV2, type SkillDef, type SkillIndexEntry, type StatusChangeRequest, type StatusChangeResult } from './types.js';
6
+ export type { EnumerationPlan } from './types.js';
7
+ export interface MemoryRiverConfig {
8
+ dataDir: string;
9
+ ramDir: string;
10
+ storageMode?: StorageMode;
11
+ embedding?: Partial<PluginConfig['embedding']>;
12
+ retrieval?: PluginConfig['retrieval'];
13
+ cleanup?: PluginConfig['cleanup'];
14
+ cleanupEngine?: PluginConfig['cleanupEngine'];
15
+ autoRecall?: boolean;
16
+ driftThreshold?: number;
17
+ health?: PluginConfig['health'];
18
+ hooks?: PluginConfig['hooks'];
19
+ causalEngine?: PluginConfig['causalEngine'];
20
+ concentration?: PluginConfig['concentration'];
21
+ }
22
+ export interface MemoryRiverDeps {
23
+ embedder: EmbeddingProvider;
24
+ llm?: LlmClient;
25
+ logger?: Logger;
26
+ notifier?: Notifier;
27
+ sessionFiles?: SessionFileAccess;
28
+ }
29
+ export interface SessionHint {
30
+ sessionKey?: string;
31
+ sessionId?: string;
32
+ }
33
+ export interface AssembleContextOptions {
34
+ onAutoRecallResults?(event: {
35
+ query: string;
36
+ results: MemorySearchResult[];
37
+ }): void;
38
+ }
39
+ export type RehydrateRequest = {
40
+ mode: 'entry_ids';
41
+ sessionKey: string;
42
+ entryIds: number[];
43
+ bleed?: number;
44
+ limit?: number;
45
+ } | {
46
+ mode: 'time_range';
47
+ sessionKey: string;
48
+ timestamp: string;
49
+ windowMinutes?: number;
50
+ limit?: number;
51
+ } | {
52
+ mode: 'keyword';
53
+ keyword: string;
54
+ sessionKey?: string;
55
+ limit?: number;
56
+ offset?: number;
57
+ };
58
+ export type MemoryUpdate = Partial<Pick<MemoryEntry, 'text' | 'category' | 'importance' | 'metadata'>>;
59
+ export interface MemoryRiver {
60
+ start(): Promise<void>;
61
+ stop(): Promise<void>;
62
+ remember(text: string, opts?: {
63
+ category?: string;
64
+ importance?: number;
65
+ metadata?: object;
66
+ }): Promise<void>;
67
+ updateMemory(id: string, updates: MemoryUpdate): Promise<boolean>;
68
+ setMemoryStatus(req: StatusChangeRequest): Promise<StatusChangeResult>;
69
+ recall(query: string, limit?: number): Promise<MemorySearchResult[]>;
70
+ enumerate(plan: EnumerationPlan, limit?: number): Promise<MemorySearchResult[]>;
71
+ searchMemory(query: string, limit?: number): Promise<MemorySearchResult[]>;
72
+ skills: {
73
+ save(def: SkillDef): Promise<{
74
+ id: string;
75
+ }>;
76
+ search(query: string, limit?: number): Promise<SkillIndexEntry[]>;
77
+ load(name: string): Promise<SkillCapsuleV2 | null>;
78
+ list(): Promise<SkillIndexEntry[]>;
79
+ };
80
+ rehydrate(req: RehydrateRequest): Promise<TranscriptEntry[]>;
81
+ assembleContext(messages: ContextMessage[], session?: SessionHint, options?: AssembleContextOptions): Promise<{
82
+ messages: ContextMessage[];
83
+ }>;
84
+ archiveTranscript(session: SessionHint, messages: ContextMessage[]): Promise<ArchiveSnapshotResult>;
85
+ compactSessionFile(session: SessionHint, opts?: object): Promise<{
86
+ ok: boolean;
87
+ compacted: boolean;
88
+ }>;
89
+ gwm: {
90
+ on(name: string, desc: string, kw?: string[]): Promise<string>;
91
+ off(): Promise<string>;
92
+ status(): string;
93
+ update(update: object): Promise<string>;
94
+ };
95
+ maintenance: {
96
+ runCleanup(): Promise<object>;
97
+ runNightConsolidation(): Promise<object>;
98
+ };
99
+ }
100
+ export declare function createMemoryRiver(config: MemoryRiverConfig, deps: MemoryRiverDeps): MemoryRiver;
package/dist/api.js ADDED
@@ -0,0 +1,156 @@
1
+ import { resolvePaths } from './paths.js';
2
+ import { resolveRamDbPath } from './storage.js';
3
+ import { MemoryRiverEngine } from './engine.js';
4
+ import { rehydrate as rehydrateByIds, rehydrateByTime } from './transcript/rehydrate.js';
5
+ import { rehydrateByKeyword } from './transcript/rehydrate-keyword.js';
6
+ import { createTranscriptArchive } from './transcript/transcript-archive.js';
7
+ import { DEFAULT_CONFIG, } from './types.js';
8
+ const consoleLogger = {
9
+ info: (msg, meta) => meta === undefined ? console.info(msg) : console.info(msg, meta),
10
+ warn: (msg, meta) => meta === undefined ? console.warn(msg) : console.warn(msg, meta),
11
+ error: (msg, meta) => meta === undefined ? console.error(msg) : console.error(msg, meta),
12
+ };
13
+ const noopNotifier = { async notify() { } };
14
+ function mergeConfig(config, dimensions) {
15
+ const paths = resolvePaths(config);
16
+ const storage = resolveRamDbPath({
17
+ dbPath: paths.dbDir,
18
+ ramDbPath: paths.ramDbDir,
19
+ storageMode: config.storageMode,
20
+ });
21
+ return {
22
+ ...DEFAULT_CONFIG,
23
+ ...config,
24
+ embedding: { ...DEFAULT_CONFIG.embedding, ...config.embedding, dimensions },
25
+ retrieval: { ...DEFAULT_CONFIG.retrieval, ...config.retrieval },
26
+ cleanup: { ...DEFAULT_CONFIG.cleanup, ...config.cleanup },
27
+ cleanupEngine: { ...DEFAULT_CONFIG.cleanupEngine, ...config.cleanupEngine },
28
+ health: { ...DEFAULT_CONFIG.health, ...config.health },
29
+ hooks: { ...DEFAULT_CONFIG.hooks, ...config.hooks },
30
+ causalEngine: { ...DEFAULT_CONFIG.causalEngine, ...config.causalEngine },
31
+ concentration: { ...DEFAULT_CONFIG.concentration, ...config.concentration },
32
+ dbPath: paths.dbDir,
33
+ ramDbPath: storage.ramDbPath,
34
+ storageMode: config.storageMode ?? 'auto',
35
+ inboxPath: paths.inboxDir,
36
+ };
37
+ }
38
+ function textFromToolResult(result) {
39
+ return result?.content?.find?.((part) => part?.type === 'text')?.text ?? '';
40
+ }
41
+ export function createMemoryRiver(config, deps) {
42
+ const paths = resolvePaths(config);
43
+ const transcriptArchive = createTranscriptArchive(paths.transcriptsDir);
44
+ const sessionFiles = deps.sessionFiles ?? null;
45
+ const engineDeps = {
46
+ paths,
47
+ transcriptArchive,
48
+ embedder: deps.embedder,
49
+ llm: deps.llm,
50
+ logger: deps.logger ?? consoleLogger,
51
+ notifier: deps.notifier ?? noopNotifier,
52
+ deriveSessionFile: identity => sessionFiles?.resolveSessionFile(identity) ?? null,
53
+ ollamaUrl: '',
54
+ geminiApiKey: '',
55
+ deepseekApiKey: '',
56
+ };
57
+ const engineConfig = mergeConfig(config, deps.embedder.getDimensions());
58
+ const engine = new MemoryRiverEngine(engineConfig, engineDeps);
59
+ engine.configure(engineConfig, engineDeps);
60
+ return {
61
+ async start() {
62
+ engine.start();
63
+ await engine.init();
64
+ },
65
+ async stop() {
66
+ await engine.stop();
67
+ },
68
+ async remember(text, opts) {
69
+ await engine.remember(text, opts);
70
+ },
71
+ updateMemory(id, updates) {
72
+ return engine.updateMemory(id, updates);
73
+ },
74
+ setMemoryStatus(req) {
75
+ return engine.setMemoryStatus(req);
76
+ },
77
+ recall(query, limit) {
78
+ return engine.recall(query, limit);
79
+ },
80
+ enumerate(plan, limit) {
81
+ return engine.enumerate(plan, limit);
82
+ },
83
+ searchMemory(query, limit) {
84
+ return engine.searchMemory(query, limit);
85
+ },
86
+ skills: {
87
+ save(def) {
88
+ return engine.saveSkill(def);
89
+ },
90
+ search(query, limit) {
91
+ return engine.searchSkills(query, limit);
92
+ },
93
+ load(name) {
94
+ return engine.loadSkill(name);
95
+ },
96
+ list() {
97
+ return engine.listSkills();
98
+ },
99
+ },
100
+ async rehydrate(req) {
101
+ if (req.mode === 'entry_ids') {
102
+ return (await rehydrateByIds(transcriptArchive.getTranscriptPath(req.sessionKey), req.entryIds, req.bleed)).slice(0, req.limit);
103
+ }
104
+ if (req.mode === 'time_range') {
105
+ return (await rehydrateByTime(transcriptArchive.getTranscriptPath(req.sessionKey), req.timestamp, req.windowMinutes)).slice(0, req.limit);
106
+ }
107
+ return rehydrateByKeyword(paths.transcriptsDir, req.keyword, {
108
+ sessionKey: req.sessionKey,
109
+ limit: req.limit,
110
+ offset: req.offset,
111
+ });
112
+ },
113
+ assembleContext(messages, session, options) {
114
+ return engine.assemble({ messages, ...session, ...options });
115
+ },
116
+ async archiveTranscript(session, messages) {
117
+ return await engine.archiveTranscript(session, messages);
118
+ },
119
+ async compactSessionFile(session, opts = {}) {
120
+ const sessionFile = sessionFiles?.resolveSessionFile(session) ?? null;
121
+ if (!sessionFile)
122
+ return { ok: true, compacted: false };
123
+ return engine.compact({ ...session, ...opts, sessionFile });
124
+ },
125
+ gwm: {
126
+ async on(name, desc, kw) {
127
+ return textFromToolResult(await engine.executeGwmOn({
128
+ taskName: name,
129
+ taskDescription: desc,
130
+ keywords: kw,
131
+ }));
132
+ },
133
+ async off() {
134
+ return textFromToolResult(await engine.executeGwmOff());
135
+ },
136
+ status() {
137
+ return textFromToolResult(engine.executeGwmStatus());
138
+ },
139
+ async update(update) {
140
+ return textFromToolResult(await engine.executeGwmUpdate(update));
141
+ },
142
+ },
143
+ maintenance: {
144
+ async runCleanup() {
145
+ await engine.init();
146
+ await engine.runCleanupEngineNow('session-end');
147
+ return { ok: true };
148
+ },
149
+ async runNightConsolidation() {
150
+ await engine.init();
151
+ await engine.runNightConsolidatorNow();
152
+ return { ok: true };
153
+ },
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,36 @@
1
+ import type { SubsystemEffectivenessEvent } from '../types.js';
2
+ export interface InjectedMemory {
3
+ memoryId: string;
4
+ snippet: string;
5
+ source: 'autoRecall' | 'gwm_keyword' | 'manual_recall';
6
+ injectedAt: number;
7
+ viaHook?: boolean;
8
+ hookKeyword?: string;
9
+ }
10
+ export interface AttributionResult {
11
+ memoryId: string;
12
+ outcome: 'used' | 'partial' | 'unused';
13
+ score: number;
14
+ method: 'ngram' | 'embedding' | 'skipped' | 'ngram_fallback';
15
+ injectedSnippet: string;
16
+ outputSnippet: string;
17
+ fallbackReason?: string;
18
+ }
19
+ export interface SubsystemReporter {
20
+ recordEvent(event: Partial<SubsystemEffectivenessEvent>): Promise<void> | void;
21
+ }
22
+ export interface EmbeddingProvider {
23
+ embed(text: string, mode?: 'store' | 'query'): Promise<number[]>;
24
+ }
25
+ export declare class CausalAttributionEngine {
26
+ private subsystemReporter;
27
+ private embedder?;
28
+ constructor(subsystemReporter: SubsystemReporter, embedder?: EmbeddingProvider | undefined);
29
+ attributeMemoriesAsync(injected: InjectedMemory[], outputText: string, requestId: string): void;
30
+ private attributeMemories;
31
+ private scoreOne;
32
+ private ngramOverlap;
33
+ private cosineSim;
34
+ private classifyByEmbedding;
35
+ private findOutputSnippet;
36
+ }
@@ -0,0 +1,239 @@
1
+ const NGRAM_USED_THRESHOLD = 0.50;
2
+ const EMBEDDING_USED_THRESHOLD = 0.75;
3
+ const EMBEDDING_PARTIAL_THRESHOLD = 0.55;
4
+ const MIN_OUTPUT_LEN = 50;
5
+ function truncateText(text, max) {
6
+ return text.length <= max ? text : text.slice(0, max);
7
+ }
8
+ function normalizeForNgram(text) {
9
+ return text
10
+ .toLowerCase()
11
+ .replace(/\s+/g, '')
12
+ .replace(/[,。!?、;:,.!?;:"'`~()[\]{}<>《》「」『』【】]/g, '');
13
+ }
14
+ function makeNgrams(text, size) {
15
+ if (text.length < size)
16
+ return [];
17
+ const grams = [];
18
+ for (let i = 0; i <= text.length - size; i++) {
19
+ grams.push(text.slice(i, i + size));
20
+ }
21
+ return grams;
22
+ }
23
+ function cosine(a, b) {
24
+ const len = Math.min(a.length, b.length);
25
+ if (len === 0)
26
+ return 0;
27
+ let dot = 0;
28
+ let normA = 0;
29
+ let normB = 0;
30
+ for (let i = 0; i < len; i++) {
31
+ dot += a[i] * b[i];
32
+ normA += a[i] * a[i];
33
+ normB += b[i] * b[i];
34
+ }
35
+ if (normA === 0 || normB === 0)
36
+ return 0;
37
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
38
+ }
39
+ export class CausalAttributionEngine {
40
+ subsystemReporter;
41
+ embedder;
42
+ constructor(subsystemReporter, embedder) {
43
+ this.subsystemReporter = subsystemReporter;
44
+ this.embedder = embedder;
45
+ if (!embedder) {
46
+ console.warn('[causal-attribution] embedder not provided, ' +
47
+ 'embedding fallback disabled — only ngram >= USED_THRESHOLD ' +
48
+ 'will mark used, all others will be unused');
49
+ }
50
+ }
51
+ attributeMemoriesAsync(injected, outputText, requestId) {
52
+ setImmediate(() => {
53
+ void this.attributeMemories(injected, outputText, requestId).catch((err) => {
54
+ console.error('[causal-attribution] failed', err);
55
+ });
56
+ });
57
+ }
58
+ async attributeMemories(injected, outputText, requestId) {
59
+ if (!injected || injected.length === 0)
60
+ return;
61
+ if (!outputText || outputText.length < MIN_OUTPUT_LEN) {
62
+ for (const mem of injected) {
63
+ try {
64
+ await this.subsystemReporter.recordEvent({
65
+ subsystem: 'causal',
66
+ event: 'memory_attributed',
67
+ outcome: 'skipped',
68
+ entityId: mem.memoryId,
69
+ relatedId: mem.memoryId,
70
+ score: 0,
71
+ count: 1,
72
+ durationMs: 0,
73
+ metadata: {
74
+ method: 'skipped',
75
+ source: mem.source,
76
+ requestId,
77
+ reason: 'output_too_short',
78
+ outputLen: outputText?.length ?? 0,
79
+ viaHook: mem.viaHook ?? false,
80
+ hookKeyword: mem.hookKeyword ?? null,
81
+ },
82
+ });
83
+ }
84
+ catch (err) {
85
+ console.error('[causal-attribution] short-output skip write failed', {
86
+ memoryId: mem.memoryId,
87
+ error: err instanceof Error ? err.message : String(err),
88
+ });
89
+ }
90
+ }
91
+ return;
92
+ }
93
+ for (const mem of injected) {
94
+ try {
95
+ const result = await this.scoreOne(mem, outputText);
96
+ await this.subsystemReporter.recordEvent({
97
+ subsystem: 'causal',
98
+ event: 'memory_attributed',
99
+ outcome: result.outcome,
100
+ entityId: mem.memoryId,
101
+ relatedId: mem.memoryId,
102
+ score: result.score,
103
+ count: 1,
104
+ durationMs: 0,
105
+ metadata: {
106
+ method: result.method,
107
+ source: mem.source,
108
+ requestId,
109
+ injectedSnippet: result.injectedSnippet,
110
+ outputSnippet: result.outputSnippet,
111
+ outputLen: outputText.length,
112
+ fallbackReason: result.fallbackReason,
113
+ viaHook: mem.viaHook ?? false,
114
+ hookKeyword: mem.hookKeyword ?? null,
115
+ },
116
+ });
117
+ }
118
+ catch (err) {
119
+ console.error('[causal-attribution] memory attribution failed', {
120
+ memoryId: mem.memoryId,
121
+ error: err instanceof Error ? err.message : String(err),
122
+ });
123
+ }
124
+ }
125
+ }
126
+ async scoreOne(mem, outputText) {
127
+ const injectedSnippet = truncateText(mem.snippet, 80);
128
+ if (outputText.length < MIN_OUTPUT_LEN) {
129
+ return {
130
+ memoryId: mem.memoryId,
131
+ outcome: 'unused',
132
+ score: 0,
133
+ method: 'ngram',
134
+ injectedSnippet,
135
+ outputSnippet: '',
136
+ };
137
+ }
138
+ const ngramScore = this.ngramOverlap(mem.snippet, outputText);
139
+ if (ngramScore >= NGRAM_USED_THRESHOLD) {
140
+ return {
141
+ memoryId: mem.memoryId,
142
+ outcome: 'used',
143
+ score: ngramScore,
144
+ method: 'ngram',
145
+ injectedSnippet,
146
+ outputSnippet: this.findOutputSnippet(mem.snippet, outputText),
147
+ };
148
+ }
149
+ if (!this.embedder) {
150
+ return {
151
+ memoryId: mem.memoryId,
152
+ outcome: 'unused',
153
+ score: ngramScore,
154
+ method: 'ngram',
155
+ injectedSnippet,
156
+ outputSnippet: '',
157
+ };
158
+ }
159
+ try {
160
+ const embeddingScore = await this.cosineSim(mem.snippet, outputText);
161
+ return {
162
+ memoryId: mem.memoryId,
163
+ outcome: this.classifyByEmbedding(embeddingScore),
164
+ score: embeddingScore,
165
+ method: 'embedding',
166
+ injectedSnippet,
167
+ outputSnippet: embeddingScore >= EMBEDDING_PARTIAL_THRESHOLD
168
+ ? truncateText(outputText, 80)
169
+ : '',
170
+ };
171
+ }
172
+ catch (err) {
173
+ console.error('[causal-attribution] embedding fallback failed', {
174
+ memoryId: mem.memoryId,
175
+ error: err instanceof Error ? err.message : String(err),
176
+ });
177
+ const fallbackReason = err instanceof Error ? err.message : String(err);
178
+ return {
179
+ memoryId: mem.memoryId,
180
+ outcome: 'unused',
181
+ score: ngramScore,
182
+ method: 'ngram_fallback',
183
+ injectedSnippet,
184
+ outputSnippet: '',
185
+ fallbackReason,
186
+ };
187
+ }
188
+ }
189
+ ngramOverlap(a, b) {
190
+ const inj = normalizeForNgram(a);
191
+ const out = normalizeForNgram(b);
192
+ const injectedNgrams = new Set([
193
+ ...makeNgrams(inj, 2),
194
+ ...makeNgrams(inj, 3),
195
+ ...makeNgrams(inj, 4),
196
+ ]);
197
+ if (injectedNgrams.size === 0)
198
+ return 0;
199
+ const outputNgrams = new Set([
200
+ ...makeNgrams(out, 2),
201
+ ...makeNgrams(out, 3),
202
+ ...makeNgrams(out, 4),
203
+ ]);
204
+ let overlap = 0;
205
+ for (const gram of injectedNgrams) {
206
+ if (outputNgrams.has(gram))
207
+ overlap++;
208
+ }
209
+ return overlap / injectedNgrams.size;
210
+ }
211
+ async cosineSim(a, b) {
212
+ const injected = truncateText(a, 200);
213
+ const output = truncateText(b, 500);
214
+ const [aVec, bVec] = await Promise.all([
215
+ this.embedder.embed(injected, 'query'),
216
+ this.embedder.embed(output, 'query'),
217
+ ]);
218
+ return cosine(aVec, bVec);
219
+ }
220
+ classifyByEmbedding(score) {
221
+ if (score >= EMBEDDING_USED_THRESHOLD)
222
+ return 'used';
223
+ if (score >= EMBEDDING_PARTIAL_THRESHOLD)
224
+ return 'partial';
225
+ return 'unused';
226
+ }
227
+ findOutputSnippet(injected, output) {
228
+ const inj = normalizeForNgram(injected);
229
+ const out = normalizeForNgram(output);
230
+ const grams = [...makeNgrams(inj, 4), ...makeNgrams(inj, 3), ...makeNgrams(inj, 2)];
231
+ const hit = grams.find(gram => out.includes(gram));
232
+ if (!hit)
233
+ return '';
234
+ const rawIndex = output.toLowerCase().indexOf(hit.slice(0, Math.min(4, hit.length)));
235
+ if (rawIndex < 0)
236
+ return truncateText(output, 80);
237
+ return truncateText(output.slice(Math.max(0, rawIndex - 20), rawIndex + 60), 80);
238
+ }
239
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Causal - 因果突觸判定
3
+ * memory-lance-v4
4
+ */
5
+ import { MemoryStore } from "../store/store-v4.js";
6
+ import { Embedder } from "../providers/embedder-v5.js";
7
+ export type CausalAction = "UPDATE" | "CAUSAL" | "INDEPENDENT";
8
+ export interface CausalResult {
9
+ action: CausalAction;
10
+ parentId: string | null;
11
+ distance?: number;
12
+ }
13
+ /**
14
+ * ⚠️ 此參數為 Qwen3 1024d 向量空間優化(2026-04 實測預設值)。
15
+ * 更換 embedding 模型(如 OpenAI 1536d、Gemini 3072d)需自行重新校準。
16
+ * 建議值:先用目前設定跑 50 筆 query 觀察 distance 分布,再調參。
17
+ *
18
+ * @example
19
+ * // Qwen3 1024d(預設,Ollama 本地部署)
20
+ * causalEngine: { updateThreshold: 0.25, causalThreshold: 0.40, embeddingModel: "hf.co/Qwen/Qwen3-Embedding-0.6B-GGUF" }
21
+ *
22
+ * // Gemini 3072d(參考範圍,需重新觀察)
23
+ * causalEngine: { updateThreshold: 0.15, causalThreshold: 0.45, embeddingModel: "gemini-embedding-exp-0325" }
24
+ */
25
+ export interface CausalEngineConfig {
26
+ /**
27
+ * 標記目前使用的 embedding 模型維度。
28
+ * Qwen3 1024d: 向量更緊密,相似度普遍較高,需較高閾值
29
+ * Gemini 3072d: 向量更稀疏,相似度普遍較低,需較低閾值
30
+ * 預設值:1024
31
+ */
32
+ embeddingDim?: number;
33
+ /**
34
+ * ⚠️ Qwen3 1024d 向量空間優化參數(維度調整後預設值)。
35
+ * UPDATE threshold:低於此值視為同一記憶的更新(覆寫)。
36
+ * 預設值:0.25
37
+ */
38
+ updateThreshold?: number;
39
+ /**
40
+ * ⚠️ Qwen3 1024d 向量空間優化參數(維度調整後預設值)。
41
+ * CAUSAL threshold:低於此值視為因果關聯(建立 parentId 鏈)。
42
+ * 預設值:0.40
43
+ */
44
+ causalThreshold?: number;
45
+ /**
46
+ * ⚠️ Qwen3 1024d 向量空間優化參數。
47
+ * UPDATE 的字面重疊閾值(用於防止 distance 漂移誤判)。
48
+ * 當 distance < updateThreshold 時,需同時滿足字面重疊度 >= 此值,才判定 UPDATE。
49
+ * 預設值:0.30(Jaccard overlap,0=完全無重疊,1=完全相同)
50
+ * ⚠️ 設太高會讓 UPDATE 幾乎失效,建議 0.25~0.35。
51
+ */
52
+ overlapThreshold?: number;
53
+ /**
54
+ * ⚠️ 標記目前使用的 embedding 模型。
55
+ * 更換模型後強烈建議重新校準 updateThreshold / causalThreshold。
56
+ * 預設值:"hf.co/Qwen/Qwen3-Embedding-0.6B-GGUF"(Ollama 本地部署 Qwen3 1024d)
57
+ */
58
+ embeddingModel?: string;
59
+ }
60
+ export declare class CausalEngine {
61
+ private store;
62
+ private embedder;
63
+ private readonly updateThreshold;
64
+ private readonly causalThreshold;
65
+ private readonly overlapThreshold;
66
+ private readonly embeddingModel;
67
+ private readonly embeddingDim;
68
+ constructor(store: MemoryStore, embedder: Embedder, config?: CausalEngineConfig);
69
+ /**
70
+ * 判定新記憶與現有記憶的因果關係
71
+ * @param category - 新記憶的類別(可選,用於 Category-aware UPDATE 加速)
72
+ */
73
+ determineRelation(text: string, excludeId?: string, category?: string): Promise<CausalResult>;
74
+ /**
75
+ * 判斷具體因果關係(使用精準距離判斷 + 字面重疊二次確認 + Category 感知)
76
+ * ⚠️ threshold 來自 configurable 參數(Qwen3 1024d 預設:0.25 / 0.40)
77
+ *
78
+ * 🛡️ 距離漂移保護:當 distance < updateThreshold 時,
79
+ * 需同時通過字面重疊度檢查(Jaccard >= overlapThreshold)才判定 UPDATE,
80
+ * 否則降級為 CAUSAL,防止語意蒸餾後的向量飄移導致誤覆寫。
81
+ *
82
+ * 🆕 Category 同源加速:相同 category 的記憶,UPDATE 門檻放寬 50%,
83
+ * 且 distance 極低時即使字面重疊不達標也判定 UPDATE。
84
+ */
85
+ private judgeAction;
86
+ /**
87
+ * 計算兩段文字的字面重疊度(Jaccard similarity)
88
+ * - 統一小寫、移除停用詞、比較 word-level set
89
+ * - lightweight,無需額外依賴
90
+ */
91
+ private computeOverlap;
92
+ /**
93
+ * 回傳目前 embedding 模型建議的 threshold 組。
94
+ * 閾值會根據 embeddingDim 動態調整:
95
+ * - Qwen3 1024d(低維):updateThreshold=0.28, causalThreshold=0.32
96
+ * - Gemini 3072d(高維):updateThreshold=0.20, causalThreshold=0.23
97
+ */
98
+ getRecommendedThresholds(): {
99
+ updateThreshold: number;
100
+ causalThreshold: number;
101
+ overlapThreshold: number;
102
+ embeddingDim: number;
103
+ note: string;
104
+ };
105
+ }