claude-memory-layer 1.0.27 → 1.0.29
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/.env.example +7 -0
- package/AGENTS.md +11 -0
- package/README.md +374 -49
- package/benchmarks/replay/anonymized-real-sessions.json +48 -0
- package/dist/cli/index.js +10097 -6003
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +9745 -5587
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +6545 -5270
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +6646 -5354
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +6618 -5347
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +6619 -5354
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +6614 -5325
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +6702 -5356
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/index.js +13537 -0
- package/dist/index.js.map +7 -0
- package/dist/mcp/index.js +20770 -0
- package/dist/mcp/index.js.map +7 -0
- package/dist/server/api/index.js +6632 -5319
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +6667 -5340
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +6568 -5350
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/assets/js/bootstrap.js +244 -0
- package/dist/ui/assets/js/chat.js +373 -0
- package/dist/ui/assets/js/disclosure.js +232 -0
- package/dist/ui/assets/js/modals.js +298 -0
- package/dist/ui/assets/js/overview.js +655 -0
- package/dist/ui/assets/js/state.js +72 -0
- package/dist/ui/assets/js/views.js +468 -0
- package/dist/ui/index.html +43 -1
- package/dist/ui/index.ts +3 -0
- package/dist/ui/style.css +222 -0
- package/docs/ARCHITECTURE_COMPARISON_AND_RECOMMENDATIONS.md +627 -0
- package/docs/HERMES_MEMORY_INGESTION_ANALYSIS.md +440 -0
- package/docs/MEMORY_USEFULNESS_AUDIT.md +371 -0
- package/docs/MEMORY_USEFULNESS_AUDIT_RAW.json +80 -0
- package/docs/MEMSEARCH_PROJECT_STRUCTURE_ANALYSIS.md +333 -0
- package/docs/PRODUCT_VALIDATION_MATRIX.md +82 -0
- package/docs/PROJECT_STRUCTURE_ANALYSIS.md +421 -0
- package/docs/REFACTORING_MILESTONES_AND_ISSUES.md +501 -0
- package/docs/REFACTORING_PLAN_THIN_CORE.md +414 -0
- package/docs/REFERENCE_PROJECT_ANALYSES.md +25 -0
- package/docs/SUPERLOCALMEMORY_PROJECT_STRUCTURE_ANALYSIS.md +452 -0
- package/docs/TARGET_ARCHITECTURE_AND_FOLDER_STRUCTURE.md +446 -0
- package/docs/architecture/comparison-index.md +47 -0
- package/docs/reports/codex-real-data-validation-20260505T040447Z.md +46 -0
- package/package.json +12 -5
- package/scripts/build.ts +25 -8
- package/scripts/generate-session-qrels.ts +126 -0
- package/scripts/postinstall-embedding-backend.cjs +142 -0
- package/scripts/replay-retrieval-benchmark.ts +69 -0
- package/specs/thin-core-refactor/context.md +275 -0
- package/specs/thin-core-refactor/plan.md +536 -0
- package/specs/thin-core-refactor/spec.md +465 -0
- package/src/adapters/claude/capture/index.ts +3 -0
- package/src/adapters/claude/context/index.ts +3 -0
- package/src/adapters/claude/hooks/index.ts +21 -0
- package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
- package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
- package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
- package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
- package/src/adapters/claude/hooks/session-end.ts +59 -0
- package/src/adapters/claude/hooks/session-start.ts +73 -0
- package/src/adapters/claude/hooks/stop.ts +128 -0
- package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
- package/src/adapters/claude/index.ts +4 -0
- package/src/adapters/claude/transcript/index.ts +4 -0
- package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
- package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
- package/src/apps/cli/claude-settings-hooks.ts +138 -0
- package/src/apps/cli/codex-import-runner.ts +125 -0
- package/src/apps/cli/codex-validation-output.ts +95 -0
- package/src/apps/cli/hermes-import-runner.ts +130 -0
- package/src/apps/cli/hermes-validation-output.ts +91 -0
- package/src/apps/cli/index.ts +1731 -0
- package/src/apps/cli/mcp-install.ts +106 -0
- package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
- package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
- package/src/apps/dashboard/assets/js/chat.js +373 -0
- package/src/apps/dashboard/assets/js/disclosure.js +232 -0
- package/src/apps/dashboard/assets/js/modals.js +298 -0
- package/src/apps/dashboard/assets/js/overview.js +655 -0
- package/src/apps/dashboard/assets/js/state.js +72 -0
- package/src/apps/dashboard/assets/js/views.js +468 -0
- package/src/{ui → apps/dashboard}/index.html +43 -1
- package/src/apps/dashboard/index.ts +3 -0
- package/src/{ui → apps/dashboard}/style.css +222 -0
- package/src/apps/index.ts +5 -0
- package/src/apps/server/api/chat.ts +244 -0
- package/src/apps/server/api/citations.ts +105 -0
- package/src/apps/server/api/events.ts +137 -0
- package/src/apps/server/api/health.ts +53 -0
- package/src/apps/server/api/index.ts +26 -0
- package/src/apps/server/api/projects.ts +74 -0
- package/src/apps/server/api/search.ts +184 -0
- package/src/apps/server/api/sessions.ts +115 -0
- package/src/apps/server/api/stats.ts +723 -0
- package/src/apps/server/api/turns.ts +143 -0
- package/src/apps/server/api/utils.ts +65 -0
- package/src/apps/server/index.ts +111 -0
- package/src/cli/index.ts +2 -1311
- package/src/cli/retrieval-disclosure-output.ts +2 -0
- package/src/compat/index.ts +5 -0
- package/src/core/derive/fact-deriver.ts +170 -0
- package/src/core/derive/index.ts +2 -0
- package/src/core/derive/summary-deriver.ts +76 -0
- package/src/core/embedder.ts +4 -152
- package/src/core/engine/embedding-maintenance-service.ts +187 -0
- package/src/core/engine/endless-memory-services.ts +4 -0
- package/src/core/engine/index.ts +19 -0
- package/src/core/engine/memory-engine-services.ts +170 -0
- package/src/core/engine/memory-ingest-service.ts +317 -0
- package/src/core/engine/memory-query-service.ts +173 -0
- package/src/core/engine/memory-runtime-service.ts +162 -0
- package/src/core/engine/memory-service-composition.ts +231 -0
- package/src/core/engine/retrieval-analytics-service.ts +181 -0
- package/src/core/engine/retrieval-disclosure-service.ts +420 -0
- package/src/core/engine/retrieval-orchestrator.ts +377 -0
- package/src/core/engine/retrieval-services.ts +176 -0
- package/src/core/engine/shared-memory-services.ts +4 -0
- package/src/core/entity-repo.ts +1 -3
- package/src/core/event-store.ts +3 -3
- package/src/core/evidence-aligner.ts +2 -2
- package/src/core/external-market-context.ts +582 -0
- package/src/core/graduation.ts +2 -3
- package/src/core/index.ts +21 -0
- package/src/core/matcher.ts +2 -4
- package/src/core/model/memory-fact.ts +30 -0
- package/src/core/model/memory-rule.ts +14 -0
- package/src/core/model/memory-summary.ts +21 -0
- package/src/core/model/raw-event.ts +28 -0
- package/src/core/model/retrieval-result.ts +35 -0
- package/src/core/privacy/filter.ts +21 -10
- package/src/core/product-validation-matrix.ts +314 -0
- package/src/core/progressive-retriever.ts +1 -2
- package/src/core/registry/project-path.ts +54 -0
- package/src/core/registry/session-registry.ts +69 -0
- package/src/core/replay-evaluator.ts +625 -0
- package/src/core/retrieval-benchmark.ts +117 -0
- package/src/core/retrieval-quality.ts +109 -0
- package/src/core/retriever.ts +53 -15
- package/src/core/session-qrels.ts +360 -0
- package/src/core/shared-event-store.ts +1 -1
- package/src/core/sqlite-event-store.ts +35 -11
- package/src/core/task/blocker-resolver.ts +2 -2
- package/src/core/task/task-resolver.ts +0 -1
- package/src/core/vector-outbox.ts +1 -10
- package/src/core/vector-worker.ts +1 -1
- package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
- package/src/extensions/endless-memory/index.ts +1 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/mcp/handlers.ts +960 -0
- package/src/extensions/mcp/index.ts +48 -0
- package/src/extensions/mcp/tools.ts +252 -0
- package/src/extensions/shared-memory/index.ts +1 -0
- package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
- package/src/extensions/vector/embedder.ts +197 -0
- package/src/extensions/vector/index.ts +1 -0
- package/src/hooks/post-tool-use.ts +3 -236
- package/src/hooks/semantic-daemon-client.ts +1 -208
- package/src/hooks/semantic-daemon.ts +6 -271
- package/src/hooks/session-end.ts +4 -79
- package/src/hooks/session-start.ts +4 -73
- package/src/hooks/stop.ts +3 -173
- package/src/hooks/user-prompt-submit.ts +3 -338
- package/src/index.ts +13 -0
- package/src/mcp/handlers.ts +2 -212
- package/src/mcp/index.ts +3 -46
- package/src/mcp/tools.ts +2 -78
- package/src/server/api/chat.ts +2 -244
- package/src/server/api/citations.ts +2 -105
- package/src/server/api/events.ts +2 -137
- package/src/server/api/health.ts +2 -53
- package/src/server/api/index.ts +2 -26
- package/src/server/api/projects.ts +2 -74
- package/src/server/api/search.ts +2 -102
- package/src/server/api/sessions.ts +2 -115
- package/src/server/api/stats.ts +2 -724
- package/src/server/api/turns.ts +2 -143
- package/src/server/api/utils.ts +2 -46
- package/src/server/index.ts +2 -100
- package/src/services/bootstrap-organizer.ts +46 -26
- package/src/services/codex-session-history-importer.ts +521 -29
- package/src/services/hermes-session-history-importer.ts +733 -0
- package/src/services/memory-service-config.ts +36 -0
- package/src/services/memory-service-registry.ts +150 -0
- package/src/services/memory-service.ts +211 -1325
- package/src/services/session-history-importer.ts +58 -14
- package/tests/README.md +23 -0
- package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
- package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
- package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
- package/tests/apps/app-layer-boundary.test.ts +48 -0
- package/tests/apps/claude-settings-hooks.test.ts +107 -0
- package/tests/apps/cli-disclosure-output.test.ts +212 -0
- package/tests/apps/codex-import-runner.test.ts +99 -0
- package/tests/apps/codex-validation-output.test.ts +100 -0
- package/tests/apps/hermes-import-runner.test.ts +99 -0
- package/tests/apps/mcp-install-command.test.ts +59 -0
- package/tests/apps/package-build-entrypoints.test.ts +30 -0
- package/tests/apps/postinstall-embedding-backend.test.ts +167 -0
- package/tests/apps/search-api-disclosure.test.ts +162 -0
- package/tests/apps/stats-api-lightweight.test.ts +67 -0
- package/tests/apps/ui-disclosure-output.test.ts +140 -0
- package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
- package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
- package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
- package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
- package/tests/core/embedding-maintenance-service.test.ts +282 -0
- package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
- package/tests/core/external-market-context.test.ts +209 -0
- package/tests/core/fact-deriver.test.ts +79 -0
- package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
- package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
- package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
- package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
- package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
- package/tests/core/memory-engine-services.test.ts +240 -0
- package/tests/core/memory-ingest-service.test.ts +296 -0
- package/tests/core/memory-query-service.test.ts +129 -0
- package/tests/core/memory-runtime-service.test.ts +201 -0
- package/tests/core/memory-service-composition.test.ts +192 -0
- package/tests/core/memory-service-config.test.ts +41 -0
- package/tests/core/memory-service-facade.test.ts +30 -0
- package/tests/core/memory-service-registry.test.ts +206 -0
- package/tests/core/product-validation-matrix.test.ts +61 -0
- package/tests/core/project-registry.test.ts +78 -0
- package/tests/core/replay-evaluator.test.ts +181 -0
- package/tests/core/retrieval-analytics-service.test.ts +210 -0
- package/tests/core/retrieval-benchmark.test.ts +93 -0
- package/tests/core/retrieval-disclosure-service.test.ts +264 -0
- package/tests/core/retrieval-orchestrator.test.ts +403 -0
- package/tests/core/retrieval-quality.test.ts +31 -0
- package/tests/core/retrieval-services.test.ts +185 -0
- package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
- package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
- package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
- package/tests/core/session-history-importer-filter.test.ts +78 -0
- package/tests/core/session-qrels.test.ts +250 -0
- package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
- package/tests/core/summary-deriver.test.ts +66 -0
- package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
- package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
- package/tests/extensions/endless-memory-services.test.ts +325 -0
- package/tests/extensions/mcp-context-tools.test.ts +905 -0
- package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
- package/tests/extensions/mcp-package-build.test.ts +22 -0
- package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
- package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
- package/tests/extensions/shared-memory-services.test.ts +309 -0
- package/tests/extensions/vector-extension-boundary.test.ts +21 -0
- package/.claude/settings.local.json +0 -25
- package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
- package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
- package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
- package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
- package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
- package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
- package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
- package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
- package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
- package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
- package/.npm-cache/_update-notifier-last-checked +0 -0
- package/bootstrap-kb/decisions/decisions.md +0 -244
- package/bootstrap-kb/glossary/glossary.md +0 -46
- package/bootstrap-kb/modules/.claude-plugin.md +0 -22
- package/bootstrap-kb/modules/agents.md.md +0 -15
- package/bootstrap-kb/modules/claude.md.md +0 -15
- package/bootstrap-kb/modules/context.md.md +0 -15
- package/bootstrap-kb/modules/docs.md +0 -18
- package/bootstrap-kb/modules/handoff.md.md +0 -15
- package/bootstrap-kb/modules/package-lock.json.md +0 -15
- package/bootstrap-kb/modules/package.json.md +0 -15
- package/bootstrap-kb/modules/plan.md.md +0 -15
- package/bootstrap-kb/modules/readme.md.md +0 -15
- package/bootstrap-kb/modules/scripts.md +0 -26
- package/bootstrap-kb/modules/spec.md.md +0 -15
- package/bootstrap-kb/modules/specs.md +0 -20
- package/bootstrap-kb/modules/src.md +0 -51
- package/bootstrap-kb/modules/tests.md +0 -42
- package/bootstrap-kb/modules/tsconfig.json.md +0 -15
- package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
- package/bootstrap-kb/overview/overview.md +0 -40
- package/bootstrap-kb/sources/manifest.json +0 -950
- package/bootstrap-kb/sources/manifest.md +0 -227
- package/bootstrap-kb/timeline/timeline.md +0 -57
- package/claude-memory-layer-1.0.14.tgz +0 -0
- package/d.sh +0 -3
- package/deploy.sh +0 -3
- package/dist/ui/app.js +0 -2101
- package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
- package/memory/_index.md +0 -419
- package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
- package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
- package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
- package/memory/agent_response/uncategorized/2026-03-05.md +0 -157
- package/memory/default/uncategorized/2026-02-25.md +0 -4839
- package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
- package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
- package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
- package/memory/specs/citations-system/2026-02-25.md +0 -1121
- package/memory/specs/endless-mode/2026-02-25.md +0 -1392
- package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
- package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
- package/memory/specs/private-tags/2026-02-25.md +0 -1057
- package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
- package/memory/specs/task-entity-system/2026-02-25.md +0 -924
- package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
- package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
- package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
- package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
- package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
- package/memory/tool_observation/uncategorized/2026-03-05.md +0 -33
- package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
- package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
- package/memory/user_prompt/uncategorized/2026-03-05.md +0 -6
- package/specs/optional-duckdb/context.md +0 -77
- package/specs/optional-duckdb/plan.md +0 -142
- package/specs/optional-duckdb/spec.md +0 -35
- package/src/ui/app.js +0 -2101
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats API
|
|
3
|
+
* Endpoints for storage statistics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { getMemoryServiceForProject } from '../../../services/memory-service.js';
|
|
10
|
+
import { getLightweightServiceFromQuery, getServiceFromQuery } from './utils.js';
|
|
11
|
+
import type { MemoryEvent } from '../../../core/types.js';
|
|
12
|
+
|
|
13
|
+
export const statsRouter = new Hono();
|
|
14
|
+
|
|
15
|
+
type KpiWindow = '24h' | '7d' | '30d';
|
|
16
|
+
|
|
17
|
+
type KpiThresholds = {
|
|
18
|
+
usefulRecallRateMin: number;
|
|
19
|
+
reworkRateMax: number;
|
|
20
|
+
postChangeFailureRateMax: number;
|
|
21
|
+
avgCompletionTurnsMax: number;
|
|
22
|
+
memoryHitRateMin: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_KPI_THRESHOLDS: KpiThresholds = {
|
|
26
|
+
usefulRecallRateMin: 0.45,
|
|
27
|
+
reworkRateMax: 0.25,
|
|
28
|
+
postChangeFailureRateMax: 0.2,
|
|
29
|
+
avgCompletionTurnsMax: 12,
|
|
30
|
+
memoryHitRateMin: 0.35
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function loadKpiThresholds(): KpiThresholds {
|
|
34
|
+
try {
|
|
35
|
+
const filePath = path.resolve(process.cwd(), 'config', 'kpi-thresholds.json');
|
|
36
|
+
if (!fs.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
|
|
37
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Partial<KpiThresholds>;
|
|
38
|
+
return {
|
|
39
|
+
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
40
|
+
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
41
|
+
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
42
|
+
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
43
|
+
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function windowToMs(window: KpiWindow): number {
|
|
51
|
+
if (window === '24h') return 24 * 60 * 60 * 1000;
|
|
52
|
+
if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
|
|
53
|
+
return 30 * 24 * 60 * 60 * 1000;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function inWindow(e: MemoryEvent, now: number, window: KpiWindow): boolean {
|
|
57
|
+
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isEditToolName(name: string): boolean {
|
|
61
|
+
return ['Write', 'Edit', 'MultiEdit', 'NotebookEdit'].includes(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseToolPayload(e: MemoryEvent): { toolName?: string; success?: boolean; filePath?: string; command?: string } | null {
|
|
65
|
+
if (e.eventType !== 'tool_observation') return null;
|
|
66
|
+
try {
|
|
67
|
+
const payload = JSON.parse(e.content) as any;
|
|
68
|
+
return {
|
|
69
|
+
toolName: payload?.toolName,
|
|
70
|
+
success: payload?.success,
|
|
71
|
+
filePath: payload?.metadata?.filePath,
|
|
72
|
+
command: payload?.metadata?.command
|
|
73
|
+
};
|
|
74
|
+
} catch {
|
|
75
|
+
return {
|
|
76
|
+
toolName: (e.metadata as any)?.toolName,
|
|
77
|
+
success: (e.metadata as any)?.success,
|
|
78
|
+
filePath: (e.metadata as any)?.filePath,
|
|
79
|
+
command: (e.metadata as any)?.command
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isTestLikeCommand(command?: string): boolean {
|
|
85
|
+
if (!command) return false;
|
|
86
|
+
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function safeRatio(num: number, den: number): number {
|
|
90
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
|
|
91
|
+
return num / den;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function round(value: number, digits = 4): number {
|
|
95
|
+
const factor = 10 ** digits;
|
|
96
|
+
return Math.round(value * factor) / factor;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function computeSessionTurnCount(sessionEvents: MemoryEvent[]): number {
|
|
100
|
+
const turnIds = new Set<string>();
|
|
101
|
+
for (const e of sessionEvents) {
|
|
102
|
+
const turnId = (e.metadata as any)?.turnId;
|
|
103
|
+
if (typeof turnId === 'string' && turnId.length > 0) turnIds.add(turnId);
|
|
104
|
+
}
|
|
105
|
+
if (turnIds.size > 0) return turnIds.size;
|
|
106
|
+
return sessionEvents.filter((e) => e.eventType === 'user_prompt').length;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type KpiMetrics = {
|
|
110
|
+
memoryHitRate: number;
|
|
111
|
+
usefulRecallRate: number;
|
|
112
|
+
avgCompletionTurns: number;
|
|
113
|
+
timeToFirstValidEditMinutes: number;
|
|
114
|
+
reworkRate: number;
|
|
115
|
+
postChangeFailureRate: number;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function computeKpiMetrics(events: MemoryEvent[], usefulRecallRate: number): KpiMetrics {
|
|
119
|
+
const prompts = events.filter((e) => e.eventType === 'user_prompt');
|
|
120
|
+
const promptCount = prompts.length;
|
|
121
|
+
const memoryHitPrompts = prompts.filter((p) => (p.metadata as any)?.adherence?.checked).length;
|
|
122
|
+
const memoryHitRate = round(safeRatio(memoryHitPrompts, promptCount));
|
|
123
|
+
|
|
124
|
+
const sessions = new Map<string, MemoryEvent[]>();
|
|
125
|
+
for (const e of events) {
|
|
126
|
+
const arr = sessions.get(e.sessionId) || [];
|
|
127
|
+
arr.push(e);
|
|
128
|
+
sessions.set(e.sessionId, arr);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let sessionTurnTotal = 0;
|
|
132
|
+
let sessionTurnSamples = 0;
|
|
133
|
+
let firstValidEditMinutesTotal = 0;
|
|
134
|
+
let firstValidEditSamples = 0;
|
|
135
|
+
|
|
136
|
+
for (const sessionEvents of sessions.values()) {
|
|
137
|
+
sessionEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
138
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
139
|
+
if (turns > 0) {
|
|
140
|
+
sessionTurnTotal += turns;
|
|
141
|
+
sessionTurnSamples++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const firstPrompt = sessionEvents.find((e) => e.eventType === 'user_prompt');
|
|
145
|
+
const firstEdit = sessionEvents.find((e) => {
|
|
146
|
+
const payload = parseToolPayload(e);
|
|
147
|
+
return payload?.toolName && isEditToolName(payload.toolName) && payload.success === true;
|
|
148
|
+
});
|
|
149
|
+
if (firstPrompt && firstEdit) {
|
|
150
|
+
const minutes = (firstEdit.timestamp.getTime() - firstPrompt.timestamp.getTime()) / 60000;
|
|
151
|
+
if (minutes >= 0) {
|
|
152
|
+
firstValidEditMinutesTotal += minutes;
|
|
153
|
+
firstValidEditSamples++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const avgCompletionTurns = round(safeRatio(sessionTurnTotal, sessionTurnSamples), 2);
|
|
159
|
+
const timeToFirstValidEditMinutes = round(safeRatio(firstValidEditMinutesTotal, firstValidEditSamples), 2);
|
|
160
|
+
|
|
161
|
+
const editActions: Array<{ sessionId: string; timestamp: number; filePath?: string }> = [];
|
|
162
|
+
let testRunsAfterEdit = 0;
|
|
163
|
+
let failedTestRunsAfterEdit = 0;
|
|
164
|
+
|
|
165
|
+
for (const [sessionId, sessionEvents] of sessions.entries()) {
|
|
166
|
+
const sorted = [...sessionEvents].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
167
|
+
let seenEdit = false;
|
|
168
|
+
|
|
169
|
+
for (const e of sorted) {
|
|
170
|
+
const payload = parseToolPayload(e);
|
|
171
|
+
if (!payload?.toolName) continue;
|
|
172
|
+
|
|
173
|
+
if (isEditToolName(payload.toolName) && payload.success === true) {
|
|
174
|
+
editActions.push({ sessionId, timestamp: e.timestamp.getTime(), filePath: payload.filePath });
|
|
175
|
+
seenEdit = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (seenEdit && isTestLikeCommand(payload.command)) {
|
|
180
|
+
testRunsAfterEdit++;
|
|
181
|
+
if (payload.success === false) failedTestRunsAfterEdit++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const THIRTY_MIN_MS = 30 * 60 * 1000;
|
|
187
|
+
let reworkCount = 0;
|
|
188
|
+
const bySessionFile = new Map<string, number>();
|
|
189
|
+
const sortedEdits = [...editActions].sort((a, b) => a.timestamp - b.timestamp);
|
|
190
|
+
for (const edit of sortedEdits) {
|
|
191
|
+
if (!edit.filePath) continue;
|
|
192
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
193
|
+
const prev = bySessionFile.get(key);
|
|
194
|
+
if (typeof prev === 'number' && edit.timestamp - prev <= THIRTY_MIN_MS) {
|
|
195
|
+
reworkCount++;
|
|
196
|
+
}
|
|
197
|
+
bySessionFile.set(key, edit.timestamp);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const reworkRate = round(safeRatio(reworkCount, editActions.length));
|
|
201
|
+
const postChangeFailureRate = round(safeRatio(failedTestRunsAfterEdit, testRunsAfterEdit));
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
memoryHitRate,
|
|
205
|
+
usefulRecallRate,
|
|
206
|
+
avgCompletionTurns,
|
|
207
|
+
timeToFirstValidEditMinutes,
|
|
208
|
+
reworkRate,
|
|
209
|
+
postChangeFailureRate
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// GET /api/stats/shared - Get shared store statistics
|
|
215
|
+
statsRouter.get('/shared', async (c) => {
|
|
216
|
+
const memoryService = getServiceFromQuery(c);
|
|
217
|
+
try {
|
|
218
|
+
await memoryService.initialize();
|
|
219
|
+
const sharedStats = await memoryService.getSharedStoreStats();
|
|
220
|
+
return c.json({
|
|
221
|
+
troubleshooting: sharedStats?.total || 0,
|
|
222
|
+
bestPractices: 0,
|
|
223
|
+
commonErrors: 0,
|
|
224
|
+
totalUsageCount: sharedStats?.totalUsageCount || 0,
|
|
225
|
+
lastUpdated: null
|
|
226
|
+
});
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return c.json({
|
|
229
|
+
troubleshooting: 0,
|
|
230
|
+
bestPractices: 0,
|
|
231
|
+
commonErrors: 0,
|
|
232
|
+
totalUsageCount: 0,
|
|
233
|
+
lastUpdated: null
|
|
234
|
+
});
|
|
235
|
+
} finally {
|
|
236
|
+
await memoryService.shutdown();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// GET /api/stats/endless - Get endless mode status
|
|
241
|
+
statsRouter.get('/endless', async (c) => {
|
|
242
|
+
const projectPath = c.req.query('project') || process.cwd();
|
|
243
|
+
const memoryService = getMemoryServiceForProject(projectPath);
|
|
244
|
+
try {
|
|
245
|
+
await memoryService.initialize();
|
|
246
|
+
const status = await memoryService.getEndlessModeStatus();
|
|
247
|
+
return c.json({
|
|
248
|
+
mode: status.mode,
|
|
249
|
+
continuityScore: status.continuityScore,
|
|
250
|
+
workingSetSize: status.workingSetSize,
|
|
251
|
+
consolidatedCount: status.consolidatedCount,
|
|
252
|
+
lastConsolidation: status.lastConsolidation?.toISOString() || null
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return c.json({
|
|
256
|
+
mode: 'session',
|
|
257
|
+
continuityScore: 0,
|
|
258
|
+
workingSetSize: 0,
|
|
259
|
+
consolidatedCount: 0,
|
|
260
|
+
lastConsolidation: null
|
|
261
|
+
});
|
|
262
|
+
} finally {
|
|
263
|
+
await memoryService.shutdown();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// GET /api/stats/levels/:level - Get events by memory level
|
|
268
|
+
statsRouter.get('/levels/:level', async (c) => {
|
|
269
|
+
const { level } = c.req.param();
|
|
270
|
+
const limit = parseInt(c.req.query('limit') || '20', 10);
|
|
271
|
+
const offset = parseInt(c.req.query('offset') || '0', 10);
|
|
272
|
+
const sort = c.req.query('sort') || 'recent';
|
|
273
|
+
|
|
274
|
+
// Validate level
|
|
275
|
+
const validLevels = ['L0', 'L1', 'L2', 'L3', 'L4'];
|
|
276
|
+
if (!validLevels.includes(level)) {
|
|
277
|
+
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(', ')}` }, 400);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const memoryService = getServiceFromQuery(c);
|
|
281
|
+
try {
|
|
282
|
+
await memoryService.initialize();
|
|
283
|
+
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
284
|
+
const stats = await memoryService.getStats();
|
|
285
|
+
const levelStat = stats.levelStats.find(s => s.level === level);
|
|
286
|
+
|
|
287
|
+
// Apply sorting
|
|
288
|
+
if (sort === 'accessed') {
|
|
289
|
+
// Sort by access count (will need to get from SQLite)
|
|
290
|
+
// For now, add access count from SQLite if available
|
|
291
|
+
const sqliteStore = (memoryService as any).sqliteEventStore;
|
|
292
|
+
if (sqliteStore) {
|
|
293
|
+
const accessedEvents = await sqliteStore.getMostAccessed(1000);
|
|
294
|
+
const accessMap = new Map(accessedEvents.map((e: any) => [e.id, e.access_count || 0]));
|
|
295
|
+
events = events.map((e: any) => ({
|
|
296
|
+
...e,
|
|
297
|
+
accessCount: accessMap.get(e.id) || 0
|
|
298
|
+
}));
|
|
299
|
+
events.sort((a: any, b: any) => b.accessCount - a.accessCount);
|
|
300
|
+
}
|
|
301
|
+
} else if (sort === 'oldest') {
|
|
302
|
+
events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
303
|
+
} else {
|
|
304
|
+
// 'recent' - default sorting (newest first)
|
|
305
|
+
events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Apply limit after sorting
|
|
309
|
+
events = events.slice(0, limit);
|
|
310
|
+
|
|
311
|
+
return c.json({
|
|
312
|
+
level,
|
|
313
|
+
events: events.map((e: any) => ({
|
|
314
|
+
id: e.id,
|
|
315
|
+
eventType: e.eventType,
|
|
316
|
+
sessionId: e.sessionId,
|
|
317
|
+
timestamp: e.timestamp.toISOString(),
|
|
318
|
+
content: e.content.slice(0, 500) + (e.content.length > 500 ? '...' : ''),
|
|
319
|
+
metadata: e.metadata,
|
|
320
|
+
accessCount: e.accessCount || 0
|
|
321
|
+
})),
|
|
322
|
+
total: levelStat?.count || 0,
|
|
323
|
+
limit,
|
|
324
|
+
offset,
|
|
325
|
+
hasMore: events.length === limit
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return c.json({ error: (error as Error).message }, 500);
|
|
329
|
+
} finally {
|
|
330
|
+
await memoryService.shutdown();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// GET /api/stats - Get overall statistics
|
|
335
|
+
statsRouter.get('/', async (c) => {
|
|
336
|
+
const memoryService = getLightweightServiceFromQuery(c);
|
|
337
|
+
try {
|
|
338
|
+
await memoryService.initialize();
|
|
339
|
+
const stats = await memoryService.getStats();
|
|
340
|
+
const recentEvents = await memoryService.getRecentEvents(10000);
|
|
341
|
+
|
|
342
|
+
// Calculate event types
|
|
343
|
+
const eventsByType = recentEvents.reduce((acc, e) => {
|
|
344
|
+
acc[e.eventType] = (acc[e.eventType] || 0) + 1;
|
|
345
|
+
return acc;
|
|
346
|
+
}, {} as Record<string, number>);
|
|
347
|
+
|
|
348
|
+
// Calculate unique sessions
|
|
349
|
+
const uniqueSessions = new Set(recentEvents.map(e => e.sessionId));
|
|
350
|
+
|
|
351
|
+
// Calculate events by day (last 7 days)
|
|
352
|
+
const now = new Date();
|
|
353
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
354
|
+
const eventsByDay = recentEvents
|
|
355
|
+
.filter(e => e.timestamp >= sevenDaysAgo)
|
|
356
|
+
.reduce((acc, e) => {
|
|
357
|
+
const day = e.timestamp.toISOString().split('T')[0];
|
|
358
|
+
acc[day] = (acc[day] || 0) + 1;
|
|
359
|
+
return acc;
|
|
360
|
+
}, {} as Record<string, number>);
|
|
361
|
+
|
|
362
|
+
const retrievalTrace = await memoryService.getRetrievalTraceStats();
|
|
363
|
+
|
|
364
|
+
return c.json({
|
|
365
|
+
storage: {
|
|
366
|
+
eventCount: stats.totalEvents,
|
|
367
|
+
vectorCount: stats.vectorCount
|
|
368
|
+
},
|
|
369
|
+
sessions: {
|
|
370
|
+
total: uniqueSessions.size
|
|
371
|
+
},
|
|
372
|
+
eventsByType,
|
|
373
|
+
activity: {
|
|
374
|
+
daily: eventsByDay,
|
|
375
|
+
total7Days: recentEvents.filter(e => e.timestamp >= sevenDaysAgo).length
|
|
376
|
+
},
|
|
377
|
+
memory: {
|
|
378
|
+
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
379
|
+
heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
|
|
380
|
+
},
|
|
381
|
+
levelStats: stats.levelStats,
|
|
382
|
+
retrievalTrace
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return c.json({ error: (error as Error).message }, 500);
|
|
386
|
+
} finally {
|
|
387
|
+
await memoryService.shutdown();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// GET /api/stats/most-accessed - Get most accessed memories
|
|
392
|
+
statsRouter.get('/most-accessed', async (c) => {
|
|
393
|
+
const limit = parseInt(c.req.query('limit') || '10', 10);
|
|
394
|
+
// Use the same read-only service that other stats endpoints use
|
|
395
|
+
const memoryService = getServiceFromQuery(c);
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
await memoryService.initialize();
|
|
399
|
+
console.log('[most-accessed] Fetching most accessed memories, limit:', limit);
|
|
400
|
+
const memories = await memoryService.getMostAccessedMemories(limit);
|
|
401
|
+
console.log('[most-accessed] Got memories:', memories.length);
|
|
402
|
+
|
|
403
|
+
return c.json({
|
|
404
|
+
memories: memories.map(m => ({
|
|
405
|
+
memoryId: m.memoryId,
|
|
406
|
+
summary: m.summary,
|
|
407
|
+
topics: m.topics,
|
|
408
|
+
accessCount: m.accessCount,
|
|
409
|
+
lastAccessed: m.lastAccessed || null,
|
|
410
|
+
confidence: m.confidence,
|
|
411
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt
|
|
412
|
+
})),
|
|
413
|
+
total: memories.length
|
|
414
|
+
});
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error('[most-accessed] Error:', error);
|
|
417
|
+
return c.json({
|
|
418
|
+
memories: [],
|
|
419
|
+
total: 0,
|
|
420
|
+
error: (error as Error).message
|
|
421
|
+
});
|
|
422
|
+
} finally {
|
|
423
|
+
await memoryService.shutdown();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// GET /api/stats/timeline - Get activity timeline
|
|
428
|
+
statsRouter.get('/timeline', async (c) => {
|
|
429
|
+
const days = parseInt(c.req.query('days') || '7', 10);
|
|
430
|
+
const memoryService = getServiceFromQuery(c);
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
await memoryService.initialize();
|
|
434
|
+
const recentEvents = await memoryService.getRecentEvents(10000);
|
|
435
|
+
|
|
436
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
437
|
+
const filteredEvents = recentEvents.filter(e => e.timestamp >= cutoff);
|
|
438
|
+
|
|
439
|
+
// Group by day
|
|
440
|
+
const daily = filteredEvents.reduce((acc, e) => {
|
|
441
|
+
const day = e.timestamp.toISOString().split('T')[0];
|
|
442
|
+
if (!acc[day]) {
|
|
443
|
+
acc[day] = { date: day, total: 0, prompts: 0, responses: 0, tools: 0 };
|
|
444
|
+
}
|
|
445
|
+
acc[day].total++;
|
|
446
|
+
if (e.eventType === 'user_prompt') acc[day].prompts++;
|
|
447
|
+
if (e.eventType === 'agent_response') acc[day].responses++;
|
|
448
|
+
if (e.eventType === 'tool_observation') acc[day].tools++;
|
|
449
|
+
return acc;
|
|
450
|
+
}, {} as Record<string, { date: string; total: number; prompts: number; responses: number; tools: number }>);
|
|
451
|
+
|
|
452
|
+
return c.json({
|
|
453
|
+
days,
|
|
454
|
+
daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
|
|
455
|
+
});
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return c.json({ error: (error as Error).message }, 500);
|
|
458
|
+
} finally {
|
|
459
|
+
await memoryService.shutdown();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// GET /api/stats/helpfulness - Get helpfulness statistics and top helpful memories
|
|
464
|
+
statsRouter.get('/helpfulness', async (c) => {
|
|
465
|
+
const limit = parseInt(c.req.query('limit') || '10', 10);
|
|
466
|
+
const memoryService = getServiceFromQuery(c);
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
await memoryService.initialize();
|
|
470
|
+
const stats = await memoryService.getHelpfulnessStats();
|
|
471
|
+
const topMemories = await memoryService.getHelpfulMemories(limit);
|
|
472
|
+
|
|
473
|
+
return c.json({
|
|
474
|
+
...stats,
|
|
475
|
+
topMemories: topMemories.map(m => ({
|
|
476
|
+
eventId: m.eventId,
|
|
477
|
+
summary: m.summary,
|
|
478
|
+
helpfulnessScore: m.helpfulnessScore,
|
|
479
|
+
accessCount: m.accessCount,
|
|
480
|
+
evaluationCount: m.evaluationCount
|
|
481
|
+
}))
|
|
482
|
+
});
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return c.json({
|
|
485
|
+
avgScore: 0,
|
|
486
|
+
totalEvaluated: 0,
|
|
487
|
+
totalRetrievals: 0,
|
|
488
|
+
helpful: 0,
|
|
489
|
+
neutral: 0,
|
|
490
|
+
unhelpful: 0,
|
|
491
|
+
topMemories: []
|
|
492
|
+
});
|
|
493
|
+
} finally {
|
|
494
|
+
await memoryService.shutdown();
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
// GET /api/stats/retrieval-traces - Get recent retrieval traces (query -> selected context)
|
|
501
|
+
statsRouter.get('/retrieval-traces', async (c) => {
|
|
502
|
+
const limit = parseInt(c.req.query('limit') || '50', 10);
|
|
503
|
+
const memoryService = getServiceFromQuery(c);
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
await memoryService.initialize();
|
|
507
|
+
const traces = await memoryService.getRecentRetrievalTraces(limit);
|
|
508
|
+
const traceStats = await memoryService.getRetrievalTraceStats();
|
|
509
|
+
|
|
510
|
+
return c.json({
|
|
511
|
+
stats: traceStats,
|
|
512
|
+
traces: traces.map((t) => ({
|
|
513
|
+
traceId: t.traceId,
|
|
514
|
+
sessionId: t.sessionId || null,
|
|
515
|
+
projectHash: t.projectHash || null,
|
|
516
|
+
queryText: t.queryText,
|
|
517
|
+
strategy: t.strategy || null,
|
|
518
|
+
candidateEventIds: t.candidateEventIds,
|
|
519
|
+
selectedEventIds: t.selectedEventIds,
|
|
520
|
+
candidateDetails: t.candidateDetails || [],
|
|
521
|
+
selectedDetails: t.selectedDetails || [],
|
|
522
|
+
candidateCount: t.candidateCount,
|
|
523
|
+
selectedCount: t.selectedCount,
|
|
524
|
+
confidence: t.confidence || null,
|
|
525
|
+
fallbackTrace: t.fallbackTrace,
|
|
526
|
+
createdAt: t.createdAt.toISOString()
|
|
527
|
+
}))
|
|
528
|
+
});
|
|
529
|
+
} catch (error) {
|
|
530
|
+
return c.json({
|
|
531
|
+
stats: { totalQueries: 0, avgCandidateCount: 0, avgSelectedCount: 0, selectionRate: 0 },
|
|
532
|
+
traces: [],
|
|
533
|
+
error: (error as Error).message
|
|
534
|
+
}, 500);
|
|
535
|
+
} finally {
|
|
536
|
+
await memoryService.shutdown();
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// GET /api/stats/kpi - Productivity KPI summary + trend
|
|
541
|
+
statsRouter.get('/kpi', async (c) => {
|
|
542
|
+
const rawWindow = (c.req.query('window') || '7d') as KpiWindow;
|
|
543
|
+
const window: KpiWindow = rawWindow === '24h' || rawWindow === '30d' ? rawWindow : '7d';
|
|
544
|
+
const memoryService = getServiceFromQuery(c);
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
await memoryService.initialize();
|
|
548
|
+
const now = Date.now();
|
|
549
|
+
const thresholds = loadKpiThresholds();
|
|
550
|
+
const allEvents = await memoryService.getRecentEvents(20000);
|
|
551
|
+
const events = allEvents.filter((e) => inWindow(e, now, window));
|
|
552
|
+
|
|
553
|
+
const helpfulness = await memoryService.getHelpfulnessStats();
|
|
554
|
+
const usefulRecallRate = helpfulness.totalEvaluated > 0
|
|
555
|
+
? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated))
|
|
556
|
+
: 0;
|
|
557
|
+
|
|
558
|
+
const metrics = computeKpiMetrics(events, usefulRecallRate);
|
|
559
|
+
|
|
560
|
+
const windowMs = windowToMs(window);
|
|
561
|
+
const prevEvents = allEvents.filter((e) => {
|
|
562
|
+
const age = now - e.timestamp.getTime();
|
|
563
|
+
return age > windowMs && age <= windowMs * 2;
|
|
564
|
+
});
|
|
565
|
+
const previousMetrics = computeKpiMetrics(prevEvents, usefulRecallRate);
|
|
566
|
+
const deltas = {
|
|
567
|
+
memoryHitRate: round(metrics.memoryHitRate - previousMetrics.memoryHitRate),
|
|
568
|
+
usefulRecallRate: round(metrics.usefulRecallRate - previousMetrics.usefulRecallRate),
|
|
569
|
+
avgCompletionTurns: round(metrics.avgCompletionTurns - previousMetrics.avgCompletionTurns, 2),
|
|
570
|
+
timeToFirstValidEditMinutes: round(metrics.timeToFirstValidEditMinutes - previousMetrics.timeToFirstValidEditMinutes, 2),
|
|
571
|
+
reworkRate: round(metrics.reworkRate - previousMetrics.reworkRate),
|
|
572
|
+
postChangeFailureRate: round(metrics.postChangeFailureRate - previousMetrics.postChangeFailureRate)
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const THIRTY_MIN_MS = 30 * 60 * 1000;
|
|
576
|
+
|
|
577
|
+
// Trend (daily buckets for last 30 days)
|
|
578
|
+
const trendWindowMs = 30 * 24 * 60 * 60 * 1000;
|
|
579
|
+
const trendEvents = allEvents.filter((e) => now - e.timestamp.getTime() <= trendWindowMs);
|
|
580
|
+
const buckets = new Map<string, MemoryEvent[]>();
|
|
581
|
+
for (const e of trendEvents) {
|
|
582
|
+
const day = e.timestamp.toISOString().split('T')[0];
|
|
583
|
+
const arr = buckets.get(day) || [];
|
|
584
|
+
arr.push(e);
|
|
585
|
+
buckets.set(day, arr);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const trendDaily = Array.from(buckets.entries())
|
|
589
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
590
|
+
.map(([date, dayEvents]) => {
|
|
591
|
+
const dayPrompts = dayEvents.filter((e) => e.eventType === 'user_prompt');
|
|
592
|
+
const dayPromptCount = dayPrompts.length;
|
|
593
|
+
const dayMemoryHit = dayPrompts.filter((p) => (p.metadata as any)?.adherence?.checked).length;
|
|
594
|
+
|
|
595
|
+
// lightweight day rework/failure approximation
|
|
596
|
+
const dayEdits = dayEvents.filter((e) => {
|
|
597
|
+
const p = parseToolPayload(e);
|
|
598
|
+
return Boolean(p?.toolName && isEditToolName(p.toolName) && p.success === true);
|
|
599
|
+
});
|
|
600
|
+
const dayEditActions = dayEdits
|
|
601
|
+
.map((e) => {
|
|
602
|
+
const p = parseToolPayload(e);
|
|
603
|
+
return { sessionId: e.sessionId, timestamp: e.timestamp.getTime(), filePath: p?.filePath };
|
|
604
|
+
})
|
|
605
|
+
.filter((x) => Boolean(x.filePath));
|
|
606
|
+
let dayReworkCount = 0;
|
|
607
|
+
const dayBySessionFile = new Map<string, number>();
|
|
608
|
+
for (const edit of dayEditActions) {
|
|
609
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
610
|
+
const prev = dayBySessionFile.get(key);
|
|
611
|
+
if (typeof prev === 'number' && edit.timestamp - prev <= THIRTY_MIN_MS) dayReworkCount++;
|
|
612
|
+
dayBySessionFile.set(key, edit.timestamp);
|
|
613
|
+
}
|
|
614
|
+
const dayTests = dayEvents.filter((e) => {
|
|
615
|
+
const p = parseToolPayload(e);
|
|
616
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command));
|
|
617
|
+
});
|
|
618
|
+
const dayFailedTests = dayEvents.filter((e) => {
|
|
619
|
+
const p = parseToolPayload(e);
|
|
620
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command) && p.success === false);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const turnsBySession = new Map<string, MemoryEvent[]>();
|
|
624
|
+
for (const e of dayEvents) {
|
|
625
|
+
const arr = turnsBySession.get(e.sessionId) || [];
|
|
626
|
+
arr.push(e);
|
|
627
|
+
turnsBySession.set(e.sessionId, arr);
|
|
628
|
+
}
|
|
629
|
+
let dayTurnsTotal = 0;
|
|
630
|
+
let dayTurnsSamples = 0;
|
|
631
|
+
for (const sessionEvents of turnsBySession.values()) {
|
|
632
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
633
|
+
if (turns > 0) {
|
|
634
|
+
dayTurnsTotal += turns;
|
|
635
|
+
dayTurnsSamples++;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
date,
|
|
641
|
+
memoryHitRate: round(safeRatio(dayMemoryHit, dayPromptCount)),
|
|
642
|
+
usefulRecallRate,
|
|
643
|
+
reworkRate: round(safeRatio(dayReworkCount, dayEditActions.length)),
|
|
644
|
+
postChangeFailureRate: round(safeRatio(dayFailedTests.length, dayTests.length)),
|
|
645
|
+
avgCompletionTurns: round(safeRatio(dayTurnsTotal, dayTurnsSamples), 2)
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const alerts: Array<{ metric: string; level: 'warn'; message: string; value: number; threshold: number }> = [];
|
|
650
|
+
if (metrics.usefulRecallRate < thresholds.usefulRecallRateMin) {
|
|
651
|
+
alerts.push({ metric: 'usefulRecallRate', level: 'warn', message: 'Useful recall rate is below threshold', value: metrics.usefulRecallRate, threshold: thresholds.usefulRecallRateMin });
|
|
652
|
+
}
|
|
653
|
+
if (metrics.reworkRate > thresholds.reworkRateMax) {
|
|
654
|
+
alerts.push({ metric: 'reworkRate', level: 'warn', message: 'Rework rate is above threshold', value: metrics.reworkRate, threshold: thresholds.reworkRateMax });
|
|
655
|
+
}
|
|
656
|
+
if (metrics.postChangeFailureRate > thresholds.postChangeFailureRateMax) {
|
|
657
|
+
alerts.push({ metric: 'postChangeFailureRate', level: 'warn', message: 'Post-change failure rate is above threshold', value: metrics.postChangeFailureRate, threshold: thresholds.postChangeFailureRateMax });
|
|
658
|
+
}
|
|
659
|
+
if (metrics.avgCompletionTurns > thresholds.avgCompletionTurnsMax) {
|
|
660
|
+
alerts.push({ metric: 'avgCompletionTurns', level: 'warn', message: 'Average completion turns is above threshold', value: metrics.avgCompletionTurns, threshold: thresholds.avgCompletionTurnsMax });
|
|
661
|
+
}
|
|
662
|
+
if (metrics.memoryHitRate < thresholds.memoryHitRateMin) {
|
|
663
|
+
alerts.push({ metric: 'memoryHitRate', level: 'warn', message: 'Memory hit rate is below threshold', value: metrics.memoryHitRate, threshold: thresholds.memoryHitRateMin });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return c.json({
|
|
667
|
+
window,
|
|
668
|
+
metrics,
|
|
669
|
+
previousMetrics,
|
|
670
|
+
deltas,
|
|
671
|
+
trend: {
|
|
672
|
+
daily: trendDaily
|
|
673
|
+
},
|
|
674
|
+
thresholds,
|
|
675
|
+
alerts
|
|
676
|
+
});
|
|
677
|
+
} catch (error) {
|
|
678
|
+
return c.json({ error: (error as Error).message }, 500);
|
|
679
|
+
} finally {
|
|
680
|
+
await memoryService.shutdown();
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// POST /api/stats/graduation/run - Force graduation evaluation
|
|
685
|
+
statsRouter.post('/graduation/run', async (c) => {
|
|
686
|
+
const memoryService = getServiceFromQuery(c);
|
|
687
|
+
try {
|
|
688
|
+
await memoryService.initialize();
|
|
689
|
+
const result = await memoryService.forceGraduation();
|
|
690
|
+
|
|
691
|
+
return c.json({
|
|
692
|
+
success: true,
|
|
693
|
+
evaluated: result.evaluated,
|
|
694
|
+
graduated: result.graduated,
|
|
695
|
+
byLevel: result.byLevel
|
|
696
|
+
});
|
|
697
|
+
} catch (error) {
|
|
698
|
+
return c.json({
|
|
699
|
+
success: false,
|
|
700
|
+
error: (error as Error).message
|
|
701
|
+
}, 500);
|
|
702
|
+
} finally {
|
|
703
|
+
await memoryService.shutdown();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// GET /api/stats/graduation - Get graduation criteria info
|
|
708
|
+
statsRouter.get('/graduation', async (c) => {
|
|
709
|
+
return c.json({
|
|
710
|
+
criteria: {
|
|
711
|
+
L0toL1: { minAccessCount: 1, minConfidence: 0.5, minCrossSessionRefs: 0, maxAgeDays: 30 },
|
|
712
|
+
L1toL2: { minAccessCount: 3, minConfidence: 0.7, minCrossSessionRefs: 1, maxAgeDays: 60 },
|
|
713
|
+
L2toL3: { minAccessCount: 5, minConfidence: 0.85, minCrossSessionRefs: 2, maxAgeDays: 90 },
|
|
714
|
+
L3toL4: { minAccessCount: 10, minConfidence: 0.92, minCrossSessionRefs: 3, maxAgeDays: 180 }
|
|
715
|
+
},
|
|
716
|
+
description: {
|
|
717
|
+
accessCount: 'Number of times the memory was retrieved/referenced',
|
|
718
|
+
confidence: 'Match confidence score when retrieved (0.0-1.0)',
|
|
719
|
+
crossSessionRefs: 'Number of different sessions that referenced this memory',
|
|
720
|
+
maxAgeDays: 'Maximum days since last access (prevents stale promotion)'
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
});
|