claude-memory-layer 1.0.11 → 1.0.13

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 (101) 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 +2389 -286
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1017 -132
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1347 -202
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1339 -194
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1343 -198
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1351 -206
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1347 -202
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +1436 -211
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +1445 -220
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1345 -199
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +69 -2
  49. package/dist/ui/index.html +8 -0
  50. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  51. package/docs/MEMU_ADOPTION.md +40 -0
  52. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  53. package/memory/_index.md +405 -0
  54. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  55. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  56. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  57. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  58. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  59. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  60. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  61. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  62. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  63. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  64. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  65. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  66. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  67. package/package.json +2 -1
  68. package/scripts/build.ts +6 -0
  69. package/scripts/bump-patch-version.sh +18 -0
  70. package/src/cli/index.ts +281 -2
  71. package/src/core/consolidated-store.ts +63 -1
  72. package/src/core/consolidation-worker.ts +115 -6
  73. package/src/core/event-store.ts +14 -0
  74. package/src/core/index.ts +1 -0
  75. package/src/core/ingest-interceptor.ts +80 -0
  76. package/src/core/markdown-mirror.ts +70 -0
  77. package/src/core/md-mirror.ts +92 -0
  78. package/src/core/mongo-sync-config.ts +165 -0
  79. package/src/core/mongo-sync-worker.ts +381 -0
  80. package/src/core/retriever.ts +540 -150
  81. package/src/core/sqlite-event-store.ts +350 -1
  82. package/src/core/tag-taxonomy.ts +51 -0
  83. package/src/core/types.ts +28 -0
  84. package/src/server/api/health.ts +53 -0
  85. package/src/server/api/index.ts +3 -1
  86. package/src/server/api/stats.ts +46 -1
  87. package/src/services/bootstrap-organizer.ts +443 -0
  88. package/src/services/codex-session-history-importer.ts +474 -0
  89. package/src/services/memory-service.ts +373 -68
  90. package/src/services/session-history-importer.ts +53 -25
  91. package/src/ui/app.js +69 -2
  92. package/src/ui/index.html +8 -0
  93. package/tests/bootstrap-organizer.test.ts +111 -0
  94. package/tests/consolidation-worker.test.ts +75 -0
  95. package/tests/ingest-interceptor.test.ts +38 -0
  96. package/tests/markdown-mirror.test.ts +85 -0
  97. package/tests/md-mirror.test.ts +50 -0
  98. package/tests/retriever-fallback-chain.test.ts +223 -0
  99. package/tests/retriever-strategy-scope.test.ts +97 -0
  100. package/tests/retriever.memu-adoption.test.ts +122 -0
  101. package/tests/sqlite-event-store-replication.test.ts +92 -0
@@ -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;
@@ -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
  /**
@@ -493,29 +608,28 @@ export class MemoryService {
493
608
  // Extract turnId from payload metadata if present (set by PostToolUse hook)
494
609
  const turnId = payload.metadata?.turnId;
495
610
 
496
- const result = await this.sqliteStore.append({
497
- eventType: 'tool_observation',
498
- sessionId,
499
- timestamp: new Date(),
500
- content,
501
- metadata: {
502
- toolName: payload.toolName,
503
- success: payload.success,
504
- ...(turnId ? { turnId } : {})
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);
505
631
  }
506
- });
507
-
508
- // Create embedding content (optimized for search)
509
- if (result.success && !result.isDuplicate) {
510
- const embeddingContent = createToolObservationEmbedding(
511
- payload.toolName,
512
- payload.metadata || {},
513
- payload.success
514
- );
515
- await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
516
- }
517
-
518
- return result;
632
+ );
519
633
  }
520
634
 
521
635
  /**
@@ -528,6 +642,10 @@ export class MemoryService {
528
642
  minScore?: number;
529
643
  sessionId?: string;
530
644
  includeShared?: boolean;
645
+ adaptiveRerank?: boolean;
646
+ intentRewrite?: boolean;
647
+ projectScopeMode?: 'strict' | 'prefer' | 'global';
648
+ allowedProjectHashes?: string[];
531
649
  }
532
650
  ): Promise<UnifiedRetrievalResult> {
533
651
  await this.initialize();
@@ -535,16 +653,177 @@ export class MemoryService {
535
653
  // Note: Pending embeddings are processed by the background worker
536
654
  // Don't block retrieval - search with whatever vectors are available
537
655
 
656
+ const rerankWeights = await this.getRerankWeights(options?.adaptiveRerank === true);
657
+
538
658
  // Use unified retrieval if shared search is requested
659
+ let result: UnifiedRetrievalResult;
660
+
539
661
  if (options?.includeShared && this.sharedStore) {
540
- return this.retriever.retrieveUnified(query, {
662
+ result = await this.retriever.retrieveUnified(query, {
541
663
  ...options,
664
+ intentRewrite: options?.intentRewrite === true,
665
+ rerankWeights,
542
666
  includeShared: true,
543
- 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,
544
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);
545
796
  }
797
+ }
798
+
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
+ }
546
822
 
547
- return this.retriever.retrieve(query, options);
823
+ return { semantic, lexical, recency };
824
+ } catch {
825
+ return undefined;
826
+ }
548
827
  }
549
828
 
550
829
  /**
@@ -598,6 +877,30 @@ export class MemoryService {
598
877
  /**
599
878
  * Get memory statistics
600
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
+
601
904
  async getStats(): Promise<{
602
905
  totalEvents: number;
603
906
  vectorCount: number;
@@ -1278,6 +1581,7 @@ export function getMemoryServiceForProject(
1278
1581
  serviceCache.set(hash, new MemoryService({
1279
1582
  storagePath,
1280
1583
  projectHash: hash,
1584
+ projectPath,
1281
1585
  // Override shared store config - hooks don't need DuckDB
1282
1586
  sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
1283
1587
  analyticsEnabled: false // Hooks don't need DuckDB
@@ -1319,6 +1623,7 @@ export function getLightweightMemoryService(sessionId: string): MemoryService {
1319
1623
  serviceCache.set(key, new MemoryService({
1320
1624
  storagePath,
1321
1625
  projectHash: projectInfo?.projectHash,
1626
+ projectPath: projectInfo?.projectPath,
1322
1627
  lightweightMode: true, // Skip embedder/vector/workers
1323
1628
  analyticsEnabled: false,
1324
1629
  sharedStoreConfig: { enabled: false }
@@ -126,19 +126,31 @@ export class SessionHistoryImporter {
126
126
 
127
127
  // Find project directory
128
128
  onProgress?.({ phase: 'scan', message: 'Scanning for session files...' });
129
- const projectDir = await this.findProjectDir(projectPath);
130
- if (!projectDir) {
129
+ const projectDirs = await this.findProjectDirs(projectPath);
130
+ if (projectDirs.length === 0) {
131
131
  result.errors.push(`Project directory not found for: ${projectPath}`);
132
132
  return result;
133
133
  }
134
134
 
135
- // Find all session files
136
- const sessionFiles = await this.findSessionFiles(projectDir);
135
+ // Find all session files across matched directories
136
+ const allSessionFiles: string[] = [];
137
+ for (const dir of projectDirs) {
138
+ const files = await this.findSessionFiles(dir);
139
+ allSessionFiles.push(...files);
140
+ }
141
+ const sessionFiles = [...new Set(allSessionFiles)];
137
142
  result.totalSessions = sessionFiles.length;
138
- onProgress?.({ phase: 'scan', message: `Found ${sessionFiles.length} sessions in ${path.basename(projectDir)}` });
143
+ onProgress?.({
144
+ phase: 'scan',
145
+ message: `Found ${sessionFiles.length} sessions in ${projectDirs.length} matched project folder(s)`
146
+ });
139
147
 
140
148
  if (options.verbose) {
141
- console.log(`Found ${sessionFiles.length} session files in ${projectDir}`);
149
+ console.log(`Matched project folders:`);
150
+ for (const dir of projectDirs) {
151
+ console.log(` - ${dir}`);
152
+ }
153
+ console.log(`Found ${sessionFiles.length} session files across matched folders`);
142
154
  }
143
155
 
144
156
  // Import each session
@@ -401,33 +413,52 @@ export class SessionHistoryImporter {
401
413
  }
402
414
 
403
415
  /**
404
- * Find project directory from project path
416
+ * Find project directories from project path.
417
+ * Supports wrappers (e.g. happy) that append extra path segments in folder names.
405
418
  */
406
- private async findProjectDir(projectPath: string): Promise<string | null> {
419
+ private async findProjectDirs(projectPath: string): Promise<string[]> {
407
420
  const projectsDir = path.join(this.claudeDir, 'projects');
408
421
  if (!fs.existsSync(projectsDir)) {
409
- return null;
422
+ return [];
410
423
  }
411
424
 
412
- // Claude uses a hash of the project path as directory name
413
- // Try to find matching directory by checking all projects
414
425
  const projectDirs = fs.readdirSync(projectsDir)
415
426
  .map(name => path.join(projectsDir, name))
416
427
  .filter(p => fs.statSync(p).isDirectory());
417
428
 
418
- // Look for directory that matches the project path pattern
419
- // The directory name format is: -home-user-project-name
420
- const normalizedPath = projectPath.replace(/\//g, '-').replace(/^-/, '');
429
+ const normalizedPath = projectPath.replace(/\/+/g, '/').replace(/\/$/, '');
430
+ const normalizedDashed = normalizedPath.replace(/\//g, '-').replace(/^-/, '');
431
+ const baseName = path.basename(normalizedPath);
421
432
 
422
- for (const dir of projectDirs) {
433
+ const scored = projectDirs.map((dir) => {
423
434
  const dirName = path.basename(dir);
424
- if (dirName.includes(normalizedPath) || normalizedPath.includes(dirName)) {
425
- return dir;
426
- }
427
- }
435
+ let score = 0;
436
+
437
+ // strong matches
438
+ if (dirName.includes(normalizedDashed)) score += 100;
439
+ if (normalizedDashed.includes(dirName)) score += 80;
440
+
441
+ // basename signal (handles wrappers adding extra suffix)
442
+ if (baseName && dirName.includes(baseName)) score += 30;
443
+
444
+ // token overlap signal
445
+ const pathTokens = normalizedDashed.split('-').filter(Boolean);
446
+ const tokenHits = pathTokens.filter(t => t.length >= 3 && dirName.includes(t)).length;
447
+ score += Math.min(tokenHits, 20);
428
448
 
429
- // If exact match not found, return first match or null
430
- return projectDirs.length > 0 ? projectDirs[0] : null;
449
+ return { dir, score, dirName };
450
+ }).filter(x => x.score > 0)
451
+ .sort((a, b) => b.score - a.score);
452
+
453
+ if (scored.length === 0) return [];
454
+
455
+ // Keep close matches (same family) to include wrapper-generated variants
456
+ const top = scored[0].score;
457
+ const threshold = Math.max(30, top - 25);
458
+
459
+ return scored
460
+ .filter(x => x.score >= threshold)
461
+ .map(x => x.dir);
431
462
  }
432
463
 
433
464
  /**
@@ -489,10 +520,7 @@ export class SessionHistoryImporter {
489
520
  let projectDirs: string[] = [];
490
521
 
491
522
  if (projectPath) {
492
- const projectDir = await this.findProjectDir(projectPath);
493
- if (projectDir) {
494
- projectDirs = [projectDir];
495
- }
523
+ projectDirs = await this.findProjectDirs(projectPath);
496
524
  } else {
497
525
  const projectsDir = path.join(this.claudeDir, 'projects');
498
526
  if (fs.existsSync(projectsDir)) {