nlm-memory 0.20.0 → 0.21.0
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/README.md +19 -10
- package/dist/cli/digest.js +60 -1
- package/dist/cli/digest.js.map +1 -1
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +49 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/nlm.d.ts +13 -1
- package/dist/cli/nlm.js +872 -61
- package/dist/cli/nlm.js.map +1 -1
- package/dist/cli/scope-backfill.d.ts +34 -0
- package/dist/cli/scope-backfill.js +237 -0
- package/dist/cli/scope-backfill.js.map +1 -0
- package/dist/cli/scope-coverage.d.ts +44 -0
- package/dist/cli/scope-coverage.js +205 -0
- package/dist/cli/scope-coverage.js.map +1 -0
- package/dist/core/actions/overlay.d.ts +20 -6
- package/dist/core/actions/overlay.js +32 -18
- package/dist/core/actions/overlay.js.map +1 -1
- package/dist/core/adapters/claude-code.d.ts +14 -0
- package/dist/core/adapters/claude-code.js +36 -0
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/classifier/prompt.js +3 -3
- package/dist/core/dataset/build-dataset.js +1 -0
- package/dist/core/dataset/build-dataset.js.map +1 -1
- package/dist/core/digest/compose.d.ts +13 -3
- package/dist/core/digest/compose.js +23 -18
- package/dist/core/digest/compose.js.map +1 -1
- package/dist/core/digest/hook-injection.d.ts +43 -0
- package/dist/core/digest/hook-injection.js +79 -0
- package/dist/core/digest/hook-injection.js.map +1 -0
- package/dist/core/embedding/embed-backfill.d.ts +13 -2
- package/dist/core/embedding/embed-backfill.js +181 -43
- package/dist/core/embedding/embed-backfill.js.map +1 -1
- package/dist/core/embedding/embedding-config.d.ts +17 -0
- package/dist/core/embedding/embedding-config.js +12 -0
- package/dist/core/embedding/embedding-config.js.map +1 -0
- package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
- package/dist/core/embedding/pg-embed-backfill.js +264 -0
- package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
- package/dist/core/entities/dedup-suggest.d.ts +25 -0
- package/dist/core/entities/dedup-suggest.js +107 -0
- package/dist/core/entities/dedup-suggest.js.map +1 -0
- package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
- package/dist/core/eval/classifier-fixture-eval.js +217 -0
- package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
- package/dist/core/eval/extraction-scoring.d.ts +63 -0
- package/dist/core/eval/extraction-scoring.js +79 -0
- package/dist/core/eval/extraction-scoring.js.map +1 -0
- package/dist/core/eval/run-eval.d.ts +40 -0
- package/dist/core/eval/run-eval.js +34 -0
- package/dist/core/eval/run-eval.js.map +1 -0
- package/dist/core/exemplars/capture-from-session.d.ts +2 -0
- package/dist/core/exemplars/capture-from-session.js +7 -3
- package/dist/core/exemplars/capture-from-session.js.map +1 -1
- package/dist/core/exemplars/diff-parse.d.ts +23 -0
- package/dist/core/exemplars/diff-parse.js +73 -0
- package/dist/core/exemplars/diff-parse.js.map +1 -0
- package/dist/core/exemplars/embed-backfill.d.ts +36 -0
- package/dist/core/exemplars/embed-backfill.js +85 -0
- package/dist/core/exemplars/embed-backfill.js.map +1 -0
- package/dist/core/exemplars/embed-text.d.ts +15 -0
- package/dist/core/exemplars/embed-text.js +18 -0
- package/dist/core/exemplars/embed-text.js.map +1 -0
- package/dist/core/exemplars/extract-exemplar.d.ts +2 -0
- package/dist/core/exemplars/extract-exemplar.js +9 -66
- package/dist/core/exemplars/extract-exemplar.js.map +1 -1
- package/dist/core/exemplars/ingest-exemplar.d.ts +1 -0
- package/dist/core/exemplars/ingest-exemplar.js +1 -0
- package/dist/core/exemplars/ingest-exemplar.js.map +1 -1
- package/dist/core/exemplars/recall-code.js +4 -0
- package/dist/core/exemplars/recall-code.js.map +1 -1
- package/dist/core/exemplars/warm-embedder.d.ts +14 -0
- package/dist/core/exemplars/warm-embedder.js +18 -0
- package/dist/core/exemplars/warm-embedder.js.map +1 -0
- package/dist/core/health/corpus-state.d.ts +8 -0
- package/dist/core/health/corpus-state.js +11 -0
- package/dist/core/health/corpus-state.js.map +1 -0
- package/dist/core/health/embed-inflight.d.ts +9 -0
- package/dist/core/health/embed-inflight.js +30 -0
- package/dist/core/health/embed-inflight.js.map +1 -0
- package/dist/core/health/embedding-lane-state.d.ts +6 -0
- package/dist/core/health/embedding-lane-state.js +18 -0
- package/dist/core/health/embedding-lane-state.js.map +1 -0
- package/dist/core/health/warmup-state.d.ts +12 -0
- package/dist/core/health/warmup-state.js +12 -0
- package/dist/core/health/warmup-state.js.map +1 -0
- package/dist/core/hook/claude-settings.d.ts +1 -1
- package/dist/core/hook/claude-settings.js +0 -1
- package/dist/core/hook/claude-settings.js.map +1 -1
- package/dist/core/hook/gate.d.ts +13 -6
- package/dist/core/hook/gate.js +47 -6
- package/dist/core/hook/gate.js.map +1 -1
- package/dist/core/hook/hook-log.d.ts +5 -0
- package/dist/core/hook/hook-log.js.map +1 -1
- package/dist/core/hook/memo.d.ts +12 -0
- package/dist/core/hook/memo.js +41 -1
- package/dist/core/hook/memo.js.map +1 -1
- package/dist/core/hook/pointer-block.d.ts +1 -0
- package/dist/core/hook/pointer-block.js +23 -2
- package/dist/core/hook/pointer-block.js.map +1 -1
- package/dist/core/hook/query-extract.d.ts +2 -1
- package/dist/core/hook/query-extract.js +10 -1
- package/dist/core/hook/query-extract.js.map +1 -1
- package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
- package/dist/core/hook/resolve-conversation-by-query.js +98 -0
- package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
- package/dist/core/hook/select.d.ts +8 -0
- package/dist/core/hook/select.js +9 -2
- package/dist/core/hook/select.js.map +1 -1
- package/dist/core/hook/transcript.js +16 -2
- package/dist/core/hook/transcript.js.map +1 -1
- package/dist/core/ingest/backfill-derivables.d.ts +92 -0
- package/dist/core/ingest/backfill-derivables.js +175 -0
- package/dist/core/ingest/backfill-derivables.js.map +1 -0
- package/dist/core/ingest/ingest-session.d.ts +5 -0
- package/dist/core/ingest/ingest-session.js +23 -0
- package/dist/core/ingest/ingest-session.js.map +1 -1
- package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
- package/dist/core/ingest/reclassify-oversized.js +9 -0
- package/dist/core/ingest/reclassify-oversized.js.map +1 -1
- package/dist/core/ingest/reprocess.d.ts +96 -0
- package/dist/core/ingest/reprocess.js +280 -0
- package/dist/core/ingest/reprocess.js.map +1 -0
- package/dist/core/ingest/transcript-derivables.d.ts +27 -0
- package/dist/core/ingest/transcript-derivables.js +138 -0
- package/dist/core/ingest/transcript-derivables.js.map +1 -0
- package/dist/core/integrity/check-invariants.d.ts +6 -1
- package/dist/core/integrity/check-invariants.js +209 -3
- package/dist/core/integrity/check-invariants.js.map +1 -1
- package/dist/core/metrics/corpus-stats.d.ts +49 -0
- package/dist/core/metrics/corpus-stats.js +111 -0
- package/dist/core/metrics/corpus-stats.js.map +1 -0
- package/dist/core/metrics/re-derivation.d.ts +48 -0
- package/dist/core/metrics/re-derivation.js +87 -0
- package/dist/core/metrics/re-derivation.js.map +1 -0
- package/dist/core/outcome/coverage.d.ts +33 -0
- package/dist/core/outcome/coverage.js +46 -0
- package/dist/core/outcome/coverage.js.map +1 -0
- package/dist/core/outcome/rollup.d.ts +33 -0
- package/dist/core/outcome/rollup.js +113 -0
- package/dist/core/outcome/rollup.js.map +1 -0
- package/dist/core/recall/citation-log.d.ts +8 -0
- package/dist/core/recall/citation-log.js +17 -0
- package/dist/core/recall/citation-log.js.map +1 -1
- package/dist/core/recall/hook-recall-log.d.ts +2 -2
- package/dist/core/recall/hook-recall-log.js +2 -2
- package/dist/core/recall/query-intent.d.ts +7 -0
- package/dist/core/recall/query-intent.js +25 -0
- package/dist/core/recall/query-intent.js.map +1 -0
- package/dist/core/recall/query-log.d.ts +4 -0
- package/dist/core/recall/query-log.js +4 -0
- package/dist/core/recall/query-log.js.map +1 -1
- package/dist/core/recall/recall-service.d.ts +37 -1
- package/dist/core/recall/recall-service.js +106 -63
- package/dist/core/recall/recall-service.js.map +1 -1
- package/dist/core/recall/related-exemplars.d.ts +1 -0
- package/dist/core/recall/related-exemplars.js +4 -1
- package/dist/core/recall/related-exemplars.js.map +1 -1
- package/dist/core/recall/related-facts.js +2 -2
- package/dist/core/recall/related-facts.js.map +1 -1
- package/dist/core/recall/reranker.d.ts +2 -1
- package/dist/core/recall/reranker.js +3 -3
- package/dist/core/recall/reranker.js.map +1 -1
- package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
- package/dist/core/recall-facts/fact-query-log.js +2 -0
- package/dist/core/recall-facts/fact-query-log.js.map +1 -1
- package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
- package/dist/core/recall-facts/fact-recall-service.js +22 -8
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scan-once.d.ts +6 -6
- package/dist/core/scheduler/scan-once.js +15 -49
- package/dist/core/scheduler/scan-once.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +10 -0
- package/dist/core/scheduler/scheduler.js +109 -13
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/scope/alias-map.d.ts +10 -0
- package/dist/core/scope/alias-map.js +41 -0
- package/dist/core/scope/alias-map.js.map +1 -0
- package/dist/core/scope/derive-scope.d.ts +8 -0
- package/dist/core/scope/derive-scope.js +54 -0
- package/dist/core/scope/derive-scope.js.map +1 -0
- package/dist/core/scope/scope-clause.d.ts +16 -0
- package/dist/core/scope/scope-clause.js +21 -0
- package/dist/core/scope/scope-clause.js.map +1 -0
- package/dist/core/scope/stamp-flag.d.ts +1 -0
- package/dist/core/scope/stamp-flag.js +4 -0
- package/dist/core/scope/stamp-flag.js.map +1 -0
- package/dist/core/signals/code-signal.d.ts +50 -0
- package/dist/core/signals/code-signal.js +54 -0
- package/dist/core/signals/code-signal.js.map +1 -0
- package/dist/core/signals/ingest-signal.d.ts +1 -1
- package/dist/core/signals/ingest-signal.js +3 -1
- package/dist/core/signals/ingest-signal.js.map +1 -1
- package/dist/core/signals/stamp-scope.d.ts +18 -0
- package/dist/core/signals/stamp-scope.js +28 -0
- package/dist/core/signals/stamp-scope.js.map +1 -0
- package/dist/core/storage/db-restore.js +3 -1
- package/dist/core/storage/db-restore.js.map +1 -1
- package/dist/core/storage/fact-batch.d.ts +9 -0
- package/dist/core/storage/fact-batch.js +14 -0
- package/dist/core/storage/fact-batch.js.map +1 -0
- package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/pg-code-exemplar-store.js +13 -2
- package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/pg-embedding-config.d.ts +29 -0
- package/dist/core/storage/pg-embedding-config.js +53 -0
- package/dist/core/storage/pg-embedding-config.js.map +1 -0
- package/dist/core/storage/pg-entity-store.d.ts +12 -0
- package/dist/core/storage/pg-entity-store.js +79 -0
- package/dist/core/storage/pg-entity-store.js.map +1 -0
- package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
- package/dist/core/storage/pg-fact-ingest.js +34 -0
- package/dist/core/storage/pg-fact-ingest.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +3 -0
- package/dist/core/storage/pg-fact-store.js +26 -30
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- package/dist/core/storage/pg-migrate.d.ts +18 -0
- package/dist/core/storage/pg-migrate.js +73 -0
- package/dist/core/storage/pg-migrate.js.map +1 -0
- package/dist/core/storage/pg-session-store.d.ts +11 -2
- package/dist/core/storage/pg-session-store.js +224 -96
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/pg-signal-store.js +4 -3
- package/dist/core/storage/pg-signal-store.js.map +1 -1
- package/dist/core/storage/pg-storage.d.ts +9 -7
- package/dist/core/storage/pg-storage.js +20 -50
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-workstream-store.d.ts +25 -0
- package/dist/core/storage/pg-workstream-store.js +97 -0
- package/dist/core/storage/pg-workstream-store.js.map +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js +15 -2
- package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
- package/dist/core/storage/sqlite-embedding-config.js +27 -0
- package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
- package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
- package/dist/core/storage/sqlite-entity-store.js +96 -0
- package/dist/core/storage/sqlite-entity-store.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
- package/dist/core/storage/sqlite-fact-store.js +81 -27
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-outcome-store.d.ts +89 -0
- package/dist/core/storage/sqlite-outcome-store.js +158 -0
- package/dist/core/storage/sqlite-outcome-store.js.map +1 -0
- package/dist/core/storage/sqlite-session-store.d.ts +42 -7
- package/dist/core/storage/sqlite-session-store.js +208 -95
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-signal-store.js +4 -2
- package/dist/core/storage/sqlite-signal-store.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +8 -8
- package/dist/core/storage/sqlite-storage.js +21 -32
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/sqlite-workstream-store.d.ts +25 -0
- package/dist/core/storage/sqlite-workstream-store.js +99 -0
- package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
- package/dist/core/storage/supersedence-log.js +2 -0
- package/dist/core/storage/supersedence-log.js.map +1 -1
- package/dist/core/telemetry/probe-filter.d.ts +35 -0
- package/dist/core/telemetry/probe-filter.js +56 -0
- package/dist/core/telemetry/probe-filter.js.map +1 -0
- package/dist/core/update-check/check.d.ts +6 -0
- package/dist/core/update-check/check.js +35 -0
- package/dist/core/update-check/check.js.map +1 -1
- package/dist/core/work-digest/active-spans.d.ts +8 -0
- package/dist/core/work-digest/active-spans.js +29 -0
- package/dist/core/work-digest/active-spans.js.map +1 -0
- package/dist/core/work-digest/attribute.d.ts +15 -0
- package/dist/core/work-digest/attribute.js +76 -0
- package/dist/core/work-digest/attribute.js.map +1 -0
- package/dist/core/work-digest/build-work-digest.d.ts +12 -0
- package/dist/core/work-digest/build-work-digest.js +69 -0
- package/dist/core/work-digest/build-work-digest.js.map +1 -0
- package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
- package/dist/core/work-digest/compose-work-digest.js +52 -0
- package/dist/core/work-digest/compose-work-digest.js.map +1 -0
- package/dist/core/work-digest/merge-active.d.ts +10 -0
- package/dist/core/work-digest/merge-active.js +22 -0
- package/dist/core/work-digest/merge-active.js.map +1 -0
- package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
- package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
- package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
- package/dist/core/work-digest/topics.d.ts +18 -0
- package/dist/core/work-digest/topics.js +26 -0
- package/dist/core/work-digest/topics.js.map +1 -0
- package/dist/core/work-digest/types.d.ts +45 -0
- package/dist/core/work-digest/types.js +2 -0
- package/dist/core/work-digest/types.js.map +1 -0
- package/dist/core/workstream/bind.d.ts +26 -0
- package/dist/core/workstream/bind.js +42 -0
- package/dist/core/workstream/bind.js.map +1 -0
- package/dist/core/workstream/compose-recall.d.ts +2 -0
- package/dist/core/workstream/compose-recall.js +39 -0
- package/dist/core/workstream/compose-recall.js.map +1 -0
- package/dist/core/workstream/merge-suggest.d.ts +19 -0
- package/dist/core/workstream/merge-suggest.js +61 -0
- package/dist/core/workstream/merge-suggest.js.map +1 -0
- package/dist/core/workstream/model.d.ts +21 -0
- package/dist/core/workstream/model.js +9 -0
- package/dist/core/workstream/model.js.map +1 -0
- package/dist/core/workstream/name-match.d.ts +8 -0
- package/dist/core/workstream/name-match.js +18 -0
- package/dist/core/workstream/name-match.js.map +1 -0
- package/dist/core/workstream/resolve.d.ts +10 -0
- package/dist/core/workstream/resolve.js +21 -0
- package/dist/core/workstream/resolve.js.map +1 -0
- package/dist/core/workstream/rollup.d.ts +12 -0
- package/dist/core/workstream/rollup.js +17 -0
- package/dist/core/workstream/rollup.js.map +1 -0
- package/dist/core/workstream/work-topics.d.ts +10 -0
- package/dist/core/workstream/work-topics.js +55 -0
- package/dist/core/workstream/work-topics.js.map +1 -0
- package/dist/hook/hook-helpers.d.ts +4 -0
- package/dist/hook/hook-helpers.js +36 -0
- package/dist/hook/hook-helpers.js.map +1 -0
- package/dist/hook/pre-compact-hook.d.ts +23 -0
- package/dist/hook/pre-compact-hook.js +67 -0
- package/dist/hook/pre-compact-hook.js.map +1 -0
- package/dist/hook/prompt-recall-hook.d.ts +38 -0
- package/dist/hook/prompt-recall-hook.js +124 -20
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/recall-gate.d.ts +29 -0
- package/dist/hook/recall-gate.js +50 -0
- package/dist/hook/recall-gate.js.map +1 -0
- package/dist/hook/recall-over-http.d.ts +3 -2
- package/dist/hook/recall-over-http.js +26 -13
- package/dist/hook/recall-over-http.js.map +1 -1
- package/dist/hook/recent-context.d.ts +25 -0
- package/dist/hook/recent-context.js +104 -0
- package/dist/hook/recent-context.js.map +1 -0
- package/dist/hook/score-floor.d.ts +27 -0
- package/dist/hook/score-floor.js +42 -0
- package/dist/hook/score-floor.js.map +1 -0
- package/dist/hook/session-end-hook.js +8 -29
- package/dist/hook/session-end-hook.js.map +1 -1
- package/dist/hook/session-start-hook.js +26 -60
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +24 -60
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/http/app.d.ts +6 -0
- package/dist/http/app.js +52 -44
- package/dist/http/app.js.map +1 -1
- package/dist/install/setup.js +2 -0
- package/dist/install/setup.js.map +1 -1
- package/dist/llm/build-classifier.d.ts +22 -0
- package/dist/llm/build-classifier.js +42 -0
- package/dist/llm/build-classifier.js.map +1 -0
- package/dist/llm/build-embedder.d.ts +11 -0
- package/dist/llm/build-embedder.js +42 -0
- package/dist/llm/build-embedder.js.map +1 -0
- package/dist/llm/bundled-embedder-client.d.ts +37 -0
- package/dist/llm/bundled-embedder-client.js +90 -0
- package/dist/llm/bundled-embedder-client.js.map +1 -0
- package/dist/llm/classifier-box.d.ts +13 -5
- package/dist/llm/classifier-box.js +28 -6
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/classifier-egress.d.ts +18 -0
- package/dist/llm/classifier-egress.js +59 -0
- package/dist/llm/classifier-egress.js.map +1 -0
- package/dist/llm/client-shared.d.ts +4 -0
- package/dist/llm/client-shared.js +39 -0
- package/dist/llm/client-shared.js.map +1 -0
- package/dist/llm/deepseek-client.d.ts +20 -0
- package/dist/llm/deepseek-client.js +59 -45
- package/dist/llm/deepseek-client.js.map +1 -1
- package/dist/llm/embedder-info.d.ts +15 -0
- package/dist/llm/embedder-info.js +27 -0
- package/dist/llm/embedder-info.js.map +1 -0
- package/dist/llm/model-quirks.d.ts +4 -0
- package/dist/llm/model-quirks.js +7 -0
- package/dist/llm/model-quirks.js.map +1 -0
- package/dist/llm/naming.d.ts +5 -0
- package/dist/llm/naming.js +20 -0
- package/dist/llm/naming.js.map +1 -0
- package/dist/llm/ollama-client.d.ts +7 -8
- package/dist/llm/ollama-client.js +59 -51
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/llm/ollama-code-embedder.d.ts +8 -3
- package/dist/llm/ollama-code-embedder.js +14 -24
- package/dist/llm/ollama-code-embedder.js.map +1 -1
- package/dist/llm/openai-code-embedder-client.d.ts +25 -0
- package/dist/llm/openai-code-embedder-client.js +52 -0
- package/dist/llm/openai-code-embedder-client.js.map +1 -0
- package/dist/llm/openai-embedder-client.d.ts +41 -0
- package/dist/llm/openai-embedder-client.js +83 -0
- package/dist/llm/openai-embedder-client.js.map +1 -0
- package/dist/mcp/server.d.ts +69 -2
- package/dist/mcp/server.js +404 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/code-embedder.d.ts +6 -1
- package/dist/ports/code-exemplar-store.d.ts +5 -0
- package/dist/ports/entity-store.d.ts +35 -0
- package/dist/ports/entity-store.js +7 -0
- package/dist/ports/entity-store.js.map +1 -0
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/llm-client.d.ts +9 -1
- package/dist/ports/outcome.d.ts +85 -0
- package/dist/ports/outcome.js +11 -0
- package/dist/ports/outcome.js.map +1 -0
- package/dist/ports/session-store.d.ts +12 -6
- package/dist/ports/storage.d.ts +7 -30
- package/dist/ports/storage.js +3 -7
- package/dist/ports/storage.js.map +1 -1
- package/dist/ports/transcript-adapter.d.ts +2 -0
- package/dist/ports/workstream-store.d.ts +22 -0
- package/dist/ports/workstream-store.js +2 -0
- package/dist/ports/workstream-store.js.map +1 -0
- package/dist/shared/net.d.ts +3 -0
- package/dist/shared/net.js +4 -0
- package/dist/shared/net.js.map +1 -0
- package/dist/shared/types.d.ts +56 -2
- package/dist/shared/types.js +4 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/ui/assets/{index-CMpQkbQy.js → index-CSaBtgXD.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/fixtures/classifier-gold/README.md +94 -0
- package/fixtures/classifier-gold/reference.json +318 -0
- package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
- package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
- package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
- package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
- package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
- package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
- package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
- package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
- package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
- package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
- package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
- package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
- package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
- package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
- package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
- package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
- package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
- package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
- package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
- package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
- package/migrations/000_initial_schema.sql +1 -1
- package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
- package/migrations/025_workstreams.sql +30 -0
- package/migrations/026_embedding_config.sql +12 -0
- package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
- package/migrations/028_classifier_provenance.sql +5 -0
- package/migrations/029_project_scope.sql +21 -0
- package/migrations/032_session_derivables.sql +11 -0
- package/migrations/pg/001_initial.sql +34 -0
- package/migrations/pg/019_split_replaces.sql +16 -7
- package/migrations/pg/025_workstreams.sql +34 -0
- package/migrations/pg/026_embedding_config.sql +12 -0
- package/migrations/pg/028_classifier_provenance.sql +5 -0
- package/migrations/pg/029_entity_variants.sql +9 -0
- package/migrations/pg/030_project_scope.sql +23 -0
- package/migrations/pg/031_widen_session_edges_kind.sql +11 -0
- package/migrations/pg/032_session_derivables.sql +17 -0
- package/nlm/index.js +405 -39
- package/package.json +12 -2
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/scripts/prompt-recall-hook.mjs +413 -43
- package/plugin/scripts/session-start-hook.mjs +295 -61
- package/plugin/scripts/stop-hook.mjs +89 -66
- package/plugin-hermes-agent/__init__.py +2 -1
- package/templates/agent-contract/README.md +24 -0
- package/templates/agent-contract/claude-code.md +40 -0
- package/templates/agent-contract/generic.md +38 -0
- package/dist/core/embedding/embed-normalize.d.ts +0 -28
- package/dist/core/embedding/embed-normalize.js +0 -98
- package/dist/core/embedding/embed-normalize.js.map +0 -1
- package/dist/core/storage/pg-tx-context.d.ts +0 -52
- package/dist/core/storage/pg-tx-context.js +0 -149
- package/dist/core/storage/pg-tx-context.js.map +0 -1
package/dist/cli/nlm.js
CHANGED
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
* nlm disconnect claude-code — remove MCP block from ~/.mcp.json
|
|
23
23
|
* nlm disconnect codex — remove Codex plugin
|
|
24
24
|
* nlm digest — print a daily-activity digest (or --telegram to post)
|
|
25
|
+
* nlm init - print (or write) the agent recall contract snippet
|
|
25
26
|
*/
|
|
26
|
-
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
28
|
import { dirname, resolve, join } from "node:path";
|
|
28
29
|
import { homedir } from "node:os";
|
|
29
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, copyFileSync } from "node:fs";
|
|
30
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, copyFileSync, realpathSync, appendFileSync } from "node:fs";
|
|
30
31
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
31
32
|
import { Command } from "commander";
|
|
32
33
|
import pkg from "../../package.json" with { type: "json" };
|
|
@@ -40,12 +41,14 @@ import { PgStorage } from "../core/storage/pg-storage.js";
|
|
|
40
41
|
import { applyPendingRestore, stageRestore } from "../core/storage/db-restore.js";
|
|
41
42
|
import { listBackupDates, resolveBackup, runRollingBackup } from "../core/storage/backup-rotation.js";
|
|
42
43
|
import { createApp } from "../http/app.js";
|
|
43
|
-
import { createMcpServer } from "../mcp/server.js";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
44
|
+
import { createMcpServer, listMergeSuggestionsHandler, mergeWorkstreamsHandler, rebindSessionHandler, recallWorkstreamHandler, renameWorkstreamHandler, retireWorkstreamHandler } from "../mcp/server.js";
|
|
45
|
+
import { classifierEgressNotice } from "../llm/classifier-egress.js";
|
|
46
|
+
import { buildClassifier } from "../llm/build-classifier.js";
|
|
47
|
+
import { OllamaCodeEmbedder, DEFAULT_CODE_EMBED_MODEL } from "../llm/ollama-code-embedder.js";
|
|
48
|
+
import { OpenAICodeEmbedderClient } from "../llm/openai-code-embedder-client.js";
|
|
49
|
+
import { resolveEmbedderInfo } from "../llm/embedder-info.js";
|
|
48
50
|
import { autoloadEnv } from "../llm/env-autoload.js";
|
|
51
|
+
import { buildEmbedder as _buildEmbedder } from "../llm/build-embedder.js";
|
|
49
52
|
import { addHook, buildHookCommand, removeHook } from "../core/hook/claude-settings.js";
|
|
50
53
|
import { codexBinaryAvailable, connectCodex, disconnectCodex, repairCodex, pluginScriptsDir, } from "../install/codex.js";
|
|
51
54
|
import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks, mcpConfigPath } from "../install/claude-code.js";
|
|
@@ -56,6 +59,10 @@ import { embeddingModelPresent, ensureMcpToken, ollamaModelPresent } from "../in
|
|
|
56
59
|
import { connectCursor, disconnectCursor } from "../install/cursor.js";
|
|
57
60
|
import { describeRemove, describeUpsert, installCursorRules, installOpencodeRules, installWindsurfRules, uninstallCursorRules, uninstallOpencodeRules, uninstallWindsurfRules, } from "../install/rules-install.js";
|
|
58
61
|
import { runSupersedeCommand } from "./supersede.js";
|
|
62
|
+
import { runInitCommand } from "./init.js";
|
|
63
|
+
import { runScopeBackfill, formatBackfillResult } from "./scope-backfill.js";
|
|
64
|
+
import { runScopeCoverage, formatCoverageResult } from "./scope-coverage.js";
|
|
65
|
+
import { loadAliasMap } from "../core/scope/alias-map.js";
|
|
59
66
|
import { getUpdateStatus } from "../core/update-check/check.js";
|
|
60
67
|
import { connectHermes, disconnectHermes, hermesConfigPath } from "../install/hermes.js";
|
|
61
68
|
import { connectHermesAgent, disconnectHermesAgent, hermesAgentPluginDir } from "../install/hermes-agent.js";
|
|
@@ -64,8 +71,16 @@ import { connectPi, disconnectPi, piSettingsPath } from "../install/pi.js";
|
|
|
64
71
|
import { runSetup } from "../install/setup.js";
|
|
65
72
|
import { runParity } from "./classify-parity.js";
|
|
66
73
|
import { reembedCorpus } from "../core/embedding/embed-backfill.js";
|
|
74
|
+
import { reembedCorpusPg } from "../core/embedding/pg-embed-backfill.js";
|
|
75
|
+
import { backfillExemplarEmbeddings } from "../core/exemplars/embed-backfill.js";
|
|
76
|
+
import { warmCodeEmbedder } from "../core/exemplars/warm-embedder.js";
|
|
77
|
+
import { markWarm } from "../core/health/warmup-state.js";
|
|
78
|
+
import { setLaneHealth } from "../core/health/embedding-lane-state.js";
|
|
79
|
+
import { setCorpusSnapshot, corpusSnapshot } from "../core/health/corpus-state.js";
|
|
80
|
+
import { computeCorpusStats, sqliteCorpusStatsDeps, parseCorpusThresholds, thresholdState } from "../core/metrics/corpus-stats.js";
|
|
81
|
+
import { computeReDerivationRate, sqliteReDerivationDeps } from "../core/metrics/re-derivation.js";
|
|
82
|
+
import { reconcileLane } from "../core/embedding/embedding-config.js";
|
|
67
83
|
import { backfillFacts } from "../core/facts/backfill-facts.js";
|
|
68
|
-
import { normalizeEmbeddings } from "../core/embedding/embed-normalize.js";
|
|
69
84
|
import { ScanScheduler } from "../core/scheduler/scheduler.js";
|
|
70
85
|
import { MemoSweepScheduler } from "../core/hook/memo-sweep.js";
|
|
71
86
|
import { isAgentLoaded, isBenignBootoutError } from "./launchctl-helpers.js";
|
|
@@ -75,14 +90,22 @@ import { applyEnvAssignment } from "./config-env.js";
|
|
|
75
90
|
import { adapterFromSource } from "../core/adapters/from-source.js";
|
|
76
91
|
import { runDigest } from "./digest.js";
|
|
77
92
|
import { installScope } from "../core/signals/install-scope.js";
|
|
93
|
+
import { buildWorkDigest } from "../core/work-digest/build-work-digest.js";
|
|
94
|
+
import { composeWorkDigest } from "../core/work-digest/compose-work-digest.js";
|
|
95
|
+
import { defaultTopicProvider, aliasTopicProvider } from "../core/work-digest/topics.js";
|
|
78
96
|
import { runChecksOnSqlite, runChecksOnPg, applyFixOnSqlite, applyFixOnPg } from "../core/integrity/check-invariants.js";
|
|
97
|
+
import { normalizeLabel } from "../core/workstream/model.js";
|
|
98
|
+
import { resolveWorkstreamId } from "../core/workstream/resolve.js";
|
|
99
|
+
import { suggestMerges } from "../core/entities/dedup-suggest.js";
|
|
100
|
+
import { DEFAULT_NLM_PORT } from "../shared/net.js";
|
|
79
101
|
const __filename = fileURLToPath(import.meta.url);
|
|
80
102
|
const __dirname = dirname(__filename);
|
|
81
103
|
const MIGRATIONS_DIR = resolve(__dirname, "../../migrations");
|
|
104
|
+
const CLASSIFIER_FIXTURES_DIR = resolve(__dirname, "../../fixtures/classifier-gold");
|
|
82
105
|
const PG_MIGRATIONS_DIR = join(fileURLToPath(new URL(".", import.meta.url)), "../../migrations/pg");
|
|
83
106
|
const UI_DIST = resolve(__dirname, "../../dist/ui");
|
|
84
107
|
const DEFAULT_DB_PATH = resolve(homedir(), ".nlm/canonical.sqlite");
|
|
85
|
-
const DEFAULT_PORT =
|
|
108
|
+
const DEFAULT_PORT = Number.parseInt(DEFAULT_NLM_PORT, 10);
|
|
86
109
|
function dbPath() {
|
|
87
110
|
return process.env["NLM_DB_PATH"] ?? DEFAULT_DB_PATH;
|
|
88
111
|
}
|
|
@@ -98,19 +121,121 @@ function port() {
|
|
|
98
121
|
function ollamaUrl() {
|
|
99
122
|
return process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434";
|
|
100
123
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
124
|
+
export function resolveDigestDate(date) {
|
|
125
|
+
if (date !== undefined) {
|
|
126
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
127
|
+
throw new Error(`invalid --date "${date}"; expected YYYY-MM-DD`);
|
|
128
|
+
}
|
|
129
|
+
return date;
|
|
130
|
+
}
|
|
131
|
+
const d = new Date();
|
|
132
|
+
const mo = String(d.getMonth() + 1).padStart(2, "0");
|
|
133
|
+
const da = String(d.getDate()).padStart(2, "0");
|
|
134
|
+
return `${d.getFullYear()}-${mo}-${da}`;
|
|
135
|
+
}
|
|
136
|
+
function workDigestEnv() {
|
|
137
|
+
const idle = Number.parseInt(process.env["NLM_WORK_IDLE_THRESHOLD_MIN"] ?? "5", 10);
|
|
138
|
+
const deep = Number.parseInt(process.env["NLM_WORK_DEEP_BLOCK_MIN"] ?? "25", 10);
|
|
139
|
+
return {
|
|
140
|
+
idleThresholdMin: Number.isFinite(idle) && idle > 0 ? idle : 5,
|
|
141
|
+
deepBlockMin: Number.isFinite(deep) && deep > 0 ? deep : 25,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const TREND_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
145
|
+
export function shouldAppendTrend(lastLineTs, now) {
|
|
146
|
+
if (lastLineTs === null)
|
|
147
|
+
return true;
|
|
148
|
+
const parsed = Date.parse(lastLineTs);
|
|
149
|
+
if (!Number.isFinite(parsed))
|
|
150
|
+
return true;
|
|
151
|
+
return now - parsed >= TREND_INTERVAL_MS;
|
|
152
|
+
}
|
|
153
|
+
function loadTopicProvider() {
|
|
154
|
+
try {
|
|
155
|
+
const raw = readFileSync(join(homedir(), ".nlm", "work-topics.json"), "utf8");
|
|
156
|
+
const map = JSON.parse(raw);
|
|
157
|
+
return aliasTopicProvider(map);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return defaultTopicProvider;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Build the recall/document embedder. Delegates to the shared factory in
|
|
164
|
+
* src/llm/build-embedder.ts — see that file for provider routing details. */
|
|
165
|
+
function buildEmbedder() {
|
|
166
|
+
return _buildEmbedder();
|
|
167
|
+
}
|
|
168
|
+
/** Build the code-lane embedder. Follows the same destination as the prose
|
|
169
|
+
* embedder (NLM_EMBED_PROVIDER / NLM_EMBED_BASE_URL) but with its own model
|
|
170
|
+
* (NLM_CODE_EMBED_MODEL, e.g. text-embedding-coderankembed on LM Studio).
|
|
171
|
+
* Switching providers on existing exemplars requires `nlm embed-backfill
|
|
172
|
+
* --exemplars`. */
|
|
173
|
+
function buildCodeEmbedder() {
|
|
174
|
+
const provider = (process.env["NLM_EMBED_PROVIDER"] ?? "ollama").toLowerCase();
|
|
175
|
+
if (provider === "openai") {
|
|
110
176
|
autoloadEnv();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
177
|
+
const baseUrl = process.env["NLM_EMBED_BASE_URL"];
|
|
178
|
+
if (!baseUrl) {
|
|
179
|
+
throw new Error("NLM_EMBED_PROVIDER=openai requires NLM_EMBED_BASE_URL for the code embedder too, " +
|
|
180
|
+
"e.g. http://localhost:1234/v1 for LM Studio.");
|
|
181
|
+
}
|
|
182
|
+
return new OpenAICodeEmbedderClient({
|
|
183
|
+
baseUrl,
|
|
184
|
+
...(process.env["NLM_CODE_EMBED_MODEL"] ? { model: process.env["NLM_CODE_EMBED_MODEL"] } : {}),
|
|
185
|
+
...(process.env["NLM_EMBED_API_KEY"] ? { apiKey: process.env["NLM_EMBED_API_KEY"] } : {}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// The bundled provider has no code-embedding engine (CodeRankEmbed has no ONNX equivalent
|
|
189
|
+
// in slice 1). Fall through to the Ollama default: the startup probe will fail soft and
|
|
190
|
+
// the code-exemplar lane stays unknown, which is the intended degradation.
|
|
191
|
+
return new OllamaCodeEmbedder({ baseUrl: ollamaUrl() });
|
|
192
|
+
}
|
|
193
|
+
async function reconcileEmbeddingLanes(storage, embedder, codeEmbedder) {
|
|
194
|
+
const configStore = storage.embeddingConfig;
|
|
195
|
+
if (!configStore)
|
|
196
|
+
return;
|
|
197
|
+
const nowIso = new Date().toISOString();
|
|
198
|
+
try {
|
|
199
|
+
const result = await embedder.embed("nlm dimension probe", "query");
|
|
200
|
+
const dim = result.vector.length;
|
|
201
|
+
const info = resolveEmbedderInfo(process.env, dim);
|
|
202
|
+
const runtime = { lane: "prose", provider: info.provider, model: info.model, dim };
|
|
203
|
+
const rec = reconcileLane(configStore, runtime, nowIso);
|
|
204
|
+
if (rec.state === "stale") {
|
|
205
|
+
const stored = rec.stored;
|
|
206
|
+
console.error(`nlm-memory: prose embedding lane stale: stored ${stored.provider}/${stored.model}@${stored.dim}` +
|
|
207
|
+
` vs runtime ${info.provider}/${info.model}@${dim}; run \`nlm embed-backfill\` to re-embed`);
|
|
208
|
+
setLaneHealth("prose", "stale");
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
setLaneHealth("prose", "ok");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// probe failure; prose lane stays unknown
|
|
216
|
+
}
|
|
217
|
+
if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1")
|
|
218
|
+
return;
|
|
219
|
+
try {
|
|
220
|
+
const result = await codeEmbedder.embed("nlm dimension probe", "query");
|
|
221
|
+
const dim = result.dim;
|
|
222
|
+
const provider = (process.env["NLM_EMBED_PROVIDER"] ?? "ollama").toLowerCase();
|
|
223
|
+
const model = process.env["NLM_CODE_EMBED_MODEL"] ?? DEFAULT_CODE_EMBED_MODEL;
|
|
224
|
+
const runtime = { lane: "code", provider, model, dim };
|
|
225
|
+
const rec = reconcileLane(configStore, runtime, nowIso);
|
|
226
|
+
if (rec.state === "stale") {
|
|
227
|
+
const stored = rec.stored;
|
|
228
|
+
console.error(`nlm-memory: code embedding lane stale: stored ${stored.provider}/${stored.model}@${stored.dim}` +
|
|
229
|
+
` vs runtime ${provider}/${model}@${dim}; run \`nlm embed-backfill --exemplars\` to re-embed`);
|
|
230
|
+
setLaneHealth("code", "stale");
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
setLaneHealth("code", "ok");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// probe failure; code lane stays unknown
|
|
238
|
+
}
|
|
114
239
|
}
|
|
115
240
|
async function buildAdapters(sources) {
|
|
116
241
|
// Sources table is the source of truth. Each enabled row maps to one
|
|
@@ -172,21 +297,56 @@ async function buildStack() {
|
|
|
172
297
|
// seedDefaults (not on ProviderRegistryPort), so this narrows to the SQLite case.
|
|
173
298
|
if (providers instanceof ProviderRegistry)
|
|
174
299
|
await providers.seedDefaults();
|
|
175
|
-
// Recall only uses embed().
|
|
176
|
-
//
|
|
177
|
-
|
|
300
|
+
// Recall only uses embed(). Default embedder is local Ollama; NLM_EMBED_*
|
|
301
|
+
// can point it at any OpenAI-compatible endpoint. Classifier is wired
|
|
302
|
+
// separately for Phase D ingest.
|
|
303
|
+
const embedder = buildEmbedder();
|
|
178
304
|
const classifier = buildClassifier();
|
|
305
|
+
const wsStore = storage.workstreams;
|
|
179
306
|
const recall = new RecallService({
|
|
180
307
|
store,
|
|
181
308
|
llm: embedder,
|
|
182
309
|
factStore: facts,
|
|
183
310
|
exemplarStore: storage.exemplars,
|
|
184
|
-
codeEmbedder:
|
|
311
|
+
codeEmbedder: buildCodeEmbedder(),
|
|
185
312
|
installScope: scope,
|
|
313
|
+
resolveWorkstreamMembers: async (idOrLabel) => {
|
|
314
|
+
const all = await wsStore.listAll();
|
|
315
|
+
const byId = new Map(all.map((w) => [w.id, { id: w.id, mergedInto: w.mergedInto }]));
|
|
316
|
+
const target = all.find((w) => w.id === idOrLabel)
|
|
317
|
+
?? all.find((w) => normalizeLabel(w.label) === normalizeLabel(idOrLabel));
|
|
318
|
+
if (!target)
|
|
319
|
+
return [];
|
|
320
|
+
const survivor = resolveWorkstreamId(target.id, byId);
|
|
321
|
+
return all.filter((w) => resolveWorkstreamId(w.id, byId) === survivor).map((w) => w.id);
|
|
322
|
+
},
|
|
186
323
|
});
|
|
187
324
|
const factRecall = new FactRecallService({ factStore: facts, llm: embedder });
|
|
188
325
|
return { storage, store, facts, signals, scope, sources, providers, recall, factRecall, embedder, classifier };
|
|
189
326
|
}
|
|
327
|
+
export async function runClassifierEvalCommand(deps) {
|
|
328
|
+
const { runClassifierFixtureEval } = await import("../core/eval/classifier-fixture-eval.js");
|
|
329
|
+
const result = await runClassifierFixtureEval(deps.classify, deps.fixturesDir, deps.limit !== undefined ? { limit: deps.limit } : undefined);
|
|
330
|
+
if (deps.json) {
|
|
331
|
+
deps.stdout(JSON.stringify({ ...result, provider: deps.provider, model: deps.model }, null, 2) + "\n");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const a = result.aggregate;
|
|
335
|
+
const pct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
336
|
+
deps.stdout(`lane: ${deps.provider}/${deps.model}\n`);
|
|
337
|
+
deps.stdout(`n=${result.perTranscript.length}\n`);
|
|
338
|
+
deps.stdout(`schema-valid: ${pct(a.schemaValidRate)}\n`);
|
|
339
|
+
deps.stdout(`label-accuracy: ${pct(a.labelAccuracy)}\n`);
|
|
340
|
+
deps.stdout(`entity-F1: ${pct(a.entityF1)}\n`);
|
|
341
|
+
deps.stdout(`decision-F1: ${pct(a.decisionF1)}\n`);
|
|
342
|
+
deps.stdout(`conf-calibration: ${pct(a.confidenceCalibrationRate)}\n`);
|
|
343
|
+
deps.stdout(`p50-latency: ${a.p50LatencyMs}ms\n`);
|
|
344
|
+
deps.stdout(`p95-latency: ${a.p95LatencyMs}ms\n`);
|
|
345
|
+
if (a.timedOutOrUnreachable > 0) {
|
|
346
|
+
deps.stdout(`timed-out: ${a.timedOutOrUnreachable} fixture(s) scored as failures (LLM unreachable/timeout)\n`);
|
|
347
|
+
}
|
|
348
|
+
deps.stdout(`see: docs/classifier-tiers.md\n`);
|
|
349
|
+
}
|
|
190
350
|
const program = new Command();
|
|
191
351
|
program
|
|
192
352
|
.name("nlm")
|
|
@@ -208,6 +368,7 @@ program
|
|
|
208
368
|
const { storage, store, facts, signals, scope, sources, providers, recall, factRecall, embedder, classifier } = await buildStack();
|
|
209
369
|
const { existsSync } = await import("node:fs");
|
|
210
370
|
const hasMcpToken = Boolean(process.env["NLM_MCP_TOKEN"]);
|
|
371
|
+
const codeEmb = buildCodeEmbedder();
|
|
211
372
|
const app = createApp({
|
|
212
373
|
recall,
|
|
213
374
|
store,
|
|
@@ -223,15 +384,19 @@ program
|
|
|
223
384
|
embedder,
|
|
224
385
|
store,
|
|
225
386
|
...(facts ? { factStore: facts } : {}),
|
|
387
|
+
classifierDescriptor: { provider: classifier.provider, model: classifier.model },
|
|
226
388
|
},
|
|
227
389
|
signalStore: signals,
|
|
228
390
|
installScope: scope,
|
|
391
|
+
// Signal scope inheritance (NLM_SCOPE_STAMP): a signal with only a
|
|
392
|
+
// session_id inherits that session's stamped scope at ingest.
|
|
393
|
+
sessionScopeReader: store,
|
|
229
394
|
// Code-exemplar lane. The store + code embedder are always wired; the
|
|
230
395
|
// NLM_CODE_EXEMPLARS_ENABLED flag gates capture (POST /api/signal) and
|
|
231
396
|
// the /api/exemplar + /api/recall-code routes at request time.
|
|
232
397
|
exemplarStore: storage.exemplars,
|
|
233
|
-
codeEmbedder:
|
|
234
|
-
embedderInfo:
|
|
398
|
+
codeEmbedder: codeEmb,
|
|
399
|
+
embedderInfo: resolveEmbedderInfo(),
|
|
235
400
|
...(existsSync(UI_DIST) ? { uiDist: UI_DIST } : {}),
|
|
236
401
|
// Wire POST /mcp only when NLM_MCP_TOKEN is present. Absent = route never
|
|
237
402
|
// mounts, zero attack surface. Present = token-gated Streamable-HTTP MCP
|
|
@@ -246,12 +411,28 @@ program
|
|
|
246
411
|
// Parity with the stdio `nlm mcp` server: remote/container MCP
|
|
247
412
|
// clients hitting POST /mcp get recall_code too when the flag is on.
|
|
248
413
|
exemplarStore: storage.exemplars,
|
|
249
|
-
codeEmbedder:
|
|
414
|
+
codeEmbedder: codeEmb,
|
|
250
415
|
installScope: scope,
|
|
416
|
+
signalStore: signals,
|
|
417
|
+
sessionScopeReader: store,
|
|
418
|
+
workDigest: { store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() },
|
|
419
|
+
workstreams: { store: storage.workstreams, sessions: store, facts: facts, exemplars: storage.exemplars },
|
|
251
420
|
},
|
|
252
421
|
}
|
|
253
422
|
: {}),
|
|
254
423
|
});
|
|
424
|
+
warmCodeEmbedder(codeEmb);
|
|
425
|
+
void reconcileEmbeddingLanes(storage, embedder, codeEmb).catch(() => { });
|
|
426
|
+
void embedder
|
|
427
|
+
.embed("warmup init", "query")
|
|
428
|
+
.then(() => markWarm("textEmbedder"))
|
|
429
|
+
.catch(() => { });
|
|
430
|
+
setImmediate(() => {
|
|
431
|
+
void recall
|
|
432
|
+
.search({ query: "warmup init", mode: "keyword", limit: 1 })
|
|
433
|
+
.then(() => markWarm("fts5"))
|
|
434
|
+
.catch(() => { });
|
|
435
|
+
});
|
|
255
436
|
const p = port();
|
|
256
437
|
serve({ fetch: app.fetch, port: p, hostname: "127.0.0.1" }, (info) => {
|
|
257
438
|
console.error(`nlm-memory http listening on http://localhost:${info.port}`);
|
|
@@ -260,6 +441,14 @@ program
|
|
|
260
441
|
}
|
|
261
442
|
console.error(` db: ${dbPath()}`);
|
|
262
443
|
console.error(` ollama: ${ollamaUrl()}`);
|
|
444
|
+
const classifier = (process.env["NLM_CLASSIFIER"] ?? "ollama").toLowerCase();
|
|
445
|
+
const egress = classifierEgressNotice(classifier, process.env["NLM_CLASSIFIER_BASE_URL"]);
|
|
446
|
+
const classifyTarget = classifier === "openai" && process.env["NLM_CLASSIFIER_BASE_URL"]
|
|
447
|
+
? `${classifier} (${process.env["NLM_CLASSIFIER_BASE_URL"]})`
|
|
448
|
+
: classifier;
|
|
449
|
+
console.error(` classify: ${classifyTarget} [${egress ? "cloud egress" : "local"}]`);
|
|
450
|
+
if (egress)
|
|
451
|
+
console.error(` notice: ${egress}`);
|
|
263
452
|
// Passive update notice. Fire-and-forget so a slow npm registry
|
|
264
453
|
// round-trip can't delay the startup banner; surfaced only when
|
|
265
454
|
// strictly behind. See src/core/update-check/check.ts for the
|
|
@@ -306,6 +495,61 @@ program
|
|
|
306
495
|
void signals.pruneOlderThan(cutoff).catch(() => { });
|
|
307
496
|
}, SIGNAL_PRUNE_INTERVAL_MS);
|
|
308
497
|
signalPruneTimer.unref();
|
|
498
|
+
// Corpus monitor: 24h stats job + weekly re-derivation trend. SQLite only.
|
|
499
|
+
// First run is delayed ~60s so warmup finishes first; then repeats every 24h.
|
|
500
|
+
if (!(storage instanceof PgStorage)) {
|
|
501
|
+
const CORPUS_MONITOR_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
502
|
+
const CORPUS_MONITOR_INITIAL_DELAY_MS = 60 * 1000;
|
|
503
|
+
const nlmDataDir = join(homedir(), ".nlm");
|
|
504
|
+
const corpusStatsPath = join(nlmDataDir, "corpus-stats.jsonl");
|
|
505
|
+
const rederivTrendPath = join(nlmDataDir, "re-derivation-trend.jsonl");
|
|
506
|
+
const runCorpusMonitor = async () => {
|
|
507
|
+
try {
|
|
508
|
+
const sqliteStorage = storage;
|
|
509
|
+
const rawDb = sqliteStorage.rawDb();
|
|
510
|
+
const deps = sqliteCorpusStatsDeps(rawDb, dbPath());
|
|
511
|
+
const stats = await computeCorpusStats(deps);
|
|
512
|
+
const thresholds = parseCorpusThresholds(process.env);
|
|
513
|
+
const state = thresholdState(stats.dbBytes, thresholds);
|
|
514
|
+
const ts = new Date().toISOString();
|
|
515
|
+
// hapaxEntities derives from the denormalized entities.session_count column,
|
|
516
|
+
// which drifts (~6% today) until the entity-merge primitive recomputes counts
|
|
517
|
+
// exactly. This is a monitoring approximation, not ground truth.
|
|
518
|
+
const snap = { ...stats, state, lastComputedAt: ts };
|
|
519
|
+
setCorpusSnapshot(snap);
|
|
520
|
+
mkdirSync(nlmDataDir, { recursive: true });
|
|
521
|
+
appendFileSync(corpusStatsPath, JSON.stringify({ ts, ...stats, state }) + "\n", "utf8");
|
|
522
|
+
if (state === "warn" || state === "alert") {
|
|
523
|
+
console.error(`[corpus-monitor] ${state}: db is ${stats.dbBytes} bytes`);
|
|
524
|
+
}
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
let lastTrendTs = null;
|
|
527
|
+
try {
|
|
528
|
+
const trendContent = readFileSync(rederivTrendPath, "utf8");
|
|
529
|
+
const lines = trendContent.split("\n").filter(Boolean);
|
|
530
|
+
const lastLine = lines[lines.length - 1];
|
|
531
|
+
if (lastLine) {
|
|
532
|
+
const parsed = JSON.parse(lastLine);
|
|
533
|
+
lastTrendTs = typeof parsed.ts === "string" ? parsed.ts : null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// file absent or unreadable; treat as no prior entry
|
|
538
|
+
}
|
|
539
|
+
if (shouldAppendTrend(lastTrendTs, now)) {
|
|
540
|
+
const rdDeps = sqliteReDerivationDeps(rawDb);
|
|
541
|
+
const report = await computeReDerivationRate(rdDeps, 42);
|
|
542
|
+
appendFileSync(rederivTrendPath, JSON.stringify({ ts, windowDays: 42, rate: report.rate, pairCount: report.pairs.length, eligible: report.eligible }) + "\n", "utf8");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
console.error(`[corpus-monitor] error: ${e instanceof Error ? e.message : String(e)}`);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const corpusMonitorTimer = setInterval(() => { void runCorpusMonitor(); }, CORPUS_MONITOR_INTERVAL_MS);
|
|
550
|
+
corpusMonitorTimer.unref();
|
|
551
|
+
setTimeout(() => { void runCorpusMonitor(); }, CORPUS_MONITOR_INITIAL_DELAY_MS).unref();
|
|
552
|
+
}
|
|
309
553
|
// Memo sweep runs independently of the transcript scheduler — it's the
|
|
310
554
|
// backstop for SessionEnd hook unreliability (crashes, kill -9, IDE
|
|
311
555
|
// force-close don't fire SessionEnd, so memo files would otherwise
|
|
@@ -329,7 +573,9 @@ program
|
|
|
329
573
|
installScope: scope,
|
|
330
574
|
intervalMs: opts.intervalMin * 60_000,
|
|
331
575
|
exemplarStore: storage.exemplars,
|
|
332
|
-
codeEmbedder:
|
|
576
|
+
codeEmbedder: buildCodeEmbedder(),
|
|
577
|
+
workstreams: storage.workstreams,
|
|
578
|
+
classifierDescriptor: { provider: classifier.provider, model: classifier.model },
|
|
333
579
|
});
|
|
334
580
|
scheduler.start();
|
|
335
581
|
console.error(` scheduler: ${adapters.map((a) => a.name).join(", ")} every ${opts.intervalMin}m`);
|
|
@@ -368,6 +614,7 @@ program
|
|
|
368
614
|
.option("-k, --kind <kind>", "filter by marker kind (decision|open)")
|
|
369
615
|
.option("-m, --mode <mode>", "recall mode: keyword, semantic, or hybrid (default: keyword)", "keyword")
|
|
370
616
|
.option("-l, --limit <n>", "max results", (v) => Number.parseInt(v, 10), 10)
|
|
617
|
+
.option("-w, --workstream <idOrLabel>", "filter by workstream (id or label; merge chains resolve)")
|
|
371
618
|
.option("--json", "emit the raw JSON result instead of rendered lines")
|
|
372
619
|
.action(async (query, opts) => {
|
|
373
620
|
const { storage, recall } = await buildStack();
|
|
@@ -381,6 +628,7 @@ program
|
|
|
381
628
|
includeSuperseded: true,
|
|
382
629
|
...(opts.entity ? { entity: opts.entity } : {}),
|
|
383
630
|
...(opts.kind ? { kind: opts.kind } : {}),
|
|
631
|
+
...(opts.workstream ? { workstream: opts.workstream } : {}),
|
|
384
632
|
});
|
|
385
633
|
if (opts.json) {
|
|
386
634
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
@@ -398,6 +646,149 @@ program
|
|
|
398
646
|
await storage.close();
|
|
399
647
|
}
|
|
400
648
|
});
|
|
649
|
+
program
|
|
650
|
+
.command("recall-workstream")
|
|
651
|
+
.description("Recall a workstream's accumulated context (id or label)")
|
|
652
|
+
.argument("<idOrLabel>", "workstream id or label")
|
|
653
|
+
.action(async (idOrLabel) => {
|
|
654
|
+
const { storage, store } = await buildStack();
|
|
655
|
+
try {
|
|
656
|
+
const r = await recallWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel });
|
|
657
|
+
process.stdout.write(r.content[0].text + "\n");
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
await storage.close();
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
program
|
|
664
|
+
.command("rebind-session")
|
|
665
|
+
.description("Rebind a session to a workstream (operator correction)")
|
|
666
|
+
.argument("<sessionId>", "session id")
|
|
667
|
+
.argument("<workstream>", "target workstream id or label")
|
|
668
|
+
.action(async (sessionId, workstream) => {
|
|
669
|
+
const { storage, store } = await buildStack();
|
|
670
|
+
try {
|
|
671
|
+
const r = await rebindSessionHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { sessionId, workstream });
|
|
672
|
+
process.stdout.write(r.content[0].text + "\n");
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
await storage.close();
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
program
|
|
679
|
+
.command("merge-workstreams")
|
|
680
|
+
.description("Merge a duplicate workstream into the one to keep")
|
|
681
|
+
.argument("<from>", "duplicate workstream id or label")
|
|
682
|
+
.argument("<into>", "survivor workstream id or label")
|
|
683
|
+
.action(async (from, into) => {
|
|
684
|
+
const { storage, store } = await buildStack();
|
|
685
|
+
try {
|
|
686
|
+
const r = await mergeWorkstreamsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { from, into });
|
|
687
|
+
process.stdout.write(r.content[0].text + "\n");
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
await storage.close();
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
program
|
|
694
|
+
.command("rename-workstream")
|
|
695
|
+
.description("Rename a workstream")
|
|
696
|
+
.argument("<idOrLabel>", "workstream id or current label")
|
|
697
|
+
.argument("<label>", "new label")
|
|
698
|
+
.action(async (idOrLabel, label) => {
|
|
699
|
+
const { storage, store } = await buildStack();
|
|
700
|
+
try {
|
|
701
|
+
const r = await renameWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel, label });
|
|
702
|
+
process.stdout.write(r.content[0].text + "\n");
|
|
703
|
+
}
|
|
704
|
+
finally {
|
|
705
|
+
await storage.close();
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
program
|
|
709
|
+
.command("retire-workstream")
|
|
710
|
+
.description("Retire (mark dead) a workstream")
|
|
711
|
+
.argument("<idOrLabel>", "workstream id or label")
|
|
712
|
+
.action(async (idOrLabel) => {
|
|
713
|
+
const { storage, store } = await buildStack();
|
|
714
|
+
try {
|
|
715
|
+
const r = await retireWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel });
|
|
716
|
+
process.stdout.write(r.content[0].text + "\n");
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
await storage.close();
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
program
|
|
723
|
+
.command("merge-suggestions")
|
|
724
|
+
.description("List likely-duplicate workstreams to merge")
|
|
725
|
+
.option("-m, --min-score <n>", "minimum similarity score 0..1", "0.5")
|
|
726
|
+
.action(async (opts) => {
|
|
727
|
+
const minScore = Number(opts.minScore);
|
|
728
|
+
if (!Number.isFinite(minScore)) {
|
|
729
|
+
process.stderr.write(`nlm: invalid --min-score "${opts.minScore}" (expected a number 0..1)\n`);
|
|
730
|
+
process.exitCode = 1;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const { storage, store } = await buildStack();
|
|
734
|
+
try {
|
|
735
|
+
const r = await listMergeSuggestionsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { minScore });
|
|
736
|
+
process.stdout.write(r.content[0].text + "\n");
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
await storage.close();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
program
|
|
743
|
+
.command("eval")
|
|
744
|
+
.description("Evaluate recall quality (--queries) or classifier extraction quality (--classifier)")
|
|
745
|
+
.option("--queries <file>", "JSON file: [{ query, expectedIds }]")
|
|
746
|
+
.option("--classifier", "benchmark the configured classifier lane against shipped gold fixtures")
|
|
747
|
+
.option("--limit <n>", "run only the first N fixtures (classifier mode)", (v) => Number.parseInt(v, 10))
|
|
748
|
+
.option("--mode <mode>", "keyword | semantic | hybrid (queries mode)", "keyword")
|
|
749
|
+
.option("--json", "emit JSON instead of a table")
|
|
750
|
+
.action(async (opts) => {
|
|
751
|
+
if (opts.classifier) {
|
|
752
|
+
const stack = await buildStack();
|
|
753
|
+
try {
|
|
754
|
+
const limitVal = opts.limit;
|
|
755
|
+
await runClassifierEvalCommand({
|
|
756
|
+
classify: (t) => stack.classifier.classify(t),
|
|
757
|
+
provider: stack.classifier.provider,
|
|
758
|
+
model: stack.classifier.model,
|
|
759
|
+
fixturesDir: CLASSIFIER_FIXTURES_DIR,
|
|
760
|
+
...(limitVal !== undefined ? { limit: limitVal } : {}),
|
|
761
|
+
json: Boolean(opts.json),
|
|
762
|
+
stdout: (s) => { process.stdout.write(s); },
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
finally {
|
|
766
|
+
await stack.storage.close();
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (!opts.queries) {
|
|
771
|
+
process.stderr.write("error: required option '--queries <file>' not specified\n");
|
|
772
|
+
process.exitCode = 1;
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const { readFile } = await import("node:fs/promises");
|
|
776
|
+
const queries = JSON.parse(await readFile(opts.queries, "utf8"));
|
|
777
|
+
const { runEval } = await import("../core/eval/run-eval.js");
|
|
778
|
+
const { storage, recall } = await buildStack();
|
|
779
|
+
try {
|
|
780
|
+
const report = await runEval({ recall }, queries, { mode: opts.mode, k: 5 });
|
|
781
|
+
if (opts.json) {
|
|
782
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
process.stdout.write(`mode=${report.mode} n=${report.n} R@1=${(report.rAt1 * 100).toFixed(1)}% ` +
|
|
786
|
+
`R@5=${(report.rAt5 * 100).toFixed(1)}% MRR=${report.mrr.toFixed(3)}\n`);
|
|
787
|
+
}
|
|
788
|
+
finally {
|
|
789
|
+
await storage.close();
|
|
790
|
+
}
|
|
791
|
+
});
|
|
401
792
|
program
|
|
402
793
|
.command("recall-code")
|
|
403
794
|
.description("Semantic search over code exemplars (requires NLM_CODE_EXEMPLARS_ENABLED=1)")
|
|
@@ -413,11 +804,10 @@ program
|
|
|
413
804
|
process.exit(1);
|
|
414
805
|
}
|
|
415
806
|
const { storage } = await buildStack();
|
|
416
|
-
const { OllamaCodeEmbedder } = await import("../llm/ollama-code-embedder.js");
|
|
417
807
|
const { recallCode } = await import("../core/exemplars/recall-code.js");
|
|
418
808
|
const { installScope: getScope } = await import("../core/signals/install-scope.js");
|
|
419
809
|
try {
|
|
420
|
-
const embedder =
|
|
810
|
+
const embedder = buildCodeEmbedder();
|
|
421
811
|
const result = await recallCode({
|
|
422
812
|
query,
|
|
423
813
|
installScope: getScope(),
|
|
@@ -450,6 +840,48 @@ program
|
|
|
450
840
|
await storage.close();
|
|
451
841
|
}
|
|
452
842
|
});
|
|
843
|
+
program
|
|
844
|
+
.command("code-signal")
|
|
845
|
+
.description("Emit a PATH-(b) code signal for a commit (deterministic pass/fail exemplar). Best-effort: never blocks a commit.")
|
|
846
|
+
.requiredOption("--repo-path <dir>", "path to the git repo whose commit to capture")
|
|
847
|
+
.requiredOption("--sha <sha>", "commit sha to extract the diff from")
|
|
848
|
+
.requiredOption("--test-exit <n>", "test gate exit code (0 -> pass, non-zero -> fail)", (v) => Number.parseInt(v, 10))
|
|
849
|
+
.option("--task <s>", "task description (defaults to the changed funcname/file)")
|
|
850
|
+
.option("--model <s>", "model that produced the change")
|
|
851
|
+
.option("--repo <logical>", "logical repo name (defaults to the repo-path basename)")
|
|
852
|
+
.option("--dry-run", "print the payload and do not POST")
|
|
853
|
+
.action(async (opts) => {
|
|
854
|
+
const { buildCodeSignalPayload, formatCodeSignalResult } = await import("../core/signals/code-signal.js");
|
|
855
|
+
const payload = buildCodeSignalPayload({
|
|
856
|
+
repoPath: opts.repoPath,
|
|
857
|
+
sha: opts.sha,
|
|
858
|
+
testExit: opts.testExit,
|
|
859
|
+
...(opts.task ? { task: opts.task } : {}),
|
|
860
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
861
|
+
...(opts.repo ? { repo: opts.repo } : {}),
|
|
862
|
+
});
|
|
863
|
+
if (opts.dryRun) {
|
|
864
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const url = `http://localhost:${port()}/api/signal`;
|
|
868
|
+
try {
|
|
869
|
+
const res = await fetch(url, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers: { "content-type": "application/json" },
|
|
872
|
+
body: JSON.stringify(payload),
|
|
873
|
+
});
|
|
874
|
+
if (res.status !== 202) {
|
|
875
|
+
process.stderr.write(`code-signal: daemon returned ${res.status} (not 202); skipping\n`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const accepted = (await res.json());
|
|
879
|
+
process.stdout.write(formatCodeSignalResult(payload.outcome, accepted.id ?? "unknown") + "\n");
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
process.stderr.write(`code-signal: daemon unreachable at ${url}; skipping\n`);
|
|
883
|
+
}
|
|
884
|
+
});
|
|
453
885
|
program
|
|
454
886
|
.command("misses")
|
|
455
887
|
.description("Show sessions the agent explicitly fetched but the hook never surfaced (recall miss log)")
|
|
@@ -532,6 +964,43 @@ program
|
|
|
532
964
|
}
|
|
533
965
|
}
|
|
534
966
|
});
|
|
967
|
+
program
|
|
968
|
+
.command("metrics")
|
|
969
|
+
.description("Read-only retrieval-quality metrics")
|
|
970
|
+
.argument("<name>", "metric name: re-derivation")
|
|
971
|
+
.option("--window <days>", "lookback window in days", (v) => Number.parseInt(v, 10), 90)
|
|
972
|
+
.option("--json", "emit JSON instead of human-readable output")
|
|
973
|
+
.action(async (name, opts) => {
|
|
974
|
+
if (name !== "re-derivation") {
|
|
975
|
+
console.error(`unknown metric '${name}' (available: re-derivation)`);
|
|
976
|
+
process.exitCode = 1;
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const { computeReDerivationRate, sqliteReDerivationDeps } = await import("../core/metrics/re-derivation.js");
|
|
980
|
+
const { storage, store } = await buildStack();
|
|
981
|
+
try {
|
|
982
|
+
if (typeof store.rawDb !== "function") {
|
|
983
|
+
console.error("re-derivation metric requires the SQLite backend");
|
|
984
|
+
process.exitCode = 1;
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const deps = sqliteReDerivationDeps(store.rawDb());
|
|
988
|
+
const report = await computeReDerivationRate(deps, opts.window);
|
|
989
|
+
if (opts.json) {
|
|
990
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const pct = (report.rate * 100).toFixed(1);
|
|
994
|
+
console.log(`re_derivation_rate (last ${opts.window} day(s)): ${pct}% ` +
|
|
995
|
+
`(${report.pairs.length} re-derived pair${report.pairs.length === 1 ? "" : "s"})`);
|
|
996
|
+
for (const p of report.pairs) {
|
|
997
|
+
console.log(` ${p.a} <-> ${p.b} jaccard=${p.jaccard} shared=${p.sharedEntities.join(", ")}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
finally {
|
|
1001
|
+
await storage.close();
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
535
1004
|
program
|
|
536
1005
|
.command("supersede")
|
|
537
1006
|
.description("Retroactively mark a session as superseded by a newer one")
|
|
@@ -572,12 +1041,39 @@ program
|
|
|
572
1041
|
.description("Re-embed every session into session_embedding_chunks (chunk + max-pool)")
|
|
573
1042
|
.option("-l, --limit <n>", "session cap (default: all)", (v) => Number.parseInt(v, 10))
|
|
574
1043
|
.option("--state <path>", "resume state file (default ~/.nlm/embed_reembed.state)")
|
|
1044
|
+
.option("--exemplars", "instead: embed code_exemplars rows missing a vector (repairs dropped capture embeds)")
|
|
1045
|
+
.option("--dry-run", "report dim mismatch and what would be dropped without writing")
|
|
575
1046
|
.option("-v, --verbose", "per-session progress on stderr")
|
|
576
1047
|
.action(async (opts) => {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1048
|
+
if (opts.exemplars) {
|
|
1049
|
+
const { storage } = await buildStack();
|
|
1050
|
+
try {
|
|
1051
|
+
const report = await backfillExemplarEmbeddings({
|
|
1052
|
+
dbPath: dbPath(),
|
|
1053
|
+
embedder: buildCodeEmbedder(),
|
|
1054
|
+
store: storage.exemplars,
|
|
1055
|
+
...(opts.limit ? { limit: opts.limit } : {}),
|
|
1056
|
+
...(opts.verbose
|
|
1057
|
+
? {
|
|
1058
|
+
onProgress: (i, n, id, status) => {
|
|
1059
|
+
process.stderr.write(` [${i}/${n}] ${id} ${status}\n`);
|
|
1060
|
+
},
|
|
1061
|
+
}
|
|
1062
|
+
: {}),
|
|
1063
|
+
});
|
|
1064
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
1065
|
+
}
|
|
1066
|
+
finally {
|
|
1067
|
+
await storage.close();
|
|
1068
|
+
}
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const embedder = buildEmbedder();
|
|
1072
|
+
const pgUrl = process.env["NLM_PG_URL"];
|
|
1073
|
+
const sharedOpts = {
|
|
580
1074
|
embedder,
|
|
1075
|
+
embedderProvider: resolveEmbedderInfo(process.env).provider,
|
|
1076
|
+
...(opts.dryRun ? { dryRun: true } : {}),
|
|
581
1077
|
...(opts.state ? { statePath: opts.state } : {}),
|
|
582
1078
|
...(opts.limit ? { limit: opts.limit } : {}),
|
|
583
1079
|
...(opts.verbose
|
|
@@ -587,7 +1083,10 @@ program
|
|
|
587
1083
|
},
|
|
588
1084
|
}
|
|
589
1085
|
: {}),
|
|
590
|
-
}
|
|
1086
|
+
};
|
|
1087
|
+
const report = pgUrl
|
|
1088
|
+
? await reembedCorpusPg({ pgUrl, ...sharedOpts })
|
|
1089
|
+
: await reembedCorpus({ dbPath: dbPath(), ...sharedOpts });
|
|
591
1090
|
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
592
1091
|
});
|
|
593
1092
|
program
|
|
@@ -649,6 +1148,7 @@ program
|
|
|
649
1148
|
factStore: stack.storage.facts,
|
|
650
1149
|
embedder: stack.embedder,
|
|
651
1150
|
classifier: stack.classifier,
|
|
1151
|
+
classifierDescriptor: { provider: stack.classifier.provider, model: stack.classifier.model },
|
|
652
1152
|
adapters,
|
|
653
1153
|
}, { ...(opts.limit ? { limit: opts.limit } : {}), dryRun: Boolean(opts.dryRun) });
|
|
654
1154
|
console.log(`reclassify-oversized: attempted=${r.attempted} ingested=${r.ingested} ` +
|
|
@@ -660,33 +1160,108 @@ program
|
|
|
660
1160
|
}
|
|
661
1161
|
});
|
|
662
1162
|
program
|
|
663
|
-
.command("
|
|
664
|
-
.description("
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
.option("--
|
|
668
|
-
.
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1163
|
+
.command("reprocess")
|
|
1164
|
+
.description("Retroactive: re-classify sessions that lack classification or were classified by a weaker lane.\n" +
|
|
1165
|
+
"Workstream binding (workstream_id) is NOT changed by this command.\n" +
|
|
1166
|
+
"Below-floor results (<0.4 confidence) update the session row but preserve its prior facts.")
|
|
1167
|
+
.option("-l, --limit <n>", "max sessions to process this run", (v) => Number.parseInt(v, 10))
|
|
1168
|
+
.option("--min-confidence <f>", "also reprocess same-model sessions below this confidence", (v) => Number.parseFloat(v))
|
|
1169
|
+
.option("--only-null", "select only sessions with no classifier stamp yet (safe lane-switch resume: sessions already stamped by a stronger model are left untouched)")
|
|
1170
|
+
.option("--exclude-model <tag>", "exclude sessions stamped by this model tag (repeatable; use once per model to preserve corpus quality when switching classifier lanes)", (v, prev) => [...prev, v], [])
|
|
1171
|
+
.option("--state <path>", "resume state file (default ~/.nlm/reprocess.state)")
|
|
1172
|
+
.option("--dry-run", "print cohort report without writing")
|
|
1173
|
+
.option("-v, --verbose", "per-session progress on stderr")
|
|
1174
|
+
.option("--force-embed", "proceed even when the stored prose embedding lane mismatches the runtime embedder")
|
|
1175
|
+
.action(async (opts) => {
|
|
1176
|
+
const { reprocess: runReprocess } = await import("../core/ingest/reprocess.js");
|
|
1177
|
+
const stack = await buildStack();
|
|
1178
|
+
try {
|
|
1179
|
+
if (!(stack.storage instanceof SqliteStorage)) {
|
|
1180
|
+
console.error("reprocess: only supported with SQLite storage (NLM_PG_URL must not be set)");
|
|
1181
|
+
await stack.storage.close();
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
}
|
|
1184
|
+
const embedderInfo = resolveEmbedderInfo();
|
|
1185
|
+
const report = await runReprocess({
|
|
1186
|
+
db: stack.storage.rawDb(),
|
|
1187
|
+
store: stack.storage.sessions,
|
|
1188
|
+
factStore: stack.storage.facts,
|
|
1189
|
+
embedder: stack.embedder,
|
|
1190
|
+
classifier: stack.classifier,
|
|
1191
|
+
classifierDescriptor: { provider: stack.classifier.provider, model: stack.classifier.model },
|
|
1192
|
+
embeddingConfig: stack.storage.embeddingConfig,
|
|
1193
|
+
embedderDescriptor: { provider: embedderInfo.provider, model: embedderInfo.model },
|
|
1194
|
+
}, {
|
|
1195
|
+
...(opts.limit ? { limit: opts.limit } : {}),
|
|
1196
|
+
...(opts.minConfidence !== undefined ? { minConfidence: opts.minConfidence } : {}),
|
|
1197
|
+
...(opts.onlyNull ? { onlyNull: true } : {}),
|
|
1198
|
+
...(opts.excludeModel && opts.excludeModel.length > 0 ? { excludeModels: opts.excludeModel } : {}),
|
|
1199
|
+
...(opts.state ? { statePath: opts.state } : {}),
|
|
1200
|
+
dryRun: Boolean(opts.dryRun),
|
|
1201
|
+
verbose: Boolean(opts.verbose),
|
|
1202
|
+
forceEmbed: Boolean(opts.forceEmbed),
|
|
1203
|
+
...(opts.verbose
|
|
1204
|
+
? {
|
|
1205
|
+
onProgress: (i, n, sid, status) => {
|
|
1206
|
+
process.stderr.write(` [${i}/${n}] ${sid} ${status}\n`);
|
|
1207
|
+
},
|
|
1208
|
+
}
|
|
1209
|
+
: {}),
|
|
1210
|
+
});
|
|
1211
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
1212
|
+
}
|
|
1213
|
+
finally {
|
|
1214
|
+
await stack.storage.close();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
program
|
|
1218
|
+
.command("backfill-derivables")
|
|
1219
|
+
.description("Retroactive: stamp agent_persona + parent_session_id (#352 phase 2) onto sessions ingested\n" +
|
|
1220
|
+
"before the scheduler started stamping them at classify time. Idempotent (WHERE agent_persona IS NULL).")
|
|
1221
|
+
.option("--dry-run", "report counts without writing")
|
|
1222
|
+
.option("--with-transcript-scan", "also scan each candidate's transcript for primary_model/total_tokens/skill (slower)")
|
|
1223
|
+
.action(async (opts) => {
|
|
1224
|
+
const { backfillDerivables } = await import("../core/ingest/backfill-derivables.js");
|
|
1225
|
+
const stack = await buildStack();
|
|
1226
|
+
try {
|
|
1227
|
+
if (!(stack.storage instanceof SqliteStorage)) {
|
|
1228
|
+
console.error("backfill-derivables: only supported with SQLite storage (NLM_PG_URL must not be set)");
|
|
1229
|
+
await stack.storage.close();
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
const report = await backfillDerivables(stack.storage.rawDb(), {
|
|
1233
|
+
dryRun: Boolean(opts.dryRun),
|
|
1234
|
+
withTranscriptScan: Boolean(opts.withTranscriptScan),
|
|
1235
|
+
});
|
|
1236
|
+
console.log(`backfill-derivables${opts.dryRun ? " (dry-run)" : ""}: ` +
|
|
1237
|
+
`updated=${report.updated} skipped(already-stamped)=${report.skippedAlreadyStamped} ` +
|
|
1238
|
+
`skipped(no-op)=${report.skippedNoop} subagent-candidates=${report.subagentCandidates} ` +
|
|
1239
|
+
`unknown-parent=${report.unknownParent} transcript-scanned=${report.transcriptScanned} total=${report.total}`);
|
|
1240
|
+
}
|
|
1241
|
+
finally {
|
|
1242
|
+
await stack.storage.close();
|
|
1243
|
+
}
|
|
676
1244
|
});
|
|
677
1245
|
program
|
|
678
1246
|
.command("mcp")
|
|
679
1247
|
.description("Run as an MCP stdio server (for ~/.mcp.json)")
|
|
680
1248
|
.action(async () => {
|
|
681
|
-
const { recall, store, facts, factRecall, storage, scope } = await buildStack();
|
|
1249
|
+
const { recall, store, facts, factRecall, storage, scope, signals } = await buildStack();
|
|
682
1250
|
const server = createMcpServer({
|
|
683
1251
|
recall,
|
|
684
1252
|
store,
|
|
685
1253
|
factStore: facts,
|
|
686
1254
|
factRecall,
|
|
687
1255
|
exemplarStore: storage.exemplars,
|
|
688
|
-
codeEmbedder:
|
|
1256
|
+
codeEmbedder: buildCodeEmbedder(),
|
|
689
1257
|
installScope: scope,
|
|
1258
|
+
signalStore: signals,
|
|
1259
|
+
sessionScopeReader: store,
|
|
1260
|
+
workDigest: { store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() },
|
|
1261
|
+
workstreams: { store: storage.workstreams, sessions: store, facts: facts, exemplars: storage.exemplars },
|
|
1262
|
+
// Tier-B outcome rollup (#352 phase 2) only has a SQLite adapter today;
|
|
1263
|
+
// PgStorage deployments skip get_session's `outcome` field until parity lands.
|
|
1264
|
+
...(storage instanceof SqliteStorage ? { outcomeDb: storage.rawDb() } : {}),
|
|
690
1265
|
});
|
|
691
1266
|
const transport = new StdioServerTransport();
|
|
692
1267
|
await server.connect(transport);
|
|
@@ -1074,14 +1649,12 @@ const SESSION_START_HOOK_JS = resolve(__dirname, "../hook/session-start-hook.js"
|
|
|
1074
1649
|
const SESSION_END_HOOK_JS = resolve(__dirname, "../hook/session-end-hook.js");
|
|
1075
1650
|
const STOP_HOOK_JS = resolve(__dirname, "../hook/stop-hook.js");
|
|
1076
1651
|
const PRE_COMPACT_HOOK_JS = resolve(__dirname, "../hook/pre-compact-hook.js");
|
|
1077
|
-
const SUBAGENT_START_HOOK_JS = resolve(__dirname, "../hook/subagent-start-hook.js");
|
|
1078
1652
|
const ALL_HOOKS = [
|
|
1079
1653
|
{ event: "UserPromptSubmit", script: HOOK_JS, label: "recall" },
|
|
1080
1654
|
{ event: "SessionStart", script: SESSION_START_HOOK_JS, label: "session-start" },
|
|
1081
1655
|
{ event: "SessionEnd", script: SESSION_END_HOOK_JS, label: "session-end" },
|
|
1082
1656
|
{ event: "Stop", script: STOP_HOOK_JS, label: "stop" },
|
|
1083
1657
|
{ event: "PreCompact", script: PRE_COMPACT_HOOK_JS, label: "pre-compact" },
|
|
1084
|
-
{ event: "SubagentStart", script: SUBAGENT_START_HOOK_JS, label: "subagent-start" },
|
|
1085
1658
|
];
|
|
1086
1659
|
function claudeSettingsPath() {
|
|
1087
1660
|
return process.env["NLM_CLAUDE_SETTINGS"] ?? join(homedir(), ".claude", "settings.json");
|
|
@@ -1091,7 +1664,7 @@ const hook = program
|
|
|
1091
1664
|
.description("Manage the Claude Code NLM hooks");
|
|
1092
1665
|
hook
|
|
1093
1666
|
.command("install")
|
|
1094
|
-
.description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (live mode)")
|
|
1667
|
+
.description("Add the NLM hooks (recall + session-start + session-end + stop + pre-compact) to ~/.claude/settings.json (live mode)")
|
|
1095
1668
|
.action(() => {
|
|
1096
1669
|
const path = claudeSettingsPath();
|
|
1097
1670
|
const installed = [];
|
|
@@ -1597,7 +2170,7 @@ program
|
|
|
1597
2170
|
program
|
|
1598
2171
|
.command("digest")
|
|
1599
2172
|
.description("Compose a daily-activity digest from the running daemon (optionally post to Telegram)")
|
|
1600
|
-
.option("-p, --port <n>", "daemon port", (v) => Number.parseInt(v, 10), Number.parseInt(process.env["NLM_PORT"] ??
|
|
2173
|
+
.option("-p, --port <n>", "daemon port", (v) => Number.parseInt(v, 10), Number.parseInt(process.env["NLM_PORT"] ?? DEFAULT_NLM_PORT, 10))
|
|
1601
2174
|
.option("--telegram", "post to Telegram instead of printing to stdout (requires TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID)")
|
|
1602
2175
|
.action(async (opts) => {
|
|
1603
2176
|
autoloadEnv();
|
|
@@ -1615,6 +2188,21 @@ program
|
|
|
1615
2188
|
process.exit(1);
|
|
1616
2189
|
}
|
|
1617
2190
|
});
|
|
2191
|
+
program
|
|
2192
|
+
.command("work-digest")
|
|
2193
|
+
.description("Print the operator's agent-assisted work recap for a day")
|
|
2194
|
+
.option("-d, --date <date>", "day to summarize, YYYY-MM-DD (default: today)")
|
|
2195
|
+
.action(async (opts) => {
|
|
2196
|
+
const date = resolveDigestDate(opts.date);
|
|
2197
|
+
const { storage, store } = await buildStack();
|
|
2198
|
+
try {
|
|
2199
|
+
const digest = await buildWorkDigest({ store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() }, date);
|
|
2200
|
+
console.log(composeWorkDigest(digest));
|
|
2201
|
+
}
|
|
2202
|
+
finally {
|
|
2203
|
+
await storage.close();
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
1618
2206
|
async function gatherInstallProbe() {
|
|
1619
2207
|
autoloadEnv();
|
|
1620
2208
|
let reachable = false;
|
|
@@ -1678,7 +2266,7 @@ async function gatherInstallProbe() {
|
|
|
1678
2266
|
program
|
|
1679
2267
|
.command("doctor")
|
|
1680
2268
|
.description("Check database integrity invariants and install/runtime health, optionally repair safe violations")
|
|
1681
|
-
.option("--fix", "repair mechanically safe violations: delete self-loop edges (I1), restore orphaned superseded/replaced sessions to closed (I2)")
|
|
2269
|
+
.option("--fix", "repair mechanically safe violations: delete self-loop edges (I1), restore orphaned superseded/replaced sessions to closed (I2), delete ghost fact embeddings (I7)")
|
|
1682
2270
|
.action(async (opts) => {
|
|
1683
2271
|
const storage = await buildStorage(dbPath());
|
|
1684
2272
|
let violations;
|
|
@@ -1697,7 +2285,10 @@ program
|
|
|
1697
2285
|
if (fixReport.restoredToClosed > 0) {
|
|
1698
2286
|
console.log(` fixed I2: restored ${fixReport.restoredToClosed} session(s) to closed`);
|
|
1699
2287
|
}
|
|
1700
|
-
if (fixReport.
|
|
2288
|
+
if (fixReport.deletedGhostEmbeddings > 0) {
|
|
2289
|
+
console.log(` fixed I7: deleted ${fixReport.deletedGhostEmbeddings} ghost embedding(s)`);
|
|
2290
|
+
}
|
|
2291
|
+
if (fixReport.deletedSelfLoops === 0 && fixReport.restoredToClosed === 0 && fixReport.deletedGhostEmbeddings === 0) {
|
|
1701
2292
|
console.log(" --fix: nothing to repair");
|
|
1702
2293
|
}
|
|
1703
2294
|
}
|
|
@@ -1711,7 +2302,7 @@ program
|
|
|
1711
2302
|
finally {
|
|
1712
2303
|
await storage.close();
|
|
1713
2304
|
}
|
|
1714
|
-
const ALL_CHECKS = ["I1", "I2", "I3", "I4", "I5a", "I5b", "I6"];
|
|
2305
|
+
const ALL_CHECKS = ["I1", "I2", "I3", "I4", "I5a", "I5b", "I6", "I7"];
|
|
1715
2306
|
const byId = new Map(violations.map((v) => [v.id, v]));
|
|
1716
2307
|
let anyFail = false;
|
|
1717
2308
|
console.log("Database integrity:");
|
|
@@ -1726,6 +2317,12 @@ program
|
|
|
1726
2317
|
console.log(`PASS ${id}`);
|
|
1727
2318
|
}
|
|
1728
2319
|
}
|
|
2320
|
+
// I5a-mv is informational: multi-valued predicate groups legitimately exempt
|
|
2321
|
+
// from I5a. Not a violation; printed separately so the count is visible.
|
|
2322
|
+
const i5aMv = byId.get("I5a-mv");
|
|
2323
|
+
if (i5aMv) {
|
|
2324
|
+
console.log(`INFO I5a-mv count=${i5aMv.count} ${i5aMv.description}`);
|
|
2325
|
+
}
|
|
1729
2326
|
console.log("\nInstall & runtime health:");
|
|
1730
2327
|
if (printHealthChecks(evaluateInstallHealth(await gatherInstallProbe())))
|
|
1731
2328
|
anyFail = true;
|
|
@@ -1746,9 +2343,11 @@ function printHealthChecks(checks) {
|
|
|
1746
2343
|
async function dbIntegrityCheck() {
|
|
1747
2344
|
const storage = await buildStorage(dbPath());
|
|
1748
2345
|
try {
|
|
1749
|
-
const
|
|
2346
|
+
const all = storage instanceof PgStorage
|
|
1750
2347
|
? await runChecksOnPg(storage.pgPool())
|
|
1751
2348
|
: runChecksOnSqlite(storage.rawDb());
|
|
2349
|
+
// I5a-mv is informational (exempted multi-valued groups), not a true violation.
|
|
2350
|
+
const violations = all.filter((v) => v.id !== "I5a-mv");
|
|
1752
2351
|
if (violations.length === 0)
|
|
1753
2352
|
return { id: "db-integrity", status: "ok", detail: "all invariants hold" };
|
|
1754
2353
|
return {
|
|
@@ -1800,6 +2399,35 @@ program
|
|
|
1800
2399
|
if (anyFail)
|
|
1801
2400
|
process.exit(1);
|
|
1802
2401
|
});
|
|
2402
|
+
program
|
|
2403
|
+
.command("health")
|
|
2404
|
+
.description("Print corpus monitoring snapshot from the running daemon (null before first 24h run)")
|
|
2405
|
+
.action(async () => {
|
|
2406
|
+
let corpus = null;
|
|
2407
|
+
try {
|
|
2408
|
+
const ctrl = new AbortController();
|
|
2409
|
+
const timer = setTimeout(() => ctrl.abort(), 1_500);
|
|
2410
|
+
const res = await fetch(`http://localhost:${port()}/api/health`, { signal: ctrl.signal });
|
|
2411
|
+
clearTimeout(timer);
|
|
2412
|
+
if (res.ok) {
|
|
2413
|
+
const body = (await res.json());
|
|
2414
|
+
corpus = body.corpus ?? null;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
catch {
|
|
2418
|
+
// daemon not reachable
|
|
2419
|
+
}
|
|
2420
|
+
if (corpus) {
|
|
2421
|
+
console.log(`corpus.state: ${corpus.state}`);
|
|
2422
|
+
console.log(`corpus.dbBytes: ${corpus.dbBytes}`);
|
|
2423
|
+
console.log(`corpus.sessions: ${corpus.sessions}`);
|
|
2424
|
+
console.log(`corpus.entities: ${corpus.entities}`);
|
|
2425
|
+
console.log(`corpus.lastComputedAt: ${corpus.lastComputedAt}`);
|
|
2426
|
+
}
|
|
2427
|
+
else {
|
|
2428
|
+
console.log("corpus: (not yet available; daemon runs the first check ~60s after start)");
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
1803
2431
|
program
|
|
1804
2432
|
.command("backup")
|
|
1805
2433
|
.description("Write a dated snapshot to ~/.nlm/backups/ and prune ones past the retention window")
|
|
@@ -1862,8 +2490,191 @@ program
|
|
|
1862
2490
|
console.log(`Staged ${opts.from} (${validation.sessions} sessions, schema v${validation.schemaVersion}).`);
|
|
1863
2491
|
console.log("Run `nlm restart` to apply — the current DB is archived aside, not deleted.");
|
|
1864
2492
|
});
|
|
1865
|
-
program
|
|
1866
|
-
|
|
1867
|
-
|
|
2493
|
+
program
|
|
2494
|
+
.command("init")
|
|
2495
|
+
.description("Print (or write) the agent recall contract snippet")
|
|
2496
|
+
.requiredOption("--agent <name>", "agent variant: claude-code or generic")
|
|
2497
|
+
.option("--write <path>", "append the contract to this file instead of printing to stdout")
|
|
2498
|
+
.option("--force", "when --write: replace the existing block instead of refusing")
|
|
2499
|
+
.action((opts) => {
|
|
2500
|
+
runInitCommand({
|
|
2501
|
+
agent: opts.agent,
|
|
2502
|
+
write: opts.write,
|
|
2503
|
+
force: Boolean(opts.force),
|
|
2504
|
+
stdout: (s) => { process.stdout.write(s); },
|
|
2505
|
+
stderr: (s) => { process.stderr.write(s); },
|
|
2506
|
+
});
|
|
1868
2507
|
});
|
|
2508
|
+
const entitiesCmd = program.command("entities").description("Entity table operations");
|
|
2509
|
+
entitiesCmd
|
|
2510
|
+
.command("dedup")
|
|
2511
|
+
.description("Suggest (or apply) conservative entity merge pairs. Default: dry run.")
|
|
2512
|
+
.option("--apply-safe", "execute safe-class merges via the merge primitive", false)
|
|
2513
|
+
.option("--interactive", "step through likely-class pairs with y/n on stdin", false)
|
|
2514
|
+
.action(async (opts) => {
|
|
2515
|
+
const storage = await buildStorage(dbPath());
|
|
2516
|
+
let rows;
|
|
2517
|
+
try {
|
|
2518
|
+
if (storage instanceof SqliteStorage) {
|
|
2519
|
+
rows = storage
|
|
2520
|
+
.rawDb()
|
|
2521
|
+
.prepare("SELECT canonical, session_count, status FROM entities ORDER BY canonical")
|
|
2522
|
+
.all();
|
|
2523
|
+
}
|
|
2524
|
+
else {
|
|
2525
|
+
const res = await storage
|
|
2526
|
+
.pgPool()
|
|
2527
|
+
.query("SELECT canonical, session_count, status FROM entities ORDER BY canonical");
|
|
2528
|
+
rows = res.rows;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
catch (err) {
|
|
2532
|
+
await storage.close();
|
|
2533
|
+
console.error("nlm entities dedup: failed to enumerate entities:", err);
|
|
2534
|
+
process.exit(1);
|
|
2535
|
+
}
|
|
2536
|
+
const inputs = rows.map((r) => ({
|
|
2537
|
+
canonical: r.canonical,
|
|
2538
|
+
sessionCount: r.session_count,
|
|
2539
|
+
status: r.status,
|
|
2540
|
+
}));
|
|
2541
|
+
const suggestions = suggestMerges(inputs);
|
|
2542
|
+
const safe = suggestions.filter((s) => s.cls === "safe");
|
|
2543
|
+
const likely = suggestions.filter((s) => s.cls === "likely");
|
|
2544
|
+
let merged = 0;
|
|
2545
|
+
let failed = 0;
|
|
2546
|
+
let skipped = 0;
|
|
2547
|
+
if (!opts.applySafe && !opts.interactive) {
|
|
2548
|
+
console.log(`safe (${safe.length}):`);
|
|
2549
|
+
for (const s of safe)
|
|
2550
|
+
console.log(` ${s.source} -> ${s.target}`);
|
|
2551
|
+
console.log(`likely (${likely.length}):`);
|
|
2552
|
+
for (const s of likely)
|
|
2553
|
+
console.log(` ${s.source} -> ${s.target}`);
|
|
2554
|
+
console.log(`\ntotal: safe=${safe.length} likely=${likely.length} (dry run)`);
|
|
2555
|
+
await storage.close();
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
if (opts.applySafe) {
|
|
2559
|
+
for (const s of safe) {
|
|
2560
|
+
try {
|
|
2561
|
+
await storage.entities.merge(s.source, s.target);
|
|
2562
|
+
console.log(`merged: ${s.source} -> ${s.target}`);
|
|
2563
|
+
merged++;
|
|
2564
|
+
}
|
|
2565
|
+
catch (err) {
|
|
2566
|
+
console.error(`failed: ${s.source} -> ${s.target}: ${err}`);
|
|
2567
|
+
failed++;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
else {
|
|
2572
|
+
for (const s of safe) {
|
|
2573
|
+
console.log(`safe (skipped): ${s.source} -> ${s.target}`);
|
|
2574
|
+
skipped++;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
if (opts.interactive && likely.length > 0) {
|
|
2578
|
+
const { createInterface } = await import("node:readline");
|
|
2579
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2580
|
+
const ask = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
2581
|
+
for (const s of likely) {
|
|
2582
|
+
const answer = await ask(`merge ${s.source} -> ${s.target}? [y/N] `);
|
|
2583
|
+
if (answer.trim().toLowerCase() === "y") {
|
|
2584
|
+
try {
|
|
2585
|
+
await storage.entities.merge(s.source, s.target);
|
|
2586
|
+
console.log(`merged: ${s.source} -> ${s.target}`);
|
|
2587
|
+
merged++;
|
|
2588
|
+
}
|
|
2589
|
+
catch (err) {
|
|
2590
|
+
console.error(`failed: ${s.source} -> ${s.target}: ${err}`);
|
|
2591
|
+
failed++;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
else {
|
|
2595
|
+
skipped++;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
rl.close();
|
|
2599
|
+
}
|
|
2600
|
+
else {
|
|
2601
|
+
skipped += likely.length;
|
|
2602
|
+
}
|
|
2603
|
+
await storage.close();
|
|
2604
|
+
const remaining = suggestions.length - merged - failed;
|
|
2605
|
+
console.log(`\nsummary: merged=${merged} failed=${failed} suggested-remaining=${remaining}`);
|
|
2606
|
+
if (failed > 0)
|
|
2607
|
+
process.exit(1);
|
|
2608
|
+
});
|
|
2609
|
+
const scopeCmd = program
|
|
2610
|
+
.command("scope")
|
|
2611
|
+
.description("Scope management commands");
|
|
2612
|
+
scopeCmd
|
|
2613
|
+
.command("backfill")
|
|
2614
|
+
.description("Derive and stamp scope on legacy sessions/facts/exemplars/signals/workstreams from stored transcript evidence")
|
|
2615
|
+
.option("--apply", "write changes (default: dry run, no writes)")
|
|
2616
|
+
.option("--db <path>", "SQLite DB file to backfill (default: canonical NLM DB)")
|
|
2617
|
+
.option("--json", "emit machine-readable JSON summary instead of human-readable output")
|
|
2618
|
+
.action(async (opts) => {
|
|
2619
|
+
const targetPath = opts.db ?? dbPath();
|
|
2620
|
+
const storage = SqliteStorage.create({ dbPath: targetPath, migrationsDir: MIGRATIONS_DIR });
|
|
2621
|
+
const aliasMap = loadAliasMap();
|
|
2622
|
+
try {
|
|
2623
|
+
const result = await runScopeBackfill({
|
|
2624
|
+
db: storage.rawDb(),
|
|
2625
|
+
apply: Boolean(opts.apply),
|
|
2626
|
+
aliasMap,
|
|
2627
|
+
});
|
|
2628
|
+
if (opts.json) {
|
|
2629
|
+
const out = {
|
|
2630
|
+
dryRun: result.dryRun,
|
|
2631
|
+
sessions: { total: result.sessions.total, byScope: result.sessions.byScope },
|
|
2632
|
+
facts: { total: result.facts.total, byScope: result.facts.byScope },
|
|
2633
|
+
exemplars: { total: result.exemplars.total, byScope: result.exemplars.byScope },
|
|
2634
|
+
signals: { total: result.signals.total, byScope: result.signals.byScope },
|
|
2635
|
+
workstreams: { total: result.workstreams.total, byScope: result.workstreams.byScope },
|
|
2636
|
+
skipped: result.skipped,
|
|
2637
|
+
};
|
|
2638
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
2639
|
+
}
|
|
2640
|
+
else {
|
|
2641
|
+
formatBackfillResult(result, (s) => process.stdout.write(s));
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
finally {
|
|
2645
|
+
await storage.close();
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
scopeCmd
|
|
2649
|
+
.command("coverage")
|
|
2650
|
+
.description("Report per-table and recall-weighted scope coverage fractions")
|
|
2651
|
+
.option("--db <path>", "SQLite DB file (default: canonical NLM DB)")
|
|
2652
|
+
.option("--query-log <path>", "query log path for recall-weighted coverage")
|
|
2653
|
+
.option("--window <n>", "last N log entries for recall-weighted coverage (default 200)", (v) => Number.parseInt(v, 10), 200)
|
|
2654
|
+
.option("--json", "emit machine-readable JSON instead of human-readable output")
|
|
2655
|
+
.action(async (opts) => {
|
|
2656
|
+
const targetPath = opts.db ?? dbPath();
|
|
2657
|
+
const result = await runScopeCoverage({
|
|
2658
|
+
dbPath: targetPath,
|
|
2659
|
+
...(opts.queryLog ? { queryLogPath: opts.queryLog } : {}),
|
|
2660
|
+
window: opts.window,
|
|
2661
|
+
});
|
|
2662
|
+
if (opts.json) {
|
|
2663
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2664
|
+
}
|
|
2665
|
+
else {
|
|
2666
|
+
formatCoverageResult(result, (s) => process.stdout.write(s));
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
// Entrypoint guard: only parse argv when this file IS the executed script.
|
|
2670
|
+
// realpathSync resolves the npm bin symlink so the global `nlm` shim still
|
|
2671
|
+
// matches; without the guard, importing any helper from this module inside
|
|
2672
|
+
// vitest ran the CLI against the test runner's argv and exited the worker.
|
|
2673
|
+
const entryPath = process.argv[1];
|
|
2674
|
+
if (entryPath && import.meta.url === pathToFileURL(realpathSync(entryPath)).href) {
|
|
2675
|
+
program.parseAsync().catch((e) => {
|
|
2676
|
+
console.error("nlm: fatal", e);
|
|
2677
|
+
process.exit(1);
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
1869
2680
|
//# sourceMappingURL=nlm.js.map
|