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.
- 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 +2389 -286
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1017 -132
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1347 -202
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1339 -194
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1343 -198
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1351 -206
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1347 -202
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1436 -211
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1445 -220
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1345 -199
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +69 -2
- package/dist/ui/index.html +8 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -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 +2 -1
- package/scripts/build.ts +6 -0
- package/scripts/bump-patch-version.sh +18 -0
- package/src/cli/index.ts +281 -2
- 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 +350 -1
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/types.ts +28 -0
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +3 -1
- package/src/server/api/stats.ts +46 -1
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +373 -68
- package/src/services/session-history-importer.ts +53 -25
- package/src/ui/app.js +69 -2
- package/src/ui/index.html +8 -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
|
@@ -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
|
-
{
|
|
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
|
/**
|
|
@@ -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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
130
|
-
if (
|
|
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
|
|
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?.({
|
|
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(`
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
429
|
+
const normalizedPath = projectPath.replace(/\/+/g, '/').replace(/\/$/, '');
|
|
430
|
+
const normalizedDashed = normalizedPath.replace(/\//g, '-').replace(/^-/, '');
|
|
431
|
+
const baseName = path.basename(normalizedPath);
|
|
421
432
|
|
|
422
|
-
|
|
433
|
+
const scored = projectDirs.map((dir) => {
|
|
423
434
|
const dirName = path.basename(dir);
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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)) {
|