claude-memory-layer 1.0.10 → 1.0.12
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +3577 -389
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1383 -138
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1917 -214
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1813 -231
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1802 -205
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1909 -248
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1861 -206
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +2341 -217
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +2350 -226
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1805 -206
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +1447 -55
- package/dist/ui/index.html +318 -147
- package/dist/ui/style.css +892 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/docs/OPERATIONS.md +18 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +9 -2
- package/scripts/build.ts +6 -0
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +391 -60
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +794 -7
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +51 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +44 -5
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +9 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +89 -8
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +508 -71
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1447 -55
- package/src/ui/index.html +318 -147
- package/src/ui/style.css +892 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/.history/package_20260202121115.json +0 -49
- package/test_access.js +0 -49
|
@@ -45,6 +45,13 @@ import { ConsolidatedStore, createConsolidatedStore } from '../core/consolidated
|
|
|
45
45
|
import { ConsolidationWorker, createConsolidationWorker } from '../core/consolidation-worker.js';
|
|
46
46
|
import { ContinuityManager, createContinuityManager } from '../core/continuity-manager.js';
|
|
47
47
|
import { GraduationWorker, createGraduationWorker, GraduationRunResult } from '../core/graduation-worker.js';
|
|
48
|
+
import { MarkdownMirror } from '../core/md-mirror.js';
|
|
49
|
+
import {
|
|
50
|
+
IngestInterceptor,
|
|
51
|
+
IngestInterceptorRegistry,
|
|
52
|
+
mergeHierarchicalMetadata
|
|
53
|
+
} from '../core/ingest-interceptor.js';
|
|
54
|
+
import { normalizeTags } from '../core/tag-taxonomy.js';
|
|
48
55
|
|
|
49
56
|
export interface MemoryServiceConfig {
|
|
50
57
|
storagePath: string;
|
|
@@ -103,18 +110,18 @@ export function getProjectStoragePath(projectPath: string): string {
|
|
|
103
110
|
const REGISTRY_PATH = path.join(os.homedir(), '.claude-code', 'memory', 'session-registry.json');
|
|
104
111
|
const SHARED_STORAGE_PATH = path.join(os.homedir(), '.claude-code', 'memory', 'shared');
|
|
105
112
|
|
|
106
|
-
interface SessionRegistryEntry {
|
|
113
|
+
export interface SessionRegistryEntry {
|
|
107
114
|
projectPath: string;
|
|
108
115
|
projectHash: string;
|
|
109
116
|
registeredAt: string;
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
interface SessionRegistry {
|
|
119
|
+
export interface SessionRegistry {
|
|
113
120
|
version: number;
|
|
114
121
|
sessions: Record<string, SessionRegistryEntry>;
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
function loadSessionRegistry(): SessionRegistry {
|
|
124
|
+
export function loadSessionRegistry(): SessionRegistry {
|
|
118
125
|
try {
|
|
119
126
|
if (fs.existsSync(REGISTRY_PATH)) {
|
|
120
127
|
const data = fs.readFileSync(REGISTRY_PATH, 'utf-8');
|
|
@@ -185,6 +192,7 @@ export class MemoryService {
|
|
|
185
192
|
private vectorWorker: VectorWorker | null = null;
|
|
186
193
|
private graduationWorker: GraduationWorker | null = null;
|
|
187
194
|
private initialized = false;
|
|
195
|
+
private readonly ingestInterceptors = new IngestInterceptorRegistry();
|
|
188
196
|
|
|
189
197
|
// Endless Mode components
|
|
190
198
|
private workingSetStore: WorkingSetStore | null = null;
|
|
@@ -200,14 +208,17 @@ export class MemoryService {
|
|
|
200
208
|
private sharedPromoter: SharedPromoter | null = null;
|
|
201
209
|
private sharedStoreConfig: SharedStoreConfig | null = null;
|
|
202
210
|
private projectHash: string | null = null;
|
|
211
|
+
private projectPath: string | null = null;
|
|
203
212
|
|
|
204
213
|
private readonly readOnly: boolean;
|
|
205
214
|
private readonly lightweightMode: boolean;
|
|
215
|
+
private readonly mdMirror: MarkdownMirror;
|
|
206
216
|
|
|
207
|
-
constructor(config: MemoryServiceConfig & { projectHash?: string; sharedStoreConfig?: SharedStoreConfig }) {
|
|
217
|
+
constructor(config: MemoryServiceConfig & { projectHash?: string; projectPath?: string; sharedStoreConfig?: SharedStoreConfig }) {
|
|
208
218
|
const storagePath = this.expandPath(config.storagePath);
|
|
209
219
|
this.readOnly = config.readOnly ?? false;
|
|
210
220
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
221
|
+
this.mdMirror = new MarkdownMirror(process.cwd());
|
|
211
222
|
|
|
212
223
|
// Ensure storage directory exists (only if not read-only)
|
|
213
224
|
if (!this.readOnly && !fs.existsSync(storagePath)) {
|
|
@@ -216,6 +227,7 @@ export class MemoryService {
|
|
|
216
227
|
|
|
217
228
|
// Store project hash for shared store operations
|
|
218
229
|
this.projectHash = config.projectHash || null;
|
|
230
|
+
this.projectPath = config.projectPath || null;
|
|
219
231
|
// Default: shared store enabled
|
|
220
232
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
221
233
|
|
|
@@ -223,7 +235,10 @@ export class MemoryService {
|
|
|
223
235
|
// This is always used for writes and is the source of truth
|
|
224
236
|
this.sqliteStore = new SQLiteEventStore(
|
|
225
237
|
path.join(storagePath, 'events.sqlite'),
|
|
226
|
-
{
|
|
238
|
+
{
|
|
239
|
+
readonly: this.readOnly,
|
|
240
|
+
markdownMirrorRoot: storagePath
|
|
241
|
+
}
|
|
227
242
|
);
|
|
228
243
|
|
|
229
244
|
// Initialize ANALYTICS store: DuckDB (optional, for server reads)
|
|
@@ -264,6 +279,7 @@ export class MemoryService {
|
|
|
264
279
|
this.embedder,
|
|
265
280
|
this.matcher
|
|
266
281
|
);
|
|
282
|
+
this.retriever.setQueryRewriter((q) => this.rewriteQueryIntent(q));
|
|
267
283
|
this.graduation = createGraduationPipeline(this.sqliteStore as unknown as EventStore);
|
|
268
284
|
}
|
|
269
285
|
|
|
@@ -377,6 +393,105 @@ export class MemoryService {
|
|
|
377
393
|
this.retriever.setSharedStores(this.sharedStore, this.sharedVectorStore);
|
|
378
394
|
}
|
|
379
395
|
|
|
396
|
+
registerIngestBefore(interceptor: IngestInterceptor): () => void {
|
|
397
|
+
return this.ingestInterceptors.registerBefore(interceptor);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
registerIngestAfter(interceptor: IngestInterceptor): () => void {
|
|
401
|
+
return this.ingestInterceptors.registerAfter(interceptor);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
registerIngestOnError(interceptor: IngestInterceptor): () => void {
|
|
405
|
+
return this.ingestInterceptors.registerOnError(interceptor);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private async ingestWithInterceptors(
|
|
409
|
+
operation: 'user_prompt' | 'agent_response' | 'session_summary' | 'tool_observation',
|
|
410
|
+
input: MemoryEventInput,
|
|
411
|
+
onSuccess?: (eventId: string) => Promise<void>
|
|
412
|
+
): Promise<AppendResult> {
|
|
413
|
+
const normalizedInput: MemoryEventInput = {
|
|
414
|
+
...input,
|
|
415
|
+
metadata: mergeHierarchicalMetadata(
|
|
416
|
+
{
|
|
417
|
+
ingest: {
|
|
418
|
+
operation,
|
|
419
|
+
pipeline: 'default',
|
|
420
|
+
ts: new Date().toISOString()
|
|
421
|
+
},
|
|
422
|
+
...(this.projectHash
|
|
423
|
+
? {
|
|
424
|
+
scope: {
|
|
425
|
+
project: {
|
|
426
|
+
hash: this.projectHash,
|
|
427
|
+
...(this.projectPath ? { path: this.projectPath } : {})
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
tags: [`proj:${this.projectHash}`]
|
|
431
|
+
}
|
|
432
|
+
: {})
|
|
433
|
+
},
|
|
434
|
+
input.metadata
|
|
435
|
+
)
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (this.projectHash && normalizedInput.metadata) {
|
|
439
|
+
const meta = normalizedInput.metadata as Record<string, unknown>;
|
|
440
|
+
const currentTags = Array.isArray(meta.tags)
|
|
441
|
+
? meta.tags.filter((x): x is string => typeof x === 'string')
|
|
442
|
+
: [];
|
|
443
|
+
const projectTag = `proj:${this.projectHash}`;
|
|
444
|
+
if (!currentTags.includes(projectTag)) {
|
|
445
|
+
meta.tags = [...currentTags, projectTag];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (normalizedInput.metadata) {
|
|
450
|
+
const meta = normalizedInput.metadata as Record<string, unknown>;
|
|
451
|
+
const normalizedTags = normalizeTags(meta.tags);
|
|
452
|
+
if (normalizedTags.length > 0) {
|
|
453
|
+
meta.tags = normalizedTags;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await this.ingestInterceptors.run('before', {
|
|
458
|
+
operation,
|
|
459
|
+
sessionId: normalizedInput.sessionId,
|
|
460
|
+
event: normalizedInput
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const result = await this.sqliteStore.append(normalizedInput);
|
|
465
|
+
if (result.success && !result.isDuplicate) {
|
|
466
|
+
if (onSuccess) {
|
|
467
|
+
await onSuccess(result.eventId);
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
await this.mdMirror.append(normalizedInput, result.eventId);
|
|
471
|
+
} catch {
|
|
472
|
+
// non-breaking markdown mirror write
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await this.ingestInterceptors.run('after', {
|
|
477
|
+
operation,
|
|
478
|
+
sessionId: normalizedInput.sessionId,
|
|
479
|
+
event: normalizedInput
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return result;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
485
|
+
await this.ingestInterceptors.run('error', {
|
|
486
|
+
operation,
|
|
487
|
+
sessionId: normalizedInput.sessionId,
|
|
488
|
+
event: normalizedInput,
|
|
489
|
+
error: normalizedError
|
|
490
|
+
});
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
380
495
|
/**
|
|
381
496
|
* Start a new session
|
|
382
497
|
*/
|
|
@@ -413,20 +528,19 @@ export class MemoryService {
|
|
|
413
528
|
): Promise<AppendResult> {
|
|
414
529
|
await this.initialize();
|
|
415
530
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return result;
|
|
531
|
+
return this.ingestWithInterceptors(
|
|
532
|
+
'user_prompt',
|
|
533
|
+
{
|
|
534
|
+
eventType: 'user_prompt',
|
|
535
|
+
sessionId,
|
|
536
|
+
timestamp: new Date(),
|
|
537
|
+
content,
|
|
538
|
+
metadata
|
|
539
|
+
},
|
|
540
|
+
async (eventId) => {
|
|
541
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, content);
|
|
542
|
+
}
|
|
543
|
+
);
|
|
430
544
|
}
|
|
431
545
|
|
|
432
546
|
/**
|
|
@@ -439,20 +553,19 @@ export class MemoryService {
|
|
|
439
553
|
): Promise<AppendResult> {
|
|
440
554
|
await this.initialize();
|
|
441
555
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return result;
|
|
556
|
+
return this.ingestWithInterceptors(
|
|
557
|
+
'agent_response',
|
|
558
|
+
{
|
|
559
|
+
eventType: 'agent_response',
|
|
560
|
+
sessionId,
|
|
561
|
+
timestamp: new Date(),
|
|
562
|
+
content,
|
|
563
|
+
metadata
|
|
564
|
+
},
|
|
565
|
+
async (eventId) => {
|
|
566
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, content);
|
|
567
|
+
}
|
|
568
|
+
);
|
|
456
569
|
}
|
|
457
570
|
|
|
458
571
|
/**
|
|
@@ -460,22 +573,24 @@ export class MemoryService {
|
|
|
460
573
|
*/
|
|
461
574
|
async storeSessionSummary(
|
|
462
575
|
sessionId: string,
|
|
463
|
-
summary: string
|
|
576
|
+
summary: string,
|
|
577
|
+
metadata?: Record<string, unknown>
|
|
464
578
|
): Promise<AppendResult> {
|
|
465
579
|
await this.initialize();
|
|
466
580
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
581
|
+
return this.ingestWithInterceptors(
|
|
582
|
+
'session_summary',
|
|
583
|
+
{
|
|
584
|
+
eventType: 'session_summary',
|
|
585
|
+
sessionId,
|
|
586
|
+
timestamp: new Date(),
|
|
587
|
+
content: summary,
|
|
588
|
+
metadata
|
|
589
|
+
},
|
|
590
|
+
async (eventId) => {
|
|
591
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, summary);
|
|
592
|
+
}
|
|
593
|
+
);
|
|
479
594
|
}
|
|
480
595
|
|
|
481
596
|
/**
|
|
@@ -490,28 +605,31 @@ export class MemoryService {
|
|
|
490
605
|
// Create content for storage (JSON stringified payload)
|
|
491
606
|
const content = JSON.stringify(payload);
|
|
492
607
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
608
|
+
// Extract turnId from payload metadata if present (set by PostToolUse hook)
|
|
609
|
+
const turnId = payload.metadata?.turnId;
|
|
610
|
+
|
|
611
|
+
return this.ingestWithInterceptors(
|
|
612
|
+
'tool_observation',
|
|
613
|
+
{
|
|
614
|
+
eventType: 'tool_observation',
|
|
615
|
+
sessionId,
|
|
616
|
+
timestamp: new Date(),
|
|
617
|
+
content,
|
|
618
|
+
metadata: {
|
|
619
|
+
toolName: payload.toolName,
|
|
620
|
+
success: payload.success,
|
|
621
|
+
...(turnId ? { turnId } : {})
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
async (eventId) => {
|
|
625
|
+
const embeddingContent = createToolObservationEmbedding(
|
|
626
|
+
payload.toolName,
|
|
627
|
+
payload.metadata || {},
|
|
628
|
+
payload.success
|
|
629
|
+
);
|
|
630
|
+
await this.sqliteStore.enqueueForEmbedding(eventId, embeddingContent);
|
|
501
631
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// Create embedding content (optimized for search)
|
|
505
|
-
if (result.success && !result.isDuplicate) {
|
|
506
|
-
const embeddingContent = createToolObservationEmbedding(
|
|
507
|
-
payload.toolName,
|
|
508
|
-
payload.metadata || {},
|
|
509
|
-
payload.success
|
|
510
|
-
);
|
|
511
|
-
await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
return result;
|
|
632
|
+
);
|
|
515
633
|
}
|
|
516
634
|
|
|
517
635
|
/**
|
|
@@ -524,6 +642,10 @@ export class MemoryService {
|
|
|
524
642
|
minScore?: number;
|
|
525
643
|
sessionId?: string;
|
|
526
644
|
includeShared?: boolean;
|
|
645
|
+
adaptiveRerank?: boolean;
|
|
646
|
+
intentRewrite?: boolean;
|
|
647
|
+
projectScopeMode?: 'strict' | 'prefer' | 'global';
|
|
648
|
+
allowedProjectHashes?: string[];
|
|
527
649
|
}
|
|
528
650
|
): Promise<UnifiedRetrievalResult> {
|
|
529
651
|
await this.initialize();
|
|
@@ -531,16 +653,177 @@ export class MemoryService {
|
|
|
531
653
|
// Note: Pending embeddings are processed by the background worker
|
|
532
654
|
// Don't block retrieval - search with whatever vectors are available
|
|
533
655
|
|
|
656
|
+
const rerankWeights = await this.getRerankWeights(options?.adaptiveRerank === true);
|
|
657
|
+
|
|
534
658
|
// Use unified retrieval if shared search is requested
|
|
659
|
+
let result: UnifiedRetrievalResult;
|
|
660
|
+
|
|
535
661
|
if (options?.includeShared && this.sharedStore) {
|
|
536
|
-
|
|
662
|
+
result = await this.retriever.retrieveUnified(query, {
|
|
537
663
|
...options,
|
|
664
|
+
intentRewrite: options?.intentRewrite === true,
|
|
665
|
+
rerankWeights,
|
|
538
666
|
includeShared: true,
|
|
539
|
-
projectHash: this.projectHash || undefined
|
|
667
|
+
projectHash: this.projectHash || undefined,
|
|
668
|
+
projectScopeMode: options?.projectScopeMode ?? (this.projectHash ? 'strict' : 'global'),
|
|
669
|
+
allowedProjectHashes: options?.allowedProjectHashes
|
|
670
|
+
});
|
|
671
|
+
} else {
|
|
672
|
+
result = await this.retriever.retrieve(query, {
|
|
673
|
+
...options,
|
|
674
|
+
intentRewrite: options?.intentRewrite === true,
|
|
675
|
+
rerankWeights,
|
|
676
|
+
projectHash: this.projectHash || undefined,
|
|
677
|
+
projectScopeMode: options?.projectScopeMode ?? (this.projectHash ? 'strict' : 'global'),
|
|
678
|
+
allowedProjectHashes: options?.allowedProjectHashes
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
const selectedEventIds = result.memories.map((m) => m.event.id);
|
|
684
|
+
const selectedDetails = (result.selectedDebug || []).map((d) => ({
|
|
685
|
+
eventId: d.eventId,
|
|
686
|
+
score: d.score,
|
|
687
|
+
semanticScore: d.semanticScore,
|
|
688
|
+
lexicalScore: d.lexicalScore,
|
|
689
|
+
recencyScore: d.recencyScore,
|
|
690
|
+
}));
|
|
691
|
+
const candidateDetails = (result.candidateDebug || []).map((d) => ({
|
|
692
|
+
eventId: d.eventId,
|
|
693
|
+
score: d.score,
|
|
694
|
+
semanticScore: d.semanticScore,
|
|
695
|
+
lexicalScore: d.lexicalScore,
|
|
696
|
+
recencyScore: d.recencyScore,
|
|
697
|
+
}));
|
|
698
|
+
const candidateEventIds = candidateDetails.length > 0
|
|
699
|
+
? candidateDetails.map((d) => d.eventId)
|
|
700
|
+
: selectedEventIds;
|
|
701
|
+
await this.sqliteStore.recordRetrievalTrace({
|
|
702
|
+
sessionId: options?.sessionId,
|
|
703
|
+
projectHash: this.projectHash || undefined,
|
|
704
|
+
queryText: query,
|
|
705
|
+
strategy: options?.strategy || 'auto',
|
|
706
|
+
candidateEventIds,
|
|
707
|
+
selectedEventIds,
|
|
708
|
+
candidateDetails,
|
|
709
|
+
selectedDetails,
|
|
710
|
+
confidence: result.matchResult.confidence,
|
|
711
|
+
fallbackTrace: result.fallbackTrace || []
|
|
712
|
+
});
|
|
713
|
+
} catch {
|
|
714
|
+
// non-blocking telemetry
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private getConfiguredRerankWeights(): { semantic: number; lexical: number; recency: number } | undefined {
|
|
721
|
+
const semantic = Number(process.env.MEMORY_RERANK_WEIGHT_SEMANTIC ?? '');
|
|
722
|
+
const lexical = Number(process.env.MEMORY_RERANK_WEIGHT_LEXICAL ?? '');
|
|
723
|
+
const recency = Number(process.env.MEMORY_RERANK_WEIGHT_RECENCY ?? '');
|
|
724
|
+
|
|
725
|
+
const allFinite = [semantic, lexical, recency].every((v) => Number.isFinite(v));
|
|
726
|
+
if (!allFinite) return undefined;
|
|
727
|
+
|
|
728
|
+
const nonNegative = [semantic, lexical, recency].every((v) => v >= 0);
|
|
729
|
+
const total = semantic + lexical + recency;
|
|
730
|
+
if (!nonNegative || total <= 0) return undefined;
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
semantic: semantic / total,
|
|
734
|
+
lexical: lexical / total,
|
|
735
|
+
recency: recency / total,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private async getRerankWeights(adaptive: boolean): Promise<{ semantic: number; lexical: number; recency: number } | undefined> {
|
|
740
|
+
const configured = this.getConfiguredRerankWeights();
|
|
741
|
+
if (configured) return configured;
|
|
742
|
+
if (adaptive) return this.getAdaptiveRerankWeights();
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private async rewriteQueryIntent(query: string): Promise<string | null> {
|
|
747
|
+
if (process.env.MEMORY_INTENT_REWRITE_ENABLED !== '1') return null;
|
|
748
|
+
|
|
749
|
+
const apiUrl = process.env.COMPANY_STOCK_API_URL || process.env.COMPANY_INT_API_URL;
|
|
750
|
+
if (!apiUrl) return null;
|
|
751
|
+
|
|
752
|
+
const controller = new AbortController();
|
|
753
|
+
const timeoutMs = Number(process.env.MEMORY_INTENT_REWRITE_TIMEOUT_MS || 5000);
|
|
754
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
const prompt = [
|
|
758
|
+
'Rewrite user query for memory retrieval intent expansion.',
|
|
759
|
+
'Return plain text only, one line, no markdown.',
|
|
760
|
+
`Query: ${query}`,
|
|
761
|
+
].join('\n');
|
|
762
|
+
|
|
763
|
+
const res = await fetch(apiUrl, {
|
|
764
|
+
method: 'POST',
|
|
765
|
+
headers: {
|
|
766
|
+
'Content-Type': 'application/json',
|
|
767
|
+
Accept: '*/*',
|
|
768
|
+
Origin: process.env.COMPANY_INT_ORIGIN || 'http://company-int.aplusai.ai',
|
|
769
|
+
Referer: process.env.COMPANY_INT_REFERER || 'http://company-int.aplusai.ai/',
|
|
770
|
+
},
|
|
771
|
+
body: JSON.stringify({
|
|
772
|
+
question: prompt,
|
|
773
|
+
company_name: null,
|
|
774
|
+
conversation_id: null,
|
|
775
|
+
}),
|
|
776
|
+
signal: controller.signal,
|
|
540
777
|
});
|
|
778
|
+
|
|
779
|
+
const text = (await res.text()).trim();
|
|
780
|
+
if (!text) return null;
|
|
781
|
+
|
|
782
|
+
const oneLine = text
|
|
783
|
+
.replace(/^data:\s*/gm, '')
|
|
784
|
+
.split(/\r?\n/)
|
|
785
|
+
.map((x) => x.trim())
|
|
786
|
+
.filter(Boolean)
|
|
787
|
+
.join(' ')
|
|
788
|
+
.slice(0, 240);
|
|
789
|
+
|
|
790
|
+
if (!oneLine || oneLine.toLowerCase() === query.toLowerCase()) return null;
|
|
791
|
+
return oneLine;
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
794
|
+
} finally {
|
|
795
|
+
clearTimeout(timeout);
|
|
541
796
|
}
|
|
797
|
+
}
|
|
542
798
|
|
|
543
|
-
|
|
799
|
+
private async getAdaptiveRerankWeights(): Promise<{ semantic: number; lexical: number; recency: number } | undefined> {
|
|
800
|
+
try {
|
|
801
|
+
const s = await this.sqliteStore.getHelpfulnessStats();
|
|
802
|
+
if (s.totalEvaluated < 20) return undefined;
|
|
803
|
+
|
|
804
|
+
// base weights
|
|
805
|
+
let semantic = 0.7;
|
|
806
|
+
let lexical = 0.2;
|
|
807
|
+
let recency = 0.1;
|
|
808
|
+
|
|
809
|
+
if (s.avgScore < 0.45) {
|
|
810
|
+
semantic -= 0.1;
|
|
811
|
+
lexical += 0.1;
|
|
812
|
+
} else if (s.avgScore > 0.75) {
|
|
813
|
+
semantic += 0.05;
|
|
814
|
+
lexical -= 0.05;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (s.unhelpful > s.helpful) {
|
|
818
|
+
recency += 0.05;
|
|
819
|
+
semantic -= 0.03;
|
|
820
|
+
lexical -= 0.02;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return { semantic, lexical, recency };
|
|
824
|
+
} catch {
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
544
827
|
}
|
|
545
828
|
|
|
546
829
|
/**
|
|
@@ -594,6 +877,30 @@ export class MemoryService {
|
|
|
594
877
|
/**
|
|
595
878
|
* Get memory statistics
|
|
596
879
|
*/
|
|
880
|
+
|
|
881
|
+
async getOutboxStats(): Promise<{
|
|
882
|
+
embedding: { pending: number; processing: number; failed: number; total: number };
|
|
883
|
+
vector: { pending: number; processing: number; failed: number; total: number };
|
|
884
|
+
}> {
|
|
885
|
+
await this.initialize();
|
|
886
|
+
return this.sqliteStore.getOutboxStats();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async getRetrievalTraceStats(): Promise<{
|
|
890
|
+
totalQueries: number;
|
|
891
|
+
avgCandidateCount: number;
|
|
892
|
+
avgSelectedCount: number;
|
|
893
|
+
selectionRate: number;
|
|
894
|
+
}> {
|
|
895
|
+
await this.initialize();
|
|
896
|
+
return this.sqliteStore.getRetrievalTraceStats();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async getRecentRetrievalTraces(limit: number = 50) {
|
|
900
|
+
await this.initialize();
|
|
901
|
+
return this.sqliteStore.getRecentRetrievalTraces(limit);
|
|
902
|
+
}
|
|
903
|
+
|
|
597
904
|
async getStats(): Promise<{
|
|
598
905
|
totalEvents: number;
|
|
599
906
|
vectorCount: number;
|
|
@@ -852,6 +1159,37 @@ export class MemoryService {
|
|
|
852
1159
|
return this.consolidatedStore.getAll({ limit });
|
|
853
1160
|
}
|
|
854
1161
|
|
|
1162
|
+
/**
|
|
1163
|
+
* Extract topic keywords from event content (markdown headings and key terms)
|
|
1164
|
+
*/
|
|
1165
|
+
private extractTopicsFromContent(content: string): string[] {
|
|
1166
|
+
const topics: Set<string> = new Set();
|
|
1167
|
+
|
|
1168
|
+
// Extract markdown headings (## heading)
|
|
1169
|
+
const headings = content.match(/^#{1,3}\s+(.+)$/gm);
|
|
1170
|
+
if (headings) {
|
|
1171
|
+
for (const h of headings.slice(0, 5)) {
|
|
1172
|
+
const text = h.replace(/^#+\s+/, '').replace(/[*_`#]/g, '').trim();
|
|
1173
|
+
if (text.length > 2 && text.length < 50) {
|
|
1174
|
+
topics.add(text);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Extract bold terms (**term**)
|
|
1180
|
+
const boldTerms = content.match(/\*\*([^*]+)\*\*/g);
|
|
1181
|
+
if (boldTerms) {
|
|
1182
|
+
for (const b of boldTerms.slice(0, 5)) {
|
|
1183
|
+
const text = b.replace(/\*\*/g, '').trim();
|
|
1184
|
+
if (text.length > 2 && text.length < 30) {
|
|
1185
|
+
topics.add(text);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return Array.from(topics).slice(0, 5);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
855
1193
|
/**
|
|
856
1194
|
* Increment access count for memories that were used in prompts
|
|
857
1195
|
*/
|
|
@@ -880,7 +1218,7 @@ export class MemoryService {
|
|
|
880
1218
|
return events.map(event => ({
|
|
881
1219
|
memoryId: event.id,
|
|
882
1220
|
summary: event.content.substring(0, 200) + (event.content.length > 200 ? '...' : ''),
|
|
883
|
-
topics:
|
|
1221
|
+
topics: this.extractTopicsFromContent(event.content),
|
|
884
1222
|
accessCount: (event as any).access_count || 0,
|
|
885
1223
|
lastAccessed: (event as any).last_accessed_at || null,
|
|
886
1224
|
confidence: 1.0,
|
|
@@ -905,6 +1243,51 @@ export class MemoryService {
|
|
|
905
1243
|
return [];
|
|
906
1244
|
}
|
|
907
1245
|
|
|
1246
|
+
/**
|
|
1247
|
+
* Record a memory retrieval for helpfulness tracking
|
|
1248
|
+
*/
|
|
1249
|
+
async recordRetrieval(eventId: string, sessionId: string, score: number, query: string): Promise<void> {
|
|
1250
|
+
await this.initialize();
|
|
1251
|
+
await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Evaluate helpfulness of retrievals in a session (called at session end)
|
|
1256
|
+
*/
|
|
1257
|
+
async evaluateSessionHelpfulness(sessionId: string): Promise<void> {
|
|
1258
|
+
await this.initialize();
|
|
1259
|
+
await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Get most helpful memories ranked by helpfulness score
|
|
1264
|
+
*/
|
|
1265
|
+
async getHelpfulMemories(limit: number = 10): Promise<Array<{
|
|
1266
|
+
eventId: string;
|
|
1267
|
+
summary: string;
|
|
1268
|
+
helpfulnessScore: number;
|
|
1269
|
+
accessCount: number;
|
|
1270
|
+
evaluationCount: number;
|
|
1271
|
+
}>> {
|
|
1272
|
+
await this.initialize();
|
|
1273
|
+
return this.sqliteStore.getHelpfulMemories(limit);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Get helpfulness statistics for dashboard
|
|
1278
|
+
*/
|
|
1279
|
+
async getHelpfulnessStats(): Promise<{
|
|
1280
|
+
avgScore: number;
|
|
1281
|
+
totalEvaluated: number;
|
|
1282
|
+
totalRetrievals: number;
|
|
1283
|
+
helpful: number;
|
|
1284
|
+
neutral: number;
|
|
1285
|
+
unhelpful: number;
|
|
1286
|
+
}> {
|
|
1287
|
+
await this.initialize();
|
|
1288
|
+
return this.sqliteStore.getHelpfulnessStats();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
908
1291
|
/**
|
|
909
1292
|
* Mark a consolidated memory as accessed
|
|
910
1293
|
*/
|
|
@@ -979,6 +1362,58 @@ export class MemoryService {
|
|
|
979
1362
|
};
|
|
980
1363
|
}
|
|
981
1364
|
|
|
1365
|
+
// ============================================================
|
|
1366
|
+
// Turn Grouping Methods
|
|
1367
|
+
// ============================================================
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Get events grouped by turn for a session
|
|
1371
|
+
*/
|
|
1372
|
+
async getSessionTurns(sessionId: string, options?: { limit?: number; offset?: number }): Promise<Array<{
|
|
1373
|
+
turnId: string;
|
|
1374
|
+
events: MemoryEvent[];
|
|
1375
|
+
startedAt: Date;
|
|
1376
|
+
promptPreview: string;
|
|
1377
|
+
eventCount: number;
|
|
1378
|
+
toolCount: number;
|
|
1379
|
+
hasResponse: boolean;
|
|
1380
|
+
}>> {
|
|
1381
|
+
await this.initialize();
|
|
1382
|
+
return this.sqliteStore.getSessionTurns(sessionId, options);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Get all events for a specific turn
|
|
1387
|
+
*/
|
|
1388
|
+
async getEventsByTurn(turnId: string): Promise<MemoryEvent[]> {
|
|
1389
|
+
await this.initialize();
|
|
1390
|
+
return this.sqliteStore.getEventsByTurn(turnId);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Count total turns for a session
|
|
1395
|
+
*/
|
|
1396
|
+
async countSessionTurns(sessionId: string): Promise<number> {
|
|
1397
|
+
await this.initialize();
|
|
1398
|
+
return this.sqliteStore.countSessionTurns(sessionId);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Backfill turn_ids from metadata for events stored before the migration
|
|
1403
|
+
*/
|
|
1404
|
+
async backfillTurnIds(): Promise<number> {
|
|
1405
|
+
await this.initialize();
|
|
1406
|
+
return this.sqliteStore.backfillTurnIds();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Delete all events for a session (for force reimport)
|
|
1411
|
+
*/
|
|
1412
|
+
async deleteSessionEvents(sessionId: string): Promise<number> {
|
|
1413
|
+
await this.initialize();
|
|
1414
|
+
return this.sqliteStore.deleteSessionEvents(sessionId);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
982
1417
|
/**
|
|
983
1418
|
* Format Endless Mode context for Claude
|
|
984
1419
|
*/
|
|
@@ -1146,6 +1581,7 @@ export function getMemoryServiceForProject(
|
|
|
1146
1581
|
serviceCache.set(hash, new MemoryService({
|
|
1147
1582
|
storagePath,
|
|
1148
1583
|
projectHash: hash,
|
|
1584
|
+
projectPath,
|
|
1149
1585
|
// Override shared store config - hooks don't need DuckDB
|
|
1150
1586
|
sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
|
|
1151
1587
|
analyticsEnabled: false // Hooks don't need DuckDB
|
|
@@ -1187,6 +1623,7 @@ export function getLightweightMemoryService(sessionId: string): MemoryService {
|
|
|
1187
1623
|
serviceCache.set(key, new MemoryService({
|
|
1188
1624
|
storagePath,
|
|
1189
1625
|
projectHash: projectInfo?.projectHash,
|
|
1626
|
+
projectPath: projectInfo?.projectPath,
|
|
1190
1627
|
lightweightMode: true, // Skip embedder/vector/workers
|
|
1191
1628
|
analyticsEnabled: false,
|
|
1192
1629
|
sharedStoreConfig: { enabled: false }
|