nlm-memory 0.20.0 → 0.20.1
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 +17 -10
- package/dist/cli/digest.js +21 -0
- 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 +766 -55
- package/dist/cli/nlm.js.map +1 -1
- 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.js +6 -0
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/classifier/prompt.js +3 -3
- package/dist/core/digest/compose.d.ts +10 -0
- package/dist/core/digest/compose.js +7 -1
- package/dist/core/digest/compose.js.map +1 -1
- 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 +1 -0
- package/dist/core/exemplars/capture-from-session.js +6 -2
- 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.js +2 -65
- package/dist/core/exemplars/extract-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/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/ingest-session.d.ts +5 -0
- package/dist/core/ingest/ingest-session.js +3 -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 +3 -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 +275 -0
- package/dist/core/ingest/reprocess.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/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/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 +21 -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 +13 -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 +75 -10
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/signals/code-signal.d.ts +49 -0
- package/dist/core/signals/code-signal.js +53 -0
- package/dist/core/signals/code-signal.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 +7 -0
- 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 +9 -0
- package/dist/core/storage/pg-session-store.js +204 -76
- package/dist/core/storage/pg-session-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 +24 -0
- package/dist/core/storage/pg-workstream-store.js +96 -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 +11 -0
- 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 +76 -25
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +20 -5
- package/dist/core/storage/sqlite-session-store.js +183 -68
- package/dist/core/storage/sqlite-session-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 +24 -0
- package/dist/core/storage/sqlite-workstream-store.js +98 -0
- package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
- 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 +25 -0
- package/dist/core/workstream/bind.js +23 -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 +20 -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 +66 -0
- package/dist/hook/pre-compact-hook.js.map +1 -0
- package/dist/hook/prompt-recall-hook.d.ts +37 -0
- package/dist/hook/prompt-recall-hook.js +120 -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 +1 -1
- package/dist/hook/recall-over-http.js +11 -11
- 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 +24 -59
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +22 -59
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/hook/subagent-start-hook.d.ts +28 -0
- package/dist/hook/subagent-start-hook.js +81 -0
- package/dist/hook/subagent-start-hook.js.map +1 -0
- package/dist/http/app.js +28 -8
- 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 +47 -2
- package/dist/mcp/server.js +303 -18
- 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/session-store.d.ts +12 -0
- 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 +21 -0
- package/dist/ports/workstream-store.js +2 -0
- package/dist/ports/workstream-store.js.map +1 -0
- package/dist/shared/types.d.ts +23 -2
- 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/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/nlm/index.js +395 -37
- package/package.json +12 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/prompt-recall-hook.mjs +402 -41
- package/plugin/scripts/session-start-hook.mjs +286 -60
- package/plugin/scripts/stop-hook.mjs +85 -65
- 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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code PreCompact hook entrypoint for NLM.
|
|
3
|
+
*
|
|
4
|
+
* Fires before Claude Code compacts a long transcript. At compaction time the
|
|
5
|
+
* in-flight surfaced-IDs memo and any pending citation state would be lost.
|
|
6
|
+
* This hook POSTs a compaction event to the daemon so it can do a final
|
|
7
|
+
* synchronous citation scan of the transcript before the shape is lost.
|
|
8
|
+
*
|
|
9
|
+
* Daemon endpoint: POST /api/hook/pre-compact
|
|
10
|
+
*
|
|
11
|
+
* Payload: { conversation_id, transcript_path, surfaced_set, ts }
|
|
12
|
+
*
|
|
13
|
+
* Fail-open by design: any error yields a clean exit with no output.
|
|
14
|
+
*/
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { loadSurfaced } from "../core/hook/memo.js";
|
|
17
|
+
import { autoloadEnv } from "../llm/env-autoload.js";
|
|
18
|
+
import { hookAuthHeaders } from "./hook-auth.js";
|
|
19
|
+
import { readStdin, fetchWithTimeout, appendHookEvent } from "./hook-helpers.js";
|
|
20
|
+
const POST_TIMEOUT_MS = 1500;
|
|
21
|
+
export async function runPreCompact(input, portValue = process.env["NLM_PORT"] ?? "3940") {
|
|
22
|
+
const surfacedSet = [...loadSurfaced(input.conversationId)];
|
|
23
|
+
const payload = {
|
|
24
|
+
conversation_id: input.conversationId,
|
|
25
|
+
transcript_path: input.transcriptPath,
|
|
26
|
+
surfaced_set: surfacedSet,
|
|
27
|
+
ts: new Date().toISOString(),
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetchWithTimeout(`http://127.0.0.1:${portValue}/api/hook/pre-compact`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: hookAuthHeaders({ "content-type": "application/json" }),
|
|
33
|
+
body: JSON.stringify(payload),
|
|
34
|
+
}, POST_TIMEOUT_MS);
|
|
35
|
+
return { conversationId: input.conversationId, posted: res.ok };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { conversationId: input.conversationId, posted: false };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function logResult(result) {
|
|
42
|
+
appendHookEvent({
|
|
43
|
+
ts: new Date().toISOString(),
|
|
44
|
+
kind: "pre-compact",
|
|
45
|
+
conversationId: result.conversationId,
|
|
46
|
+
posted: result.posted,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
try {
|
|
51
|
+
autoloadEnv();
|
|
52
|
+
const raw = await readStdin();
|
|
53
|
+
const payload = JSON.parse(raw);
|
|
54
|
+
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
55
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
56
|
+
const result = await runPreCompact({ conversationId, transcriptPath });
|
|
57
|
+
logResult(result);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fail open — never block Claude Code compaction.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
64
|
+
void main();
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=pre-compact-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-compact-hook.js","sourceRoot":"","sources":["../../src/hook/pre-compact-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEjF,MAAM,eAAe,GAAG,IAAI,CAAC;AAY7B,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAsB,EACtB,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,MAAM;IAE7C,MAAM,WAAW,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG;QACd,eAAe,EAAE,KAAK,CAAC,cAAc;QACrC,eAAe,EAAE,KAAK,CAAC,cAAc;QACrC,YAAY,EAAE,WAAW;QACzB,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KAC7B,CAAC;IACF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAChC,oBAAoB,SAAS,uBAAuB,EACpD;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,eAAe,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;YAChE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;SAC9B,EACD,eAAe,CAChB,CAAC;QACF,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACjE,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,MAAwB;IACzC,eAAe,CAAC;QACd,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC5B,IAAI,EAAE,aAAa;QACnB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QACH,WAAW,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,MAAM,SAAS,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAG7B,CAAC;QACF,MAAM,cAAc,GAClB,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1E,MAAM,cAAc,GAClB,OAAO,OAAO,CAAC,eAAe,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC,CAAC;QACvE,SAAS,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/E,KAAK,IAAI,EAAE,CAAC;AACd,CAAC"}
|
|
@@ -11,17 +11,50 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type PointerExemplar, type PointerFact } from "../core/hook/pointer-block.js";
|
|
13
13
|
import { type RecallHitInput } from "../core/hook/select.js";
|
|
14
|
+
export declare function parseHookDeadline(raw: string | undefined): number;
|
|
14
15
|
export type HookMode = "shadow" | "live";
|
|
15
16
|
export declare function hookRuntimeFromEnv(env?: NodeJS.ProcessEnv): string;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the per-prompt ambient recall hook should run at all.
|
|
19
|
+
*
|
|
20
|
+
* Pull-first posture (default since #392, 2026-07-03): fresh installs run
|
|
21
|
+
* per-prompt ambient recall OFF and agents pull memory on demand via the
|
|
22
|
+
* recall MCP tools. Measured basis (U1, locked judge): pulls 72.4% useful
|
|
23
|
+
* vs ambient injection 18.2%/7.9%. NLM_HOOK_PROMPT_RECALL=on opts back in;
|
|
24
|
+
* any other already-set value keeps its pre-flip meaning (non-"off" = on)
|
|
25
|
+
* so existing installs are untouched. Independent of NLM_HOOK_MODE, which
|
|
26
|
+
* governs the once-per-session passive layer (session-start hook), which
|
|
27
|
+
* stays on.
|
|
28
|
+
*/
|
|
29
|
+
export declare function promptRecallEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
16
30
|
export interface HookInput {
|
|
17
31
|
readonly prompt: string;
|
|
18
32
|
readonly conversationId: string;
|
|
33
|
+
/** Runtime transcript path, when the runtime exposes one (Claude Code does). */
|
|
34
|
+
readonly transcriptPath?: string;
|
|
19
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The recall query. Default is the bare prompt (today's behavior). When the
|
|
38
|
+
* context-recall flag is on AND the prompt is thin (few content words — the
|
|
39
|
+
* measured off-topic failure band) AND a transcript is available, prepend the
|
|
40
|
+
* recent conversation turns so the query carries the topic the thin prompt
|
|
41
|
+
* lacks. Flag-gated + thin-only + fallback => never worse than today.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildRecallQuery(input: HookInput, env?: NodeJS.ProcessEnv): string;
|
|
20
44
|
export interface RecallFetchResult {
|
|
21
45
|
readonly hits: ReadonlyArray<RecallHitInput>;
|
|
22
46
|
readonly facts: ReadonlyArray<PointerFact>;
|
|
23
47
|
readonly exemplars?: ReadonlyArray<PointerExemplar>;
|
|
24
48
|
}
|
|
49
|
+
export type GateMode = "shadow" | "live";
|
|
50
|
+
export interface RecallGate {
|
|
51
|
+
/** shadow = log decisions, inject as usual; live = drop irrelevant candidates. */
|
|
52
|
+
readonly mode: GateMode;
|
|
53
|
+
/** Predict, from the prompt and a candidate's context, whether to inject it. */
|
|
54
|
+
readonly judge: (prompt: string, candidate: string) => Promise<"relevant" | "irrelevant">;
|
|
55
|
+
/** Judge only the top N candidates (bounds hot-path latency). Unset = all. */
|
|
56
|
+
readonly maxCandidates?: number;
|
|
57
|
+
}
|
|
25
58
|
export interface RunHookDeps {
|
|
26
59
|
readonly mode: HookMode;
|
|
27
60
|
/**
|
|
@@ -29,6 +62,10 @@ export interface RunHookDeps {
|
|
|
29
62
|
* bare hit array; runHook normalizes both shapes.
|
|
30
63
|
*/
|
|
31
64
|
readonly recall: (prompt: string) => Promise<ReadonlyArray<RecallHitInput> | RecallFetchResult>;
|
|
65
|
+
/** Optional pre-injection relevance gate. Absent => no gating (today's behavior). */
|
|
66
|
+
readonly recallGate?: RecallGate;
|
|
67
|
+
/** Wall-clock deadline for the combined recall + gate stages. Defaults to NLM_HOOK_DEADLINE_MS (4000ms). */
|
|
68
|
+
readonly deadlineMs?: number;
|
|
32
69
|
}
|
|
33
70
|
/** Orchestration. Returns the text to emit on stdout ("" for nothing). */
|
|
34
71
|
export declare function runHook(input: HookInput, deps: RunHookDeps): Promise<string>;
|
|
@@ -11,24 +11,92 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
13
|
import { classifyPrompt } from "../core/hook/gate.js";
|
|
14
|
+
import { recentConversationContext, topicalWordCount } from "./recent-context.js";
|
|
14
15
|
import { appendHookLog } from "../core/hook/hook-log.js";
|
|
15
16
|
import { loadSurfaced, recordSurfaced } from "../core/hook/memo.js";
|
|
16
17
|
import { formatPointerBlock } from "../core/hook/pointer-block.js";
|
|
17
18
|
import { selectHits } from "../core/hook/select.js";
|
|
18
19
|
import { autoloadEnv } from "../llm/env-autoload.js";
|
|
19
20
|
import { recallOverHttp } from "./recall-over-http.js";
|
|
21
|
+
import { parseScoreFloor, parseRelativeFloor } from "./score-floor.js";
|
|
22
|
+
import { makeOllamaGate, parseRecallGateMode } from "./recall-gate.js";
|
|
23
|
+
import { readStdin, hookModeFromEnv } from "./hook-helpers.js";
|
|
20
24
|
// Keyword recall returns raw BM25 scores (unbounded, not the 0..1 hybrid
|
|
21
25
|
// scale). FTS5 MATCH already gates relevance — only lexically-matching
|
|
22
|
-
// sessions come back — so the floor
|
|
23
|
-
//
|
|
24
|
-
|
|
26
|
+
// sessions come back — so the default floor is 0. NLM_RECALL_SCORE_FLOOR lets
|
|
27
|
+
// an operator raise it once the shadow log's surfaced-vs-cited score
|
|
28
|
+
// distribution (nlm precision --verbose) justifies a real cutoff.
|
|
29
|
+
// parseScoreFloor guards against a bad env value: a non-numeric / non-finite /
|
|
30
|
+
// negative input falls back to 0 instead of silently deny-all'ing recall
|
|
31
|
+
// (matchScore >= NaN is always false in select.ts).
|
|
32
|
+
const SCORE_THRESHOLD = parseScoreFloor(process.env["NLM_RECALL_SCORE_FLOOR"]);
|
|
33
|
+
// Portable per-fire noise floor: drop tail hits below this fraction of the
|
|
34
|
+
// fire's median score. Default 0.9 (calibrated, #284) — trims weak tail recalls
|
|
35
|
+
// while keeping ~97% of cited ones. Only the per-message keyword path runs this;
|
|
36
|
+
// the session-start hybrid path is left unfiltered (unmeasured).
|
|
37
|
+
const RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
|
|
25
38
|
const PER_FIRE_CAP = 3;
|
|
26
39
|
const PER_CONVERSATION_CAP = 10;
|
|
27
40
|
const PROMPT_PREVIEW_CHARS = 200;
|
|
41
|
+
export function parseHookDeadline(raw) {
|
|
42
|
+
if (raw === undefined)
|
|
43
|
+
return 4000;
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
return Number.isFinite(n) && n > 0 ? n : 4000;
|
|
46
|
+
}
|
|
47
|
+
const HOOK_DEADLINE_MS = parseHookDeadline(process.env["NLM_HOOK_DEADLINE_MS"]);
|
|
48
|
+
async function withDeadline(p, ms, fallback) {
|
|
49
|
+
if (ms <= 0)
|
|
50
|
+
return fallback;
|
|
51
|
+
let timer;
|
|
52
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(fallback), ms); });
|
|
53
|
+
try {
|
|
54
|
+
return await Promise.race([p, timeout]);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
28
60
|
export function hookRuntimeFromEnv(env = process.env) {
|
|
29
61
|
const raw = env["NLM_HOOK_RUNTIME"]?.trim();
|
|
30
62
|
return raw ? raw : "claude-code";
|
|
31
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether the per-prompt ambient recall hook should run at all.
|
|
66
|
+
*
|
|
67
|
+
* Pull-first posture (default since #392, 2026-07-03): fresh installs run
|
|
68
|
+
* per-prompt ambient recall OFF and agents pull memory on demand via the
|
|
69
|
+
* recall MCP tools. Measured basis (U1, locked judge): pulls 72.4% useful
|
|
70
|
+
* vs ambient injection 18.2%/7.9%. NLM_HOOK_PROMPT_RECALL=on opts back in;
|
|
71
|
+
* any other already-set value keeps its pre-flip meaning (non-"off" = on)
|
|
72
|
+
* so existing installs are untouched. Independent of NLM_HOOK_MODE, which
|
|
73
|
+
* governs the once-per-session passive layer (session-start hook), which
|
|
74
|
+
* stays on.
|
|
75
|
+
*/
|
|
76
|
+
export function promptRecallEnabled(env = process.env) {
|
|
77
|
+
const raw = env["NLM_HOOK_PROMPT_RECALL"]?.trim();
|
|
78
|
+
if (raw === undefined || raw === "")
|
|
79
|
+
return false;
|
|
80
|
+
return raw.toLowerCase() !== "off";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The recall query. Default is the bare prompt (today's behavior). When the
|
|
84
|
+
* context-recall flag is on AND the prompt is thin (few content words — the
|
|
85
|
+
* measured off-topic failure band) AND a transcript is available, prepend the
|
|
86
|
+
* recent conversation turns so the query carries the topic the thin prompt
|
|
87
|
+
* lacks. Flag-gated + thin-only + fallback => never worse than today.
|
|
88
|
+
*/
|
|
89
|
+
export function buildRecallQuery(input, env = process.env) {
|
|
90
|
+
if (env["NLM_HOOK_CONTEXT_RECALL"] !== "1")
|
|
91
|
+
return input.prompt;
|
|
92
|
+
if (!input.transcriptPath)
|
|
93
|
+
return input.prompt;
|
|
94
|
+
const minWords = Number.parseInt(env["NLM_HOOK_CONTEXT_MIN_WORDS"] ?? "3", 10);
|
|
95
|
+
if (topicalWordCount(input.prompt) >= minWords)
|
|
96
|
+
return input.prompt;
|
|
97
|
+
const context = recentConversationContext(input.transcriptPath);
|
|
98
|
+
return context ? `${context} ${input.prompt}` : input.prompt;
|
|
99
|
+
}
|
|
32
100
|
function normalizeRecall(raw) {
|
|
33
101
|
if (Array.isArray(raw))
|
|
34
102
|
return { hits: raw, facts: [] };
|
|
@@ -38,7 +106,7 @@ function normalizeRecall(raw) {
|
|
|
38
106
|
export async function runHook(input, deps) {
|
|
39
107
|
const gate = classifyPrompt(input.prompt);
|
|
40
108
|
const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
|
|
41
|
-
if (gate === "generative") {
|
|
109
|
+
if (gate === "generative" || gate === "skip") {
|
|
42
110
|
appendHookLog({
|
|
43
111
|
ts: new Date().toISOString(),
|
|
44
112
|
conversationId: input.conversationId,
|
|
@@ -51,9 +119,10 @@ export async function runHook(input, deps) {
|
|
|
51
119
|
});
|
|
52
120
|
return "";
|
|
53
121
|
}
|
|
122
|
+
const deadline = Date.now() + (deps.deadlineMs ?? HOOK_DEADLINE_MS);
|
|
54
123
|
let fetched = { hits: [], facts: [] };
|
|
55
124
|
try {
|
|
56
|
-
fetched = normalizeRecall(await deps.recall(input.
|
|
125
|
+
fetched = normalizeRecall(await withDeadline(deps.recall(buildRecallQuery(input)), deadline - Date.now(), { hits: [], facts: [] }));
|
|
57
126
|
}
|
|
58
127
|
catch {
|
|
59
128
|
fetched = { hits: [], facts: [] };
|
|
@@ -64,10 +133,33 @@ export async function runHook(input, deps) {
|
|
|
64
133
|
hits,
|
|
65
134
|
surfaced,
|
|
66
135
|
scoreThreshold: SCORE_THRESHOLD,
|
|
136
|
+
relativeFloor: RELATIVE_FLOOR,
|
|
67
137
|
perFireCap: PER_FIRE_CAP,
|
|
68
138
|
perConversationCap: PER_CONVERSATION_CAP,
|
|
69
139
|
});
|
|
70
|
-
|
|
140
|
+
// Relevance gate: judge each selected candidate before injecting. In shadow
|
|
141
|
+
// it only records decisions; in live it drops the irrelevant ones. Gate the
|
|
142
|
+
// already-capped `selected` set (<=PER_FIRE_CAP), not the full hit list, to
|
|
143
|
+
// bound hot-path latency.
|
|
144
|
+
let gateDecisions;
|
|
145
|
+
let injected = selected;
|
|
146
|
+
if (deps.recallGate && selected.length > 0) {
|
|
147
|
+
const g = deps.recallGate;
|
|
148
|
+
const toGate = g.maxCandidates ? selected.slice(0, g.maxCandidates) : selected;
|
|
149
|
+
const remaining = deadline - Date.now();
|
|
150
|
+
const gateFallback = toGate.map((h) => ({ id: h.id, gate: "relevant" }));
|
|
151
|
+
if (remaining <= 0) {
|
|
152
|
+
gateDecisions = gateFallback;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
gateDecisions = await withDeadline(Promise.all(toGate.map(async (h) => ({ id: h.id, gate: await g.judge(input.prompt, `${h.label}\n${h.summary ?? ""}`) }))), remaining, gateFallback);
|
|
156
|
+
}
|
|
157
|
+
if (g.mode === "live") {
|
|
158
|
+
const drop = new Set(gateDecisions.filter((d) => d.gate === "irrelevant").map((d) => d.id));
|
|
159
|
+
injected = selected.filter((h) => !drop.has(h.id));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const block = formatPointerBlock(injected, fetched.facts, fetched.exemplars);
|
|
71
163
|
const estTokens = Math.ceil(block.length / 4);
|
|
72
164
|
appendHookLog({
|
|
73
165
|
ts: new Date().toISOString(),
|
|
@@ -75,25 +167,17 @@ export async function runHook(input, deps) {
|
|
|
75
167
|
promptPreview: preview,
|
|
76
168
|
gate,
|
|
77
169
|
hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
|
|
78
|
-
wouldInject:
|
|
170
|
+
wouldInject: injected.map((h) => h.id),
|
|
79
171
|
estTokens,
|
|
80
172
|
mode: deps.mode,
|
|
173
|
+
...(gateDecisions ? { gateDecisions } : {}),
|
|
81
174
|
});
|
|
82
|
-
if (deps.mode === "live" &&
|
|
83
|
-
recordSurfaced(input.conversationId,
|
|
175
|
+
if (deps.mode === "live" && injected.length > 0) {
|
|
176
|
+
recordSurfaced(input.conversationId, injected.map((h) => h.id));
|
|
84
177
|
return block;
|
|
85
178
|
}
|
|
86
179
|
return "";
|
|
87
180
|
}
|
|
88
|
-
function readStdin() {
|
|
89
|
-
return new Promise((resolve) => {
|
|
90
|
-
let data = "";
|
|
91
|
-
process.stdin.setEncoding("utf8");
|
|
92
|
-
process.stdin.on("data", (chunk) => (data += chunk));
|
|
93
|
-
process.stdin.on("end", () => resolve(data));
|
|
94
|
-
process.stdin.on("error", () => resolve(data));
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
181
|
async function main() {
|
|
98
182
|
try {
|
|
99
183
|
// Load ~/.nlm/.env so NLM_MCP_TOKEN is available before we hit /api/recall.
|
|
@@ -104,11 +188,27 @@ async function main() {
|
|
|
104
188
|
const payload = JSON.parse(raw);
|
|
105
189
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
106
190
|
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
191
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : undefined;
|
|
107
192
|
if (!prompt)
|
|
108
193
|
return;
|
|
109
|
-
|
|
194
|
+
// Option B (push -> pull): when per-prompt ambient recall is disabled, do
|
|
195
|
+
// zero hot-path work — no recall call, no gate, no injection. Memory is
|
|
196
|
+
// pulled on demand instead. The once-per-session passive layer
|
|
197
|
+
// (session-start hook) is unaffected.
|
|
198
|
+
if (!promptRecallEnabled())
|
|
199
|
+
return;
|
|
200
|
+
const mode = hookModeFromEnv();
|
|
110
201
|
const runtime = hookRuntimeFromEnv();
|
|
111
|
-
const
|
|
202
|
+
const gateMode = parseRecallGateMode();
|
|
203
|
+
const gateUrl = process.env["OLLAMA_URL"] ?? "http://127.0.0.1:11434";
|
|
204
|
+
// Cap how many candidates the gate judges to bound hot-path latency
|
|
205
|
+
// (~1 judge call/fire by default). Override with NLM_HOOK_RECALL_GATE_TOPN.
|
|
206
|
+
const gateTopN = Math.max(1, Number.parseInt(process.env["NLM_HOOK_RECALL_GATE_TOPN"] ?? "1", 10) || 1);
|
|
207
|
+
const out = await runHook({ prompt, conversationId, ...(transcriptPath ? { transcriptPath } : {}) }, {
|
|
208
|
+
mode,
|
|
209
|
+
recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? undefined : conversationId),
|
|
210
|
+
...(gateMode ? { recallGate: { mode: gateMode, judge: makeOllamaGate(gateUrl), maxCandidates: gateTopN } } : {}),
|
|
211
|
+
});
|
|
112
212
|
if (out)
|
|
113
213
|
process.stdout.write(out);
|
|
114
214
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-recall-hook.js","sourceRoot":"","sources":["../../src/hook/prompt-recall-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAA0C,MAAM,6BAA6B,CAAC;AACzG,OAAO,EAAE,UAAU,EAAuB,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"prompt-recall-hook.js","sourceRoot":"","sources":["../../src/hook/prompt-recall-hook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-injection relevance gate (recall-precision option A).
|
|
3
|
+
*
|
|
4
|
+
* A small local model predicts, from the prompt + a candidate's context alone
|
|
5
|
+
* (no agent response yet), whether injecting the candidate would help. The
|
|
6
|
+
* prompt is deliberately CONSERVATIVE: drop only the clearly cross-topic
|
|
7
|
+
* candidates, keep anything with a plausible connection. Measured on the
|
|
8
|
+
* frontier gold (gate-feasibility.ts): keeps 96% of informed injections while
|
|
9
|
+
* skipping 25% of off-topic. The asymmetry is intentional — dropping a useful
|
|
10
|
+
* memory forfeits recall's signature value, so the gate fails toward "relevant"
|
|
11
|
+
* (here and on any judge error).
|
|
12
|
+
*
|
|
13
|
+
* Runtime home so the hot-path hook has no eval-tooling dependency; the eval
|
|
14
|
+
* harness imports RECALL_GATE_SYSTEM from here so validated == shipped.
|
|
15
|
+
*/
|
|
16
|
+
import type { GateMode } from "./prompt-recall-hook.js";
|
|
17
|
+
export declare const RECALL_GATE_MODEL = "qwen3.5:4b";
|
|
18
|
+
export declare const RECALL_GATE_SYSTEM: string;
|
|
19
|
+
/** Read the gate mode from env. Absent / "off" / unknown => undefined (gate off). */
|
|
20
|
+
export declare function parseRecallGateMode(env?: NodeJS.ProcessEnv): GateMode | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A gate judge backed by an Ollama-compatible local endpoint. Fails toward
|
|
23
|
+
* "relevant" on any error so the gate can never drop a candidate due to an
|
|
24
|
+
* infra blip — consistent with the hook's fail-open contract.
|
|
25
|
+
*/
|
|
26
|
+
/** Bound the gate's hot-path cost. A cold model load can take tens of seconds;
|
|
27
|
+
* past this the gate gives up and keeps the candidate rather than block the prompt. */
|
|
28
|
+
export declare const GATE_TIMEOUT_MS = 4000;
|
|
29
|
+
export declare function makeOllamaGate(url: string, model?: string, timeoutMs?: number): (prompt: string, candidate: string) => Promise<"relevant" | "irrelevant">;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fetchWithTimeout } from "./hook-helpers.js";
|
|
2
|
+
export const RECALL_GATE_MODEL = "qwen3.5:4b";
|
|
3
|
+
export const RECALL_GATE_SYSTEM = "You are a recall GATE protecting against off-topic memory injection. Given a USER PROMPT and a CANDIDATE prior-session context, answer irrelevant ONLY when the candidate is CLEARLY about a completely different topic, project, or task than the prompt (e.g. the prompt is about debugging a website and the candidate is about a trading pipeline). If there is ANY plausible topical connection, or you are at all unsure, answer relevant. Dropping a useful memory is worse than keeping a marginal one. You do NOT see the assistant's answer. " +
|
|
4
|
+
'Output {"gate":"relevant"|"irrelevant"}.';
|
|
5
|
+
const GATE_FORMAT = { type: "object", properties: { gate: { type: "string", enum: ["relevant", "irrelevant"] } }, required: ["gate"] };
|
|
6
|
+
const GATE_OPTS = { temperature: 0, top_p: 1, top_k: 0, presence_penalty: 0, frequency_penalty: 0 };
|
|
7
|
+
/** Read the gate mode from env. Absent / "off" / unknown => undefined (gate off). */
|
|
8
|
+
export function parseRecallGateMode(env = process.env) {
|
|
9
|
+
const v = env["NLM_HOOK_RECALL_GATE"]?.trim();
|
|
10
|
+
return v === "shadow" || v === "live" ? v : undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A gate judge backed by an Ollama-compatible local endpoint. Fails toward
|
|
14
|
+
* "relevant" on any error so the gate can never drop a candidate due to an
|
|
15
|
+
* infra blip — consistent with the hook's fail-open contract.
|
|
16
|
+
*/
|
|
17
|
+
/** Bound the gate's hot-path cost. A cold model load can take tens of seconds;
|
|
18
|
+
* past this the gate gives up and keeps the candidate rather than block the prompt. */
|
|
19
|
+
export const GATE_TIMEOUT_MS = 4000;
|
|
20
|
+
export function makeOllamaGate(url, model = RECALL_GATE_MODEL, timeoutMs = GATE_TIMEOUT_MS) {
|
|
21
|
+
return async (prompt, candidate) => {
|
|
22
|
+
try {
|
|
23
|
+
const r = await fetchWithTimeout(`${url}/api/chat`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
model,
|
|
28
|
+
stream: false,
|
|
29
|
+
think: false,
|
|
30
|
+
// Keep the gate model resident between fires so only the first fire
|
|
31
|
+
// pays the cold-load; subsequent gates are ~1 judge call.
|
|
32
|
+
keep_alive: "15m",
|
|
33
|
+
format: GATE_FORMAT,
|
|
34
|
+
options: GATE_OPTS,
|
|
35
|
+
messages: [
|
|
36
|
+
{ role: "system", content: RECALL_GATE_SYSTEM },
|
|
37
|
+
{ role: "user", content: `USER PROMPT:\n${prompt}\n\nCANDIDATE CONTEXT:\n${candidate}` },
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
const d = (await r.json());
|
|
42
|
+
const v = JSON.parse(d.message?.content ?? "{}").gate;
|
|
43
|
+
return v === "irrelevant" ? "irrelevant" : "relevant";
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return "relevant";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=recall-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recall-gate.js","sourceRoot":"","sources":["../../src/hook/recall-gate.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAC;AAE9C,MAAM,CAAC,MAAM,kBAAkB,GAC7B,yhBAAyhB;IACzhB,0CAA0C,CAAC;AAE7C,MAAM,WAAW,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;AACvI,MAAM,SAAS,GAAG,EAAE,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;AAEpG,qFAAqF;AACrF,MAAM,UAAU,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,MAAM,CAAC,GAAG,GAAG,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,CAAC;IAC9C,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH;uFACuF;AACvF,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAEpC,MAAM,UAAU,cAAc,CAC5B,GAAW,EACX,QAAgB,iBAAiB,EACjC,YAAoB,eAAe;IAEnC,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;QACjC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,gBAAgB,CAC9B,GAAG,GAAG,WAAW,EACjB;gBACE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK;oBACL,MAAM,EAAE,KAAK;oBACb,KAAK,EAAE,KAAK;oBACZ,oEAAoE;oBACpE,0DAA0D;oBAC1D,UAAU,EAAE,KAAK;oBACjB,MAAM,EAAE,WAAW;oBACnB,OAAO,EAAE,SAAS;oBAClB,QAAQ,EAAE;wBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,kBAAkB,EAAE;wBAC/C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,MAAM,2BAA2B,SAAS,EAAE,EAAE;qBACzF;iBACF,CAAC;aACH,EACD,SAAS,CACV,CAAC;YACF,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAuC,CAAC;YACjE,MAAM,CAAC,GAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,IAAI,IAAI,CAAuB,CAAC,IAAI,CAAC;YAC7E,OAAO,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,UAAU,CAAC;QACpB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -21,4 +21,4 @@ export interface RecallOverHttpResult {
|
|
|
21
21
|
readonly facts: ReadonlyArray<PointerFact>;
|
|
22
22
|
readonly exemplars: ReadonlyArray<PointerExemplar>;
|
|
23
23
|
}
|
|
24
|
-
export declare function recallOverHttp(prompt: string, runtime?: string, conversationId?: string): Promise<RecallOverHttpResult>;
|
|
24
|
+
export declare function recallOverHttp(prompt: string, runtime?: string, conversationId?: string, mode?: "keyword" | "hybrid"): Promise<RecallOverHttpResult>;
|
|
@@ -14,26 +14,26 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { hookAuthHeaders } from "./hook-auth.js";
|
|
16
16
|
import { extractRecallQuery } from "../core/hook/query-extract.js";
|
|
17
|
+
import { fetchWithTimeout } from "./hook-helpers.js";
|
|
17
18
|
export const RECALL_LIMIT = 5;
|
|
18
19
|
export const RECALL_TIMEOUT_MS = 2000;
|
|
19
|
-
export async function recallOverHttp(prompt, runtime, conversationId) {
|
|
20
|
+
export async function recallOverHttp(prompt, runtime, conversationId, mode = "keyword") {
|
|
20
21
|
const query = extractRecallQuery(prompt);
|
|
21
22
|
if (query === null)
|
|
22
23
|
return { hits: [], facts: [], exemplars: [] };
|
|
23
24
|
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
24
|
-
const url =
|
|
25
|
-
|
|
25
|
+
const url =
|
|
26
|
+
// 127.0.0.1, not localhost: each hook is a fresh process with no connection
|
|
27
|
+
// reuse, and Node resolves localhost to IPv6 ::1 first — a measured ~50-300ms
|
|
28
|
+
// per-fire connect penalty vs ~3ms on the explicit IPv4 loopback.
|
|
29
|
+
`http://127.0.0.1:${portValue}/api/recall` +
|
|
30
|
+
`?q=${encodeURIComponent(query)}&mode=${mode}&limit=${RECALL_LIMIT}&withFacts=true&withExemplars=true` +
|
|
26
31
|
(conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "");
|
|
27
|
-
const controller = new AbortController();
|
|
28
|
-
const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
|
|
29
32
|
try {
|
|
30
33
|
const extra = { "x-recall-source": "hook" };
|
|
31
34
|
if (runtime)
|
|
32
35
|
extra["x-recall-runtime"] = runtime;
|
|
33
|
-
const res = await
|
|
34
|
-
headers: hookAuthHeaders(extra),
|
|
35
|
-
signal: controller.signal,
|
|
36
|
-
});
|
|
36
|
+
const res = await fetchWithTimeout(url, { headers: hookAuthHeaders(extra) }, RECALL_TIMEOUT_MS);
|
|
37
37
|
if (!res.ok)
|
|
38
38
|
return { hits: [], facts: [], exemplars: [] };
|
|
39
39
|
let body;
|
|
@@ -64,8 +64,8 @@ export async function recallOverHttp(prompt, runtime, conversationId) {
|
|
|
64
64
|
}));
|
|
65
65
|
return { hits, facts, exemplars };
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
catch {
|
|
68
|
+
return { hits: [], facts: [], exemplars: [] };
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
//# sourceMappingURL=recall-over-http.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recall-over-http.js","sourceRoot":"","sources":["../../src/hook/recall-over-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"recall-over-http.js","sourceRoot":"","sources":["../../src/hook/recall-over-http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recent-conversation context for the recall hook.
|
|
3
|
+
*
|
|
4
|
+
* Thin prompts ("what do you recommend?", "do it") carry no topic, so recalling
|
|
5
|
+
* on the bare prompt surfaces off-topic sessions (measured: 82% off-topic). When
|
|
6
|
+
* the prompt is thin we prepend the last few conversation turns — that is where
|
|
7
|
+
* the topic actually lives — to the recall query.
|
|
8
|
+
*
|
|
9
|
+
* Hot-path safe: TAIL-reads at most `maxBytes` of the transcript (not the whole
|
|
10
|
+
* file, which can be MBs in a long session), and only thin prompts call it.
|
|
11
|
+
* Fail-open: any I/O or parse error yields "" so the hook never breaks and the
|
|
12
|
+
* caller falls back to bare-prompt recall (today's behavior).
|
|
13
|
+
*
|
|
14
|
+
* Runtime-specific: only runtimes that expose a transcript path can use this
|
|
15
|
+
* (Claude Code does). Others pass no path and get "" → bare-prompt fallback.
|
|
16
|
+
*/
|
|
17
|
+
interface Opts {
|
|
18
|
+
readonly maxTurns?: number;
|
|
19
|
+
readonly maxBytes?: number;
|
|
20
|
+
readonly perTurnChars?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Count topical (non-stopword) words — the thinness signal for context-recall. */
|
|
23
|
+
export declare function topicalWordCount(s: string): number;
|
|
24
|
+
export declare function recentConversationContext(transcriptPath: string, opts?: Opts): string;
|
|
25
|
+
export {};
|