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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nlm-memory",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Local-first persistent memory for AI coding agents. Recalls relevant prior sessions on every prompt; learns which sessions actually get cited. Owner-edited timeline beats append-only memory layers.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Edward Chalupa",
|
|
@@ -6,14 +6,192 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
// src/core/hook/gate.ts
|
|
7
7
|
var LEADING_FILLER = /^(please|can you|could you|would you|will you|i need you to|i'd like you to|i want you to|i would like you to|help me|let's|lets|hey|ok|okay)\b[\s,]*/i;
|
|
8
8
|
var GENERATIVE_OPENER = /^(write|draft|create|compose|generate|brainstorm|design|outline|sketch|invent|rename|come up with)\b/i;
|
|
9
|
+
var HARNESS_TAGS = [
|
|
10
|
+
"ide_selection",
|
|
11
|
+
"ide_opened_file",
|
|
12
|
+
"ide_closed_file",
|
|
13
|
+
"ide_diagnostics",
|
|
14
|
+
"ide_recently_modified_file",
|
|
15
|
+
"task-notification",
|
|
16
|
+
"system-reminder",
|
|
17
|
+
"command-name",
|
|
18
|
+
"command-message",
|
|
19
|
+
"command-args",
|
|
20
|
+
"local-command-stdout",
|
|
21
|
+
"local-command-stderr"
|
|
22
|
+
];
|
|
23
|
+
var HARNESS_BLOCK = new RegExp(
|
|
24
|
+
`<(${HARNESS_TAGS.join("|")})\\b[^>]*>[\\s\\S]*?(?:</\\1>|$)`,
|
|
25
|
+
"gi"
|
|
26
|
+
);
|
|
27
|
+
var ACK_ONLY = /^(yes|yep|yeah|sure|ok|okay|k|thanks|thank you|ty|done|nice|cool|great|perfect|continue|next|go|go ahead|do it|yes please|sounds good|good|got it|right|correct)\W*$/i;
|
|
28
|
+
function contentWordCount(s) {
|
|
29
|
+
const m = s.match(/[A-Za-z0-9]{2,}/g);
|
|
30
|
+
return m ? m.length : 0;
|
|
31
|
+
}
|
|
9
32
|
function classifyPrompt(prompt) {
|
|
10
|
-
let p = prompt.trim();
|
|
33
|
+
let p = prompt.replace(HARNESS_BLOCK, " ").replace(/\s+/g, " ").trim();
|
|
34
|
+
if (contentWordCount(p) === 0) return "skip";
|
|
35
|
+
if (ACK_ONLY.test(p)) return "skip";
|
|
11
36
|
for (let i = 0; i < 3 && LEADING_FILLER.test(p); i++) {
|
|
12
37
|
p = p.replace(LEADING_FILLER, "");
|
|
13
38
|
}
|
|
39
|
+
p = p.trim();
|
|
40
|
+
if (contentWordCount(p) === 0) return "skip";
|
|
14
41
|
return GENERATIVE_OPENER.test(p) ? "generative" : "evaluate";
|
|
15
42
|
}
|
|
16
43
|
|
|
44
|
+
// src/hook/recent-context.ts
|
|
45
|
+
import { closeSync, existsSync, fstatSync, openSync, readSync } from "node:fs";
|
|
46
|
+
var DEFAULT_MAX_TURNS = 3;
|
|
47
|
+
var DEFAULT_MAX_BYTES = 64 * 1024;
|
|
48
|
+
var DEFAULT_PER_TURN_CHARS = 400;
|
|
49
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
50
|
+
"the",
|
|
51
|
+
"a",
|
|
52
|
+
"an",
|
|
53
|
+
"and",
|
|
54
|
+
"or",
|
|
55
|
+
"but",
|
|
56
|
+
"is",
|
|
57
|
+
"are",
|
|
58
|
+
"was",
|
|
59
|
+
"were",
|
|
60
|
+
"be",
|
|
61
|
+
"been",
|
|
62
|
+
"being",
|
|
63
|
+
"do",
|
|
64
|
+
"does",
|
|
65
|
+
"did",
|
|
66
|
+
"doing",
|
|
67
|
+
"you",
|
|
68
|
+
"your",
|
|
69
|
+
"i",
|
|
70
|
+
"we",
|
|
71
|
+
"our",
|
|
72
|
+
"it",
|
|
73
|
+
"its",
|
|
74
|
+
"this",
|
|
75
|
+
"that",
|
|
76
|
+
"these",
|
|
77
|
+
"those",
|
|
78
|
+
"what",
|
|
79
|
+
"which",
|
|
80
|
+
"how",
|
|
81
|
+
"why",
|
|
82
|
+
"when",
|
|
83
|
+
"where",
|
|
84
|
+
"who",
|
|
85
|
+
"can",
|
|
86
|
+
"could",
|
|
87
|
+
"would",
|
|
88
|
+
"should",
|
|
89
|
+
"will",
|
|
90
|
+
"shall",
|
|
91
|
+
"may",
|
|
92
|
+
"might",
|
|
93
|
+
"must",
|
|
94
|
+
"please",
|
|
95
|
+
"to",
|
|
96
|
+
"of",
|
|
97
|
+
"in",
|
|
98
|
+
"on",
|
|
99
|
+
"for",
|
|
100
|
+
"with",
|
|
101
|
+
"so",
|
|
102
|
+
"now",
|
|
103
|
+
"then",
|
|
104
|
+
"here",
|
|
105
|
+
"there",
|
|
106
|
+
"just",
|
|
107
|
+
"also",
|
|
108
|
+
"as",
|
|
109
|
+
"at",
|
|
110
|
+
"by",
|
|
111
|
+
"if",
|
|
112
|
+
"my",
|
|
113
|
+
"me",
|
|
114
|
+
"us",
|
|
115
|
+
"ok",
|
|
116
|
+
"okay",
|
|
117
|
+
"yes",
|
|
118
|
+
"no",
|
|
119
|
+
"not",
|
|
120
|
+
"up",
|
|
121
|
+
"out",
|
|
122
|
+
"go",
|
|
123
|
+
"get",
|
|
124
|
+
"got",
|
|
125
|
+
"make",
|
|
126
|
+
"made",
|
|
127
|
+
"give",
|
|
128
|
+
"tell",
|
|
129
|
+
"want",
|
|
130
|
+
"need",
|
|
131
|
+
"think",
|
|
132
|
+
"know",
|
|
133
|
+
"let",
|
|
134
|
+
"lets",
|
|
135
|
+
"about",
|
|
136
|
+
"from",
|
|
137
|
+
"into"
|
|
138
|
+
]);
|
|
139
|
+
function topicalWordCount(s) {
|
|
140
|
+
const tokens = s.toLowerCase().match(/[a-z0-9]{2,}/g) ?? [];
|
|
141
|
+
return tokens.filter((t) => !STOPWORDS.has(t)).length;
|
|
142
|
+
}
|
|
143
|
+
function textOf(message) {
|
|
144
|
+
if (typeof message !== "object" || message === null) return "";
|
|
145
|
+
const content = message.content;
|
|
146
|
+
if (typeof content === "string") return content;
|
|
147
|
+
if (Array.isArray(content)) {
|
|
148
|
+
return content.filter(
|
|
149
|
+
(b) => typeof b === "object" && b !== null && b.type === "text"
|
|
150
|
+
).map((b) => b.text).join(" ");
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
function tailRead(path, maxBytes) {
|
|
155
|
+
const fd = openSync(path, "r");
|
|
156
|
+
try {
|
|
157
|
+
const size = fstatSync(fd).size;
|
|
158
|
+
const start = size > maxBytes ? size - maxBytes : 0;
|
|
159
|
+
const len = size - start;
|
|
160
|
+
if (len <= 0) return "";
|
|
161
|
+
const buf = Buffer.alloc(len);
|
|
162
|
+
readSync(fd, buf, 0, len, start);
|
|
163
|
+
return buf.toString("utf8");
|
|
164
|
+
} finally {
|
|
165
|
+
closeSync(fd);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function recentConversationContext(transcriptPath, opts = {}) {
|
|
169
|
+
try {
|
|
170
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return "";
|
|
171
|
+
const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
172
|
+
const perTurnChars = opts.perTurnChars ?? DEFAULT_PER_TURN_CHARS;
|
|
173
|
+
const lines = tailRead(transcriptPath, opts.maxBytes ?? DEFAULT_MAX_BYTES).split("\n");
|
|
174
|
+
const turns = [];
|
|
175
|
+
for (let i = lines.length - 1; i >= 0 && turns.length < maxTurns; i--) {
|
|
176
|
+
const line = lines[i].trim();
|
|
177
|
+
if (!line) continue;
|
|
178
|
+
let evt;
|
|
179
|
+
try {
|
|
180
|
+
evt = JSON.parse(line);
|
|
181
|
+
} catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (evt.type !== "user" && evt.type !== "assistant") continue;
|
|
185
|
+
const text = textOf(evt.message).trim();
|
|
186
|
+
if (!text) continue;
|
|
187
|
+
turns.unshift(text.slice(0, perTurnChars));
|
|
188
|
+
}
|
|
189
|
+
return turns.join(" ").trim();
|
|
190
|
+
} catch {
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
17
195
|
// src/core/hook/hook-log.ts
|
|
18
196
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
19
197
|
import { homedir } from "node:os";
|
|
@@ -32,7 +210,15 @@ function appendHookLog(entry) {
|
|
|
32
210
|
}
|
|
33
211
|
|
|
34
212
|
// src/core/hook/memo.ts
|
|
35
|
-
import {
|
|
213
|
+
import {
|
|
214
|
+
existsSync as existsSync2,
|
|
215
|
+
mkdirSync as mkdirSync2,
|
|
216
|
+
readdirSync,
|
|
217
|
+
readFileSync,
|
|
218
|
+
rmSync,
|
|
219
|
+
statSync,
|
|
220
|
+
writeFileSync
|
|
221
|
+
} from "node:fs";
|
|
36
222
|
import { homedir as homedir2 } from "node:os";
|
|
37
223
|
import { join as join2 } from "node:path";
|
|
38
224
|
function stateDir() {
|
|
@@ -45,7 +231,7 @@ function memoPath(conversationId) {
|
|
|
45
231
|
function loadSurfaced(conversationId) {
|
|
46
232
|
try {
|
|
47
233
|
const path = memoPath(conversationId);
|
|
48
|
-
if (!
|
|
234
|
+
if (!existsSync2(path)) return /* @__PURE__ */ new Set();
|
|
49
235
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
50
236
|
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
51
237
|
return new Set(parsed.filter((x) => typeof x === "string"));
|
|
@@ -64,6 +250,26 @@ function recordSurfaced(conversationId, ids) {
|
|
|
64
250
|
}
|
|
65
251
|
|
|
66
252
|
// src/core/hook/pointer-block.ts
|
|
253
|
+
function truncateSummary(s, max = 200) {
|
|
254
|
+
if (s.length <= max) return s;
|
|
255
|
+
const window = s.slice(0, max);
|
|
256
|
+
const last = window[window.length - 1];
|
|
257
|
+
if ((last === "." || last === "!" || last === "?") && window.length - 1 >= 60) {
|
|
258
|
+
return window;
|
|
259
|
+
}
|
|
260
|
+
for (let i = window.length - 2; i >= 60; i--) {
|
|
261
|
+
const c = window[i];
|
|
262
|
+
if ((c === "." || c === "!" || c === "?") && window[i + 1] === " ") {
|
|
263
|
+
return s.slice(0, i + 1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (let i = window.length - 1; i >= 60; i--) {
|
|
267
|
+
if (window[i] === " ") {
|
|
268
|
+
return s.slice(0, i) + " ...";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return window + " ...";
|
|
272
|
+
}
|
|
67
273
|
function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
68
274
|
if (hits.length === 0 && facts.length === 0 && exemplars.length === 0) return "";
|
|
69
275
|
const out = [];
|
|
@@ -72,7 +278,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
72
278
|
for (const h of hits) {
|
|
73
279
|
const datePart = h.startedAt.slice(0, 10);
|
|
74
280
|
if (h.summary) {
|
|
75
|
-
out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${h.summary
|
|
281
|
+
out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${truncateSummary(h.summary)}`);
|
|
76
282
|
} else {
|
|
77
283
|
out.push(`- ${h.id} \xB7 ${h.label} (${datePart})`);
|
|
78
284
|
}
|
|
@@ -91,7 +297,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
91
297
|
out.push("## Related code exemplars (nlm-memory)");
|
|
92
298
|
for (const e of exemplars) {
|
|
93
299
|
const langPart = e.lang ? `${e.lang} \xB7 ` : "";
|
|
94
|
-
out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${e.taskContext
|
|
300
|
+
out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${truncateSummary(e.taskContext)}`);
|
|
95
301
|
}
|
|
96
302
|
}
|
|
97
303
|
const tools = exemplars.length > 0 ? "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved), recall_code (pull the full code for a related exemplar)." : "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved).";
|
|
@@ -100,10 +306,16 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
100
306
|
}
|
|
101
307
|
|
|
102
308
|
// src/core/hook/select.ts
|
|
309
|
+
function medianScore(hits) {
|
|
310
|
+
if (hits.length === 0) return 0;
|
|
311
|
+
const sorted = hits.map((h) => h.matchScore).sort((a, b) => a - b);
|
|
312
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
313
|
+
}
|
|
103
314
|
function selectHits(params) {
|
|
104
|
-
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
|
|
315
|
+
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap, relativeFloor = 0 } = params;
|
|
316
|
+
const relCut = relativeFloor > 0 ? relativeFloor * medianScore(hits) : 0;
|
|
105
317
|
const eligible = hits.filter(
|
|
106
|
-
(h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id)
|
|
318
|
+
(h) => h.matchScore >= scoreThreshold && h.matchScore >= relCut && !surfaced.has(h.id)
|
|
107
319
|
);
|
|
108
320
|
const budget = Math.max(0, perConversationCap - surfaced.size);
|
|
109
321
|
const limit = Math.min(perFireCap, budget);
|
|
@@ -111,7 +323,7 @@ function selectHits(params) {
|
|
|
111
323
|
}
|
|
112
324
|
|
|
113
325
|
// src/llm/env-autoload.ts
|
|
114
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
326
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
115
327
|
import { homedir as homedir3 } from "node:os";
|
|
116
328
|
import { resolve } from "node:path";
|
|
117
329
|
var DEFAULT_SEARCH_PATHS = [
|
|
@@ -129,7 +341,7 @@ function autoloadEnv(extraPaths = []) {
|
|
|
129
341
|
const paths = [...DEFAULT_SEARCH_PATHS, ...extraPaths];
|
|
130
342
|
for (const raw of paths) {
|
|
131
343
|
const path = expandHome(raw);
|
|
132
|
-
if (!
|
|
344
|
+
if (!existsSync3(path)) continue;
|
|
133
345
|
try {
|
|
134
346
|
const content = readFileSync2(path, "utf8");
|
|
135
347
|
for (const line of content.split("\n")) {
|
|
@@ -161,7 +373,7 @@ function hookAuthHeaders(extra = {}) {
|
|
|
161
373
|
}
|
|
162
374
|
|
|
163
375
|
// src/core/hook/query-extract.ts
|
|
164
|
-
var
|
|
376
|
+
var STOPWORDS2 = /* @__PURE__ */ new Set([
|
|
165
377
|
"a",
|
|
166
378
|
"an",
|
|
167
379
|
"the",
|
|
@@ -293,30 +505,55 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
293
505
|
]);
|
|
294
506
|
var MIN_CONTENT_WORDS = 2;
|
|
295
507
|
var MIN_WORD_LEN = 3;
|
|
508
|
+
var SYSTEM_MESSAGE_PREFIX = /^<(task-notification|command-name|command-message|command-args|local-command-stdout|local-command-caveat|output-file|system-reminder)\b/;
|
|
296
509
|
function extractRecallQuery(prompt) {
|
|
510
|
+
if (SYSTEM_MESSAGE_PREFIX.test(prompt.trim())) return null;
|
|
297
511
|
const tokens = prompt.trim().split(/\s+/).map((t) => t.replace(/^[^\w-]+|[^\w-]+$/g, "")).filter((t) => t.length >= MIN_WORD_LEN);
|
|
298
|
-
const contentWords = tokens.filter((t) => !
|
|
512
|
+
const contentWords = tokens.filter((t) => !STOPWORDS2.has(t.toLowerCase()));
|
|
299
513
|
if (contentWords.length < MIN_CONTENT_WORDS) return null;
|
|
300
514
|
return contentWords.join(" ");
|
|
301
515
|
}
|
|
302
516
|
|
|
517
|
+
// src/hook/hook-helpers.ts
|
|
518
|
+
function readStdin() {
|
|
519
|
+
return new Promise((resolve2) => {
|
|
520
|
+
let data = "";
|
|
521
|
+
process.stdin.setEncoding("utf8");
|
|
522
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
523
|
+
process.stdin.on("end", () => resolve2(data));
|
|
524
|
+
process.stdin.on("error", () => resolve2(data));
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function fetchWithTimeout(url, init, ms) {
|
|
528
|
+
const controller = new AbortController();
|
|
529
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
530
|
+
try {
|
|
531
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
532
|
+
} finally {
|
|
533
|
+
clearTimeout(timer);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function hookModeFromEnv() {
|
|
537
|
+
return process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
538
|
+
}
|
|
539
|
+
|
|
303
540
|
// src/hook/recall-over-http.ts
|
|
304
541
|
var RECALL_LIMIT = 5;
|
|
305
542
|
var RECALL_TIMEOUT_MS = 2e3;
|
|
306
|
-
async function recallOverHttp(prompt, runtime, conversationId) {
|
|
543
|
+
async function recallOverHttp(prompt, runtime, conversationId, mode = "keyword") {
|
|
307
544
|
const query = extractRecallQuery(prompt);
|
|
308
545
|
if (query === null) return { hits: [], facts: [], exemplars: [] };
|
|
309
546
|
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
310
|
-
const url =
|
|
311
|
-
|
|
312
|
-
|
|
547
|
+
const url = (
|
|
548
|
+
// 127.0.0.1, not localhost: each hook is a fresh process with no connection
|
|
549
|
+
// reuse, and Node resolves localhost to IPv6 ::1 first — a measured ~50-300ms
|
|
550
|
+
// per-fire connect penalty vs ~3ms on the explicit IPv4 loopback.
|
|
551
|
+
`http://127.0.0.1:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=${mode}&limit=${RECALL_LIMIT}&withFacts=true&withExemplars=true` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "")
|
|
552
|
+
);
|
|
313
553
|
try {
|
|
314
554
|
const extra = { "x-recall-source": "hook" };
|
|
315
555
|
if (runtime) extra["x-recall-runtime"] = runtime;
|
|
316
|
-
const res = await
|
|
317
|
-
headers: hookAuthHeaders(extra),
|
|
318
|
-
signal: controller.signal
|
|
319
|
-
});
|
|
556
|
+
const res = await fetchWithTimeout(url, { headers: hookAuthHeaders(extra) }, RECALL_TIMEOUT_MS);
|
|
320
557
|
if (!res.ok) return { hits: [], facts: [], exemplars: [] };
|
|
321
558
|
let body;
|
|
322
559
|
try {
|
|
@@ -344,20 +581,114 @@ async function recallOverHttp(prompt, runtime, conversationId) {
|
|
|
344
581
|
taskContext: e.taskContext
|
|
345
582
|
}));
|
|
346
583
|
return { hits, facts, exemplars };
|
|
347
|
-
}
|
|
348
|
-
|
|
584
|
+
} catch {
|
|
585
|
+
return { hits: [], facts: [], exemplars: [] };
|
|
349
586
|
}
|
|
350
587
|
}
|
|
351
588
|
|
|
589
|
+
// src/hook/score-floor.ts
|
|
590
|
+
function parseScoreFloor(raw) {
|
|
591
|
+
if (raw === void 0) return 0;
|
|
592
|
+
const parsed = Number(raw);
|
|
593
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
|
594
|
+
return parsed;
|
|
595
|
+
}
|
|
596
|
+
function parseRelativeFloor(raw, fallback) {
|
|
597
|
+
if (raw === void 0) return fallback;
|
|
598
|
+
const parsed = Number(raw);
|
|
599
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
600
|
+
return parsed;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/hook/recall-gate.ts
|
|
604
|
+
var RECALL_GATE_MODEL = "qwen3.5:4b";
|
|
605
|
+
var 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. Output {"gate":"relevant"|"irrelevant"}.`;
|
|
606
|
+
var GATE_FORMAT = { type: "object", properties: { gate: { type: "string", enum: ["relevant", "irrelevant"] } }, required: ["gate"] };
|
|
607
|
+
var GATE_OPTS = { temperature: 0, top_p: 1, top_k: 0, presence_penalty: 0, frequency_penalty: 0 };
|
|
608
|
+
function parseRecallGateMode(env = process.env) {
|
|
609
|
+
const v = env["NLM_HOOK_RECALL_GATE"]?.trim();
|
|
610
|
+
return v === "shadow" || v === "live" ? v : void 0;
|
|
611
|
+
}
|
|
612
|
+
var GATE_TIMEOUT_MS = 4e3;
|
|
613
|
+
function makeOllamaGate(url, model = RECALL_GATE_MODEL, timeoutMs = GATE_TIMEOUT_MS) {
|
|
614
|
+
return async (prompt, candidate) => {
|
|
615
|
+
try {
|
|
616
|
+
const r = await fetchWithTimeout(
|
|
617
|
+
`${url}/api/chat`,
|
|
618
|
+
{
|
|
619
|
+
method: "POST",
|
|
620
|
+
headers: { "Content-Type": "application/json" },
|
|
621
|
+
body: JSON.stringify({
|
|
622
|
+
model,
|
|
623
|
+
stream: false,
|
|
624
|
+
think: false,
|
|
625
|
+
// Keep the gate model resident between fires so only the first fire
|
|
626
|
+
// pays the cold-load; subsequent gates are ~1 judge call.
|
|
627
|
+
keep_alive: "15m",
|
|
628
|
+
format: GATE_FORMAT,
|
|
629
|
+
options: GATE_OPTS,
|
|
630
|
+
messages: [
|
|
631
|
+
{ role: "system", content: RECALL_GATE_SYSTEM },
|
|
632
|
+
{ role: "user", content: `USER PROMPT:
|
|
633
|
+
${prompt}
|
|
634
|
+
|
|
635
|
+
CANDIDATE CONTEXT:
|
|
636
|
+
${candidate}` }
|
|
637
|
+
]
|
|
638
|
+
})
|
|
639
|
+
},
|
|
640
|
+
timeoutMs
|
|
641
|
+
);
|
|
642
|
+
const d = await r.json();
|
|
643
|
+
const v = JSON.parse(d.message?.content ?? "{}").gate;
|
|
644
|
+
return v === "irrelevant" ? "irrelevant" : "relevant";
|
|
645
|
+
} catch {
|
|
646
|
+
return "relevant";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
352
651
|
// src/hook/prompt-recall-hook.ts
|
|
353
|
-
var SCORE_THRESHOLD =
|
|
652
|
+
var SCORE_THRESHOLD = parseScoreFloor(process.env["NLM_RECALL_SCORE_FLOOR"]);
|
|
653
|
+
var RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
|
|
354
654
|
var PER_FIRE_CAP = 3;
|
|
355
655
|
var PER_CONVERSATION_CAP = 10;
|
|
356
656
|
var PROMPT_PREVIEW_CHARS = 200;
|
|
657
|
+
function parseHookDeadline(raw) {
|
|
658
|
+
if (raw === void 0) return 4e3;
|
|
659
|
+
const n = Number(raw);
|
|
660
|
+
return Number.isFinite(n) && n > 0 ? n : 4e3;
|
|
661
|
+
}
|
|
662
|
+
var HOOK_DEADLINE_MS = parseHookDeadline(process.env["NLM_HOOK_DEADLINE_MS"]);
|
|
663
|
+
async function withDeadline(p, ms, fallback) {
|
|
664
|
+
if (ms <= 0) return fallback;
|
|
665
|
+
let timer;
|
|
666
|
+
const timeout = new Promise((resolve2) => {
|
|
667
|
+
timer = setTimeout(() => resolve2(fallback), ms);
|
|
668
|
+
});
|
|
669
|
+
try {
|
|
670
|
+
return await Promise.race([p, timeout]);
|
|
671
|
+
} finally {
|
|
672
|
+
clearTimeout(timer);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
357
675
|
function hookRuntimeFromEnv(env = process.env) {
|
|
358
676
|
const raw = env["NLM_HOOK_RUNTIME"]?.trim();
|
|
359
677
|
return raw ? raw : "claude-code";
|
|
360
678
|
}
|
|
679
|
+
function promptRecallEnabled(env = process.env) {
|
|
680
|
+
const raw = env["NLM_HOOK_PROMPT_RECALL"]?.trim();
|
|
681
|
+
if (raw === void 0 || raw === "") return false;
|
|
682
|
+
return raw.toLowerCase() !== "off";
|
|
683
|
+
}
|
|
684
|
+
function buildRecallQuery(input, env = process.env) {
|
|
685
|
+
if (env["NLM_HOOK_CONTEXT_RECALL"] !== "1") return input.prompt;
|
|
686
|
+
if (!input.transcriptPath) return input.prompt;
|
|
687
|
+
const minWords = Number.parseInt(env["NLM_HOOK_CONTEXT_MIN_WORDS"] ?? "3", 10);
|
|
688
|
+
if (topicalWordCount(input.prompt) >= minWords) return input.prompt;
|
|
689
|
+
const context = recentConversationContext(input.transcriptPath);
|
|
690
|
+
return context ? `${context} ${input.prompt}` : input.prompt;
|
|
691
|
+
}
|
|
361
692
|
function normalizeRecall(raw) {
|
|
362
693
|
if (Array.isArray(raw)) return { hits: raw, facts: [] };
|
|
363
694
|
return raw;
|
|
@@ -365,7 +696,7 @@ function normalizeRecall(raw) {
|
|
|
365
696
|
async function runHook(input, deps) {
|
|
366
697
|
const gate = classifyPrompt(input.prompt);
|
|
367
698
|
const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
|
|
368
|
-
if (gate === "generative") {
|
|
699
|
+
if (gate === "generative" || gate === "skip") {
|
|
369
700
|
appendHookLog({
|
|
370
701
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
371
702
|
conversationId: input.conversationId,
|
|
@@ -378,9 +709,12 @@ async function runHook(input, deps) {
|
|
|
378
709
|
});
|
|
379
710
|
return "";
|
|
380
711
|
}
|
|
712
|
+
const deadline = Date.now() + (deps.deadlineMs ?? HOOK_DEADLINE_MS);
|
|
381
713
|
let fetched = { hits: [], facts: [] };
|
|
382
714
|
try {
|
|
383
|
-
fetched = normalizeRecall(
|
|
715
|
+
fetched = normalizeRecall(
|
|
716
|
+
await withDeadline(deps.recall(buildRecallQuery(input)), deadline - Date.now(), { hits: [], facts: [] })
|
|
717
|
+
);
|
|
384
718
|
} catch {
|
|
385
719
|
fetched = { hits: [], facts: [] };
|
|
386
720
|
}
|
|
@@ -390,10 +724,33 @@ async function runHook(input, deps) {
|
|
|
390
724
|
hits,
|
|
391
725
|
surfaced,
|
|
392
726
|
scoreThreshold: SCORE_THRESHOLD,
|
|
727
|
+
relativeFloor: RELATIVE_FLOOR,
|
|
393
728
|
perFireCap: PER_FIRE_CAP,
|
|
394
729
|
perConversationCap: PER_CONVERSATION_CAP
|
|
395
730
|
});
|
|
396
|
-
|
|
731
|
+
let gateDecisions;
|
|
732
|
+
let injected = selected;
|
|
733
|
+
if (deps.recallGate && selected.length > 0) {
|
|
734
|
+
const g = deps.recallGate;
|
|
735
|
+
const toGate = g.maxCandidates ? selected.slice(0, g.maxCandidates) : selected;
|
|
736
|
+
const remaining = deadline - Date.now();
|
|
737
|
+
const gateFallback = toGate.map((h) => ({ id: h.id, gate: "relevant" }));
|
|
738
|
+
if (remaining <= 0) {
|
|
739
|
+
gateDecisions = gateFallback;
|
|
740
|
+
} else {
|
|
741
|
+
gateDecisions = await withDeadline(
|
|
742
|
+
Promise.all(toGate.map(async (h) => ({ id: h.id, gate: await g.judge(input.prompt, `${h.label}
|
|
743
|
+
${h.summary ?? ""}`) }))),
|
|
744
|
+
remaining,
|
|
745
|
+
gateFallback
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
if (g.mode === "live") {
|
|
749
|
+
const drop = new Set(gateDecisions.filter((d) => d.gate === "irrelevant").map((d) => d.id));
|
|
750
|
+
injected = selected.filter((h) => !drop.has(h.id));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const block = formatPointerBlock(injected, fetched.facts, fetched.exemplars);
|
|
397
754
|
const estTokens = Math.ceil(block.length / 4);
|
|
398
755
|
appendHookLog({
|
|
399
756
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -401,25 +758,17 @@ async function runHook(input, deps) {
|
|
|
401
758
|
promptPreview: preview,
|
|
402
759
|
gate,
|
|
403
760
|
hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
|
|
404
|
-
wouldInject:
|
|
761
|
+
wouldInject: injected.map((h) => h.id),
|
|
405
762
|
estTokens,
|
|
406
|
-
mode: deps.mode
|
|
763
|
+
mode: deps.mode,
|
|
764
|
+
...gateDecisions ? { gateDecisions } : {}
|
|
407
765
|
});
|
|
408
|
-
if (deps.mode === "live" &&
|
|
409
|
-
recordSurfaced(input.conversationId,
|
|
766
|
+
if (deps.mode === "live" && injected.length > 0) {
|
|
767
|
+
recordSurfaced(input.conversationId, injected.map((h) => h.id));
|
|
410
768
|
return block;
|
|
411
769
|
}
|
|
412
770
|
return "";
|
|
413
771
|
}
|
|
414
|
-
function readStdin() {
|
|
415
|
-
return new Promise((resolve2) => {
|
|
416
|
-
let data = "";
|
|
417
|
-
process.stdin.setEncoding("utf8");
|
|
418
|
-
process.stdin.on("data", (chunk) => data += chunk);
|
|
419
|
-
process.stdin.on("end", () => resolve2(data));
|
|
420
|
-
process.stdin.on("error", () => resolve2(data));
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
772
|
async function main() {
|
|
424
773
|
try {
|
|
425
774
|
autoloadEnv();
|
|
@@ -427,12 +776,21 @@ async function main() {
|
|
|
427
776
|
const payload = JSON.parse(raw);
|
|
428
777
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
429
778
|
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
779
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : void 0;
|
|
430
780
|
if (!prompt) return;
|
|
431
|
-
|
|
781
|
+
if (!promptRecallEnabled()) return;
|
|
782
|
+
const mode = hookModeFromEnv();
|
|
432
783
|
const runtime = hookRuntimeFromEnv();
|
|
784
|
+
const gateMode = parseRecallGateMode();
|
|
785
|
+
const gateUrl = process.env["OLLAMA_URL"] ?? "http://127.0.0.1:11434";
|
|
786
|
+
const gateTopN = Math.max(1, Number.parseInt(process.env["NLM_HOOK_RECALL_GATE_TOPN"] ?? "1", 10) || 1);
|
|
433
787
|
const out = await runHook(
|
|
434
|
-
{ prompt, conversationId },
|
|
435
|
-
{
|
|
788
|
+
{ prompt, conversationId, ...transcriptPath ? { transcriptPath } : {} },
|
|
789
|
+
{
|
|
790
|
+
mode,
|
|
791
|
+
recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId),
|
|
792
|
+
...gateMode ? { recallGate: { mode: gateMode, judge: makeOllamaGate(gateUrl), maxCandidates: gateTopN } } : {}
|
|
793
|
+
}
|
|
436
794
|
);
|
|
437
795
|
if (out) process.stdout.write(out);
|
|
438
796
|
} catch {
|
|
@@ -442,6 +800,9 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
442
800
|
void main();
|
|
443
801
|
}
|
|
444
802
|
export {
|
|
803
|
+
buildRecallQuery,
|
|
445
804
|
hookRuntimeFromEnv,
|
|
805
|
+
parseHookDeadline,
|
|
806
|
+
promptRecallEnabled,
|
|
446
807
|
runHook
|
|
447
808
|
};
|