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.
Files changed (142) hide show
  1. package/AGENTS.md +60 -0
  2. package/README.md +166 -2
  3. package/bootstrap-kb/decisions/decisions.md +244 -0
  4. package/bootstrap-kb/glossary/glossary.md +46 -0
  5. package/bootstrap-kb/modules/.claude-plugin.md +22 -0
  6. package/bootstrap-kb/modules/agents.md.md +15 -0
  7. package/bootstrap-kb/modules/claude.md.md +15 -0
  8. package/bootstrap-kb/modules/context.md.md +15 -0
  9. package/bootstrap-kb/modules/docs.md +18 -0
  10. package/bootstrap-kb/modules/handoff.md.md +15 -0
  11. package/bootstrap-kb/modules/package-lock.json.md +15 -0
  12. package/bootstrap-kb/modules/package.json.md +15 -0
  13. package/bootstrap-kb/modules/plan.md.md +15 -0
  14. package/bootstrap-kb/modules/readme.md.md +15 -0
  15. package/bootstrap-kb/modules/scripts.md +26 -0
  16. package/bootstrap-kb/modules/spec.md.md +15 -0
  17. package/bootstrap-kb/modules/specs.md +20 -0
  18. package/bootstrap-kb/modules/src.md +51 -0
  19. package/bootstrap-kb/modules/tests.md +42 -0
  20. package/bootstrap-kb/modules/tsconfig.json.md +15 -0
  21. package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
  22. package/bootstrap-kb/overview/overview.md +40 -0
  23. package/bootstrap-kb/sources/manifest.json +950 -0
  24. package/bootstrap-kb/sources/manifest.md +227 -0
  25. package/bootstrap-kb/timeline/timeline.md +57 -0
  26. package/d.sh +3 -0
  27. package/deploy.sh +3 -0
  28. package/dist/cli/index.js +3577 -389
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1383 -138
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1917 -214
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1813 -231
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1802 -205
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1909 -248
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1861 -206
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +2341 -217
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +2350 -226
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1805 -206
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +1447 -55
  49. package/dist/ui/index.html +318 -147
  50. package/dist/ui/style.css +892 -0
  51. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  52. package/docs/MEMU_ADOPTION.md +40 -0
  53. package/docs/OPERATIONS.md +18 -0
  54. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  55. package/memory/_index.md +405 -0
  56. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  57. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  58. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  59. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  60. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  61. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  62. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  63. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  64. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  65. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  66. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  67. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  68. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  69. package/package.json +9 -2
  70. package/scripts/build.ts +6 -0
  71. package/scripts/fix-sync-gap.js +32 -0
  72. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  73. package/scripts/report-sync-gap.js +26 -0
  74. package/scripts/review-queue-auto-resolve.js +21 -0
  75. package/scripts/sync-gap-auto-heal.sh +17 -0
  76. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  77. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  78. package/src/cli/index.ts +391 -60
  79. package/src/core/consolidated-store.ts +63 -1
  80. package/src/core/consolidation-worker.ts +115 -6
  81. package/src/core/event-store.ts +14 -0
  82. package/src/core/index.ts +1 -0
  83. package/src/core/ingest-interceptor.ts +80 -0
  84. package/src/core/markdown-mirror.ts +70 -0
  85. package/src/core/md-mirror.ts +92 -0
  86. package/src/core/mongo-sync-config.ts +165 -0
  87. package/src/core/mongo-sync-worker.ts +381 -0
  88. package/src/core/retriever.ts +540 -150
  89. package/src/core/sqlite-event-store.ts +794 -7
  90. package/src/core/sqlite-wrapper.ts +8 -0
  91. package/src/core/tag-taxonomy.ts +51 -0
  92. package/src/core/turn-state.ts +159 -0
  93. package/src/core/types.ts +51 -8
  94. package/src/core/vector-store.ts +21 -3
  95. package/src/hooks/post-tool-use.ts +68 -23
  96. package/src/hooks/session-end.ts +8 -3
  97. package/src/hooks/stop.ts +96 -25
  98. package/src/hooks/user-prompt-submit.ts +44 -5
  99. package/src/server/api/chat.ts +244 -0
  100. package/src/server/api/citations.ts +3 -3
  101. package/src/server/api/events.ts +30 -5
  102. package/src/server/api/health.ts +53 -0
  103. package/src/server/api/index.ts +9 -1
  104. package/src/server/api/projects.ts +74 -0
  105. package/src/server/api/search.ts +3 -3
  106. package/src/server/api/sessions.ts +3 -3
  107. package/src/server/api/stats.ts +89 -8
  108. package/src/server/api/turns.ts +143 -0
  109. package/src/server/api/utils.ts +46 -0
  110. package/src/services/bootstrap-organizer.ts +443 -0
  111. package/src/services/codex-session-history-importer.ts +474 -0
  112. package/src/services/memory-service.ts +508 -71
  113. package/src/services/session-history-importer.ts +215 -51
  114. package/src/ui/app.js +1447 -55
  115. package/src/ui/index.html +318 -147
  116. package/src/ui/style.css +892 -0
  117. package/tests/bootstrap-organizer.test.ts +111 -0
  118. package/tests/consolidation-worker.test.ts +75 -0
  119. package/tests/ingest-interceptor.test.ts +38 -0
  120. package/tests/markdown-mirror.test.ts +85 -0
  121. package/tests/md-mirror.test.ts +50 -0
  122. package/tests/retriever-fallback-chain.test.ts +223 -0
  123. package/tests/retriever-strategy-scope.test.ts +97 -0
  124. package/tests/retriever.memu-adoption.test.ts +122 -0
  125. package/tests/sqlite-event-store-replication.test.ts +92 -0
  126. package/.claude/settings.local.json +0 -27
  127. package/.claude-memory/test.sqlite +0 -0
  128. package/.history/package_20260201112328.json +0 -45
  129. package/.history/package_20260201113602.json +0 -45
  130. package/.history/package_20260201113713.json +0 -45
  131. package/.history/package_20260201114110.json +0 -45
  132. package/.history/package_20260201114632.json +0 -46
  133. package/.history/package_20260201133143.json +0 -45
  134. package/.history/package_20260201134319.json +0 -45
  135. package/.history/package_20260201134326.json +0 -45
  136. package/.history/package_20260201134334.json +0 -45
  137. package/.history/package_20260201134912.json +0 -45
  138. package/.history/package_20260201142928.json +0 -46
  139. package/.history/package_20260201192048.json +0 -47
  140. package/.history/package_20260202114053.json +0 -49
  141. package/.history/package_20260202121115.json +0 -49
  142. 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
- { readonly: this.readOnly }
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
- const result = await this.sqliteStore.append({
417
- eventType: 'user_prompt',
418
- sessionId,
419
- timestamp: new Date(),
420
- content,
421
- metadata
422
- });
423
-
424
- // Enqueue for embedding if new
425
- if (result.success && !result.isDuplicate) {
426
- await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
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
- const result = await this.sqliteStore.append({
443
- eventType: 'agent_response',
444
- sessionId,
445
- timestamp: new Date(),
446
- content,
447
- metadata
448
- });
449
-
450
- // Enqueue for embedding if new
451
- if (result.success && !result.isDuplicate) {
452
- await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
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
- const result = await this.sqliteStore.append({
468
- eventType: 'session_summary',
469
- sessionId,
470
- timestamp: new Date(),
471
- content: summary
472
- });
473
-
474
- if (result.success && !result.isDuplicate) {
475
- await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
476
- }
477
-
478
- return result;
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
- const result = await this.sqliteStore.append({
494
- eventType: 'tool_observation',
495
- sessionId,
496
- timestamp: new Date(),
497
- content,
498
- metadata: {
499
- toolName: payload.toolName,
500
- success: payload.success
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
- return this.retriever.retrieveUnified(query, {
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
- return this.retriever.retrieve(query, options);
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: [], // Could extract topics from content if needed
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 }