claude-memory-layer 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +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/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/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 }
|
package/src/ui/app.js
CHANGED
|
@@ -11,6 +11,7 @@ const state = {
|
|
|
11
11
|
sharedStats: null,
|
|
12
12
|
mostAccessed: null,
|
|
13
13
|
helpfulness: null,
|
|
14
|
+
retrievalTraces: null,
|
|
14
15
|
currentLevel: 'L0',
|
|
15
16
|
currentSort: 'recent',
|
|
16
17
|
currentView: 'overview',
|
|
@@ -235,17 +236,19 @@ async function refreshData() {
|
|
|
235
236
|
if(btn) btn.classList.add('loading');
|
|
236
237
|
|
|
237
238
|
try {
|
|
238
|
-
const [stats, shared, mostAccessed, helpfulness] = await Promise.all([
|
|
239
|
+
const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
|
|
239
240
|
fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
|
|
240
241
|
fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
|
|
241
242
|
fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
|
|
242
|
-
fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null)
|
|
243
|
+
fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
|
|
244
|
+
fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null)
|
|
243
245
|
]);
|
|
244
246
|
|
|
245
247
|
state.stats = stats;
|
|
246
248
|
state.sharedStats = shared;
|
|
247
249
|
state.mostAccessed = mostAccessed;
|
|
248
250
|
state.helpfulness = helpfulness;
|
|
251
|
+
state.retrievalTraces = retrievalTraces;
|
|
249
252
|
|
|
250
253
|
updateStatsUI();
|
|
251
254
|
updateSharedUI();
|
|
@@ -397,6 +400,7 @@ function updateMemoryUsageUI() {
|
|
|
397
400
|
updateGraduationBars();
|
|
398
401
|
updateHelpfulnessUI();
|
|
399
402
|
updateMostHelpfulList();
|
|
403
|
+
updateRetrievalTraceUI();
|
|
400
404
|
}
|
|
401
405
|
|
|
402
406
|
function updateGraduationBars() {
|
|
@@ -483,6 +487,69 @@ function updateMostHelpfulList() {
|
|
|
483
487
|
}).join('');
|
|
484
488
|
}
|
|
485
489
|
|
|
490
|
+
|
|
491
|
+
function updateRetrievalTraceUI() {
|
|
492
|
+
const summaryEl = document.getElementById('retrieval-trace-summary');
|
|
493
|
+
const listEl = document.getElementById('retrieval-trace-list');
|
|
494
|
+
if (!summaryEl || !listEl) return;
|
|
495
|
+
|
|
496
|
+
const payload = state.retrievalTraces;
|
|
497
|
+
const stats = payload?.stats;
|
|
498
|
+
const traces = payload?.traces || [];
|
|
499
|
+
|
|
500
|
+
if (!stats || !Number.isFinite(stats.totalQueries) || stats.totalQueries === 0) {
|
|
501
|
+
summaryEl.innerHTML = '<span style="color:var(--text-muted);">No retrieval traces yet.</span>';
|
|
502
|
+
listEl.innerHTML = '<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">No query/context trace data</div>';
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const selectionRate = ((stats.selectionRate || 0) * 100).toFixed(1);
|
|
507
|
+
summaryEl.innerHTML = `
|
|
508
|
+
<div style="display:flex; gap:14px; flex-wrap:wrap; font-size:12px;">
|
|
509
|
+
<span><strong>${formatNumber(stats.totalQueries)}</strong> queries</span>
|
|
510
|
+
<span><strong>${Number(stats.avgCandidateCount || 0).toFixed(1)}</strong> avg candidates</span>
|
|
511
|
+
<span><strong>${Number(stats.avgSelectedCount || 0).toFixed(1)}</strong> avg selected</span>
|
|
512
|
+
<span><strong>${selectionRate}%</strong> selection rate</span>
|
|
513
|
+
</div>
|
|
514
|
+
`;
|
|
515
|
+
|
|
516
|
+
listEl.innerHTML = traces.slice(0, 8).map((t) => {
|
|
517
|
+
const ts = t.createdAt ? new Date(t.createdAt).toLocaleString() : '-';
|
|
518
|
+
const confidence = t.confidence || 'n/a';
|
|
519
|
+
const selected = Number(t.selectedCount || 0);
|
|
520
|
+
const candidates = Number(t.candidateCount || 0);
|
|
521
|
+
const selectedDetails = (t.selectedDetails || []).slice(0, 2);
|
|
522
|
+
const candidateDetails = (t.candidateDetails || []).slice(0, 3);
|
|
523
|
+
const selectedIdsHtml = selectedDetails.length > 0
|
|
524
|
+
? selectedDetails.map((d) => {
|
|
525
|
+
const breakdown = `score=${Number(d.score || 0).toFixed(3)} · s=${Number(d.semanticScore || 0).toFixed(3)} · l=${Number(d.lexicalScore || 0).toFixed(3)} · r=${Number(d.recencyScore || 0).toFixed(3)}`;
|
|
526
|
+
return `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${d.eventId}')" title="${escapeHtml(breakdown)}">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`;
|
|
527
|
+
}).join(' ')
|
|
528
|
+
: ((t.selectedEventIds || []).slice(0, 2).map((id) => `<span class="event-type-badge" style="cursor:pointer;" onclick="openDetailModal('${id}')">${escapeHtml((id || '').slice(0, 8))}...</span>`).join(' ') || '-');
|
|
529
|
+
|
|
530
|
+
const scoreBreakdownHtml = selectedDetails.length > 0
|
|
531
|
+
? selectedDetails.map((d) => `<div style="font-size:10px; color:var(--text-muted);">${escapeHtml((d.eventId || '').slice(0, 8))}... → score ${Number(d.score || 0).toFixed(3)} (s ${Number(d.semanticScore || 0).toFixed(3)}, l ${Number(d.lexicalScore || 0).toFixed(3)}, r ${Number(d.recencyScore || 0).toFixed(3)})</div>`).join('')
|
|
532
|
+
: '';
|
|
533
|
+
|
|
534
|
+
return `
|
|
535
|
+
<div class="shared-item" style="align-items:flex-start;">
|
|
536
|
+
<div class="shared-info" style="align-items:flex-start; flex-direction:column; gap:4px;">
|
|
537
|
+
<span style="font-size:12px; color:var(--text-secondary);"><strong>Q:</strong> ${escapeHtml((t.queryText || '').slice(0, 120))}</span>
|
|
538
|
+
<span style="font-size:11px; color:var(--text-muted);">${ts} · strategy=${escapeHtml(t.strategy || 'auto')} · conf=${escapeHtml(confidence)}</span>
|
|
539
|
+
<span style="font-size:11px; color:var(--text-muted);">selected IDs: ${selectedIdsHtml}</span>
|
|
540
|
+
<span style="font-size:11px; color:var(--text-muted);">candidates: ${candidateDetails.map((d) => `<span class=\"event-type-badge\" style=\"cursor:pointer;\" onclick=\"openDetailModal('${d.eventId}')\">${escapeHtml((d.eventId || '').slice(0, 8))}...</span>`).join(' ') || '-'}</span>
|
|
541
|
+
${scoreBreakdownHtml}
|
|
542
|
+
</div>
|
|
543
|
+
<div style="display:flex; flex-direction:column; align-items:flex-end; gap:2px; min-width:68px;">
|
|
544
|
+
<span style="font-size:13px; font-weight:600; color:var(--accent-primary);">${selected}/${candidates}</span>
|
|
545
|
+
<span style="font-size:10px; color:var(--text-muted);">sel/cand</span>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
`;
|
|
549
|
+
}).join('');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
|
|
486
553
|
// --- Charts ---
|
|
487
554
|
|
|
488
555
|
async function initActivityChart() {
|
package/src/ui/index.html
CHANGED
|
@@ -271,6 +271,14 @@
|
|
|
271
271
|
<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">Loading...</div>
|
|
272
272
|
</div>
|
|
273
273
|
</div>
|
|
274
|
+
|
|
275
|
+
<div style="margin-top:20px;">
|
|
276
|
+
<div class="section-label">Retrieval Trace (1:1)</div>
|
|
277
|
+
<div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
|
|
278
|
+
<div id="retrieval-trace-list" class="shared-list">
|
|
279
|
+
<div style="padding:12px; text-align:center; color:var(--text-muted); font-size:13px;">Loading...</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
274
282
|
</div>
|
|
275
283
|
|
|
276
284
|
</div>
|