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,318 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "01-bug-fix-null-pointer-api",
|
|
4
|
+
"label": "Fix null pointer bug in corvid-api events route",
|
|
5
|
+
"labelAlternates": [
|
|
6
|
+
"corvid-api POST events null source fix",
|
|
7
|
+
"Fix missing source field TypeError in events handler"
|
|
8
|
+
],
|
|
9
|
+
"entities": [
|
|
10
|
+
"corvid-api",
|
|
11
|
+
"Express",
|
|
12
|
+
"Postgres"
|
|
13
|
+
],
|
|
14
|
+
"decisions": [
|
|
15
|
+
"Reject requests missing source field with 400 instead of defaulting"
|
|
16
|
+
],
|
|
17
|
+
"expectLowConfidence": false
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "02-bug-fix-race-condition-queue",
|
|
21
|
+
"label": "Fix duplicate job execution race condition in taskvine",
|
|
22
|
+
"labelAlternates": [
|
|
23
|
+
"taskvine worker race condition fix with SELECT FOR UPDATE",
|
|
24
|
+
"Fix concurrent worker job claim race in taskvine"
|
|
25
|
+
],
|
|
26
|
+
"entities": [
|
|
27
|
+
"taskvine",
|
|
28
|
+
"PostgreSQL",
|
|
29
|
+
"SQLAlchemy"
|
|
30
|
+
],
|
|
31
|
+
"decisions": [
|
|
32
|
+
"Replace unlocked SELECT with SELECT FOR UPDATE SKIP LOCKED to eliminate duplicate job claims"
|
|
33
|
+
],
|
|
34
|
+
"expectLowConfidence": false
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "03-bug-fix-jwt-clock-skew",
|
|
38
|
+
"label": "Fix JWT clock skew validation failures in bridgeauth",
|
|
39
|
+
"labelAlternates": [
|
|
40
|
+
"bridgeauth JWT expiry leeway fix",
|
|
41
|
+
"Add clock skew leeway to PyJWT token validation"
|
|
42
|
+
],
|
|
43
|
+
"entities": [
|
|
44
|
+
"bridgeauth",
|
|
45
|
+
"PyJWT",
|
|
46
|
+
"HS256"
|
|
47
|
+
],
|
|
48
|
+
"decisions": [
|
|
49
|
+
"Add clock-skew leeway to JWT validation"
|
|
50
|
+
],
|
|
51
|
+
"expectLowConfidence": false
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "04-feature-webhook-retry",
|
|
55
|
+
"label": "Add exponential backoff webhook retry queue to taskstream",
|
|
56
|
+
"labelAlternates": [
|
|
57
|
+
"taskstream webhook retry with Redis sorted set",
|
|
58
|
+
"Implement webhook retry with exponential backoff in taskstream"
|
|
59
|
+
],
|
|
60
|
+
"entities": [
|
|
61
|
+
"taskstream",
|
|
62
|
+
"Redis"
|
|
63
|
+
],
|
|
64
|
+
"decisions": [
|
|
65
|
+
"Use Redis sorted set with score as next-attempt timestamp for retry queue",
|
|
66
|
+
"Dead-letter failed jobs after 5 attempts with exponential backoff"
|
|
67
|
+
],
|
|
68
|
+
"expectLowConfidence": false
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "05-feature-csv-export",
|
|
72
|
+
"label": "Add streaming CSV export endpoint to lumens",
|
|
73
|
+
"labelAlternates": [
|
|
74
|
+
"lumens GET /reports/export CSV streaming",
|
|
75
|
+
"Implement CSV export with streaming response in lumens"
|
|
76
|
+
],
|
|
77
|
+
"entities": [
|
|
78
|
+
"lumens",
|
|
79
|
+
"encoding/csv"
|
|
80
|
+
],
|
|
81
|
+
"decisions": [
|
|
82
|
+
"Stream CSV directly to http.ResponseWriter instead of buffering in memory"
|
|
83
|
+
],
|
|
84
|
+
"expectLowConfidence": false
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "06-feature-rate-limiting",
|
|
88
|
+
"label": "Add Redis-backed rate limiting to crestapi search endpoint",
|
|
89
|
+
"labelAlternates": [
|
|
90
|
+
"crestapi API rate limiting with slowapi and Redis",
|
|
91
|
+
"Implement per-API-key rate limiting in crestapi"
|
|
92
|
+
],
|
|
93
|
+
"entities": [
|
|
94
|
+
"crestapi",
|
|
95
|
+
"FastAPI",
|
|
96
|
+
"Redis",
|
|
97
|
+
"slowapi"
|
|
98
|
+
],
|
|
99
|
+
"decisions": [
|
|
100
|
+
"Use slowapi with Redis backend for 100 requests per minute per API key"
|
|
101
|
+
],
|
|
102
|
+
"expectLowConfidence": false
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "07-refactor-db-abstraction",
|
|
106
|
+
"label": "Extract DbAdapter interface to ports layer in vaultdb",
|
|
107
|
+
"labelAlternates": [
|
|
108
|
+
"vaultdb database adapter abstraction refactor",
|
|
109
|
+
"Refactor vaultdb to depend on DbAdapter interface not Postgres directly"
|
|
110
|
+
],
|
|
111
|
+
"entities": [
|
|
112
|
+
"vaultdb",
|
|
113
|
+
"TypeScript",
|
|
114
|
+
"Postgres",
|
|
115
|
+
"SQLite"
|
|
116
|
+
],
|
|
117
|
+
"decisions": [
|
|
118
|
+
"Extract DbAdapter interface to ports layer so core modules do not import Postgres directly"
|
|
119
|
+
],
|
|
120
|
+
"expectLowConfidence": false
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "08-refactor-config-loading",
|
|
124
|
+
"label": "Centralize config into Pydantic Settings class in axiom-worker",
|
|
125
|
+
"labelAlternates": [
|
|
126
|
+
"axiom-worker config refactor using Pydantic Settings",
|
|
127
|
+
"Replace scattered os.getenv calls with Pydantic Settings in axiom-worker"
|
|
128
|
+
],
|
|
129
|
+
"entities": [
|
|
130
|
+
"axiom-worker",
|
|
131
|
+
"Pydantic",
|
|
132
|
+
"pydantic-settings"
|
|
133
|
+
],
|
|
134
|
+
"decisions": [
|
|
135
|
+
"Replace scattered os.getenv calls with a single Pydantic Settings class loaded at startup"
|
|
136
|
+
],
|
|
137
|
+
"expectLowConfidence": false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "09-ops-docker-staging",
|
|
141
|
+
"label": "Set up Docker Compose staging environment with Traefik for droneflow",
|
|
142
|
+
"labelAlternates": [
|
|
143
|
+
"droneflow staging docker-compose with Traefik and TLS",
|
|
144
|
+
"Create staging Docker Compose stack for droneflow"
|
|
145
|
+
],
|
|
146
|
+
"entities": [
|
|
147
|
+
"droneflow",
|
|
148
|
+
"Docker Compose",
|
|
149
|
+
"Traefik"
|
|
150
|
+
],
|
|
151
|
+
"decisions": [
|
|
152
|
+
"Use Traefik for TLS termination and routing in staging",
|
|
153
|
+
"Keep staging deploys explicit via deploy script rather than auto-pull"
|
|
154
|
+
],
|
|
155
|
+
"expectLowConfidence": false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "10-ops-nginx-ssl",
|
|
159
|
+
"label": "Configure nginx reverse proxy with TLS for gridhost",
|
|
160
|
+
"labelAlternates": [
|
|
161
|
+
"gridhost nginx TLS setup with certbot and Let's Encrypt",
|
|
162
|
+
"Set up nginx with Let's Encrypt certificate for gridhost"
|
|
163
|
+
],
|
|
164
|
+
"entities": [
|
|
165
|
+
"gridhost",
|
|
166
|
+
"nginx",
|
|
167
|
+
"certbot"
|
|
168
|
+
],
|
|
169
|
+
"decisions": [
|
|
170
|
+
"Terminate TLS at nginx and proxy to app on port 8080",
|
|
171
|
+
"Add cron fallback for certbot renewal in case systemd timer is disabled"
|
|
172
|
+
],
|
|
173
|
+
"expectLowConfidence": false
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"id": "11-ops-postgres-migration",
|
|
177
|
+
"label": "Run concurrent index migration on Postgres sessions table",
|
|
178
|
+
"labelAlternates": [
|
|
179
|
+
"Add CONCURRENTLY indexes to sessions table via Alembic",
|
|
180
|
+
"Postgres sessions table index migration without table lock"
|
|
181
|
+
],
|
|
182
|
+
"entities": [
|
|
183
|
+
"PostgreSQL",
|
|
184
|
+
"Alembic"
|
|
185
|
+
],
|
|
186
|
+
"decisions": [
|
|
187
|
+
"Use CREATE INDEX CONCURRENTLY to avoid locking the 8-million-row sessions table",
|
|
188
|
+
"Set statement_timeout to 0 for the migration connection to prevent timeout killing the index build"
|
|
189
|
+
],
|
|
190
|
+
"expectLowConfidence": false
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "12-ops-cron-debugging",
|
|
194
|
+
"label": "Debug and fix flarepath nightly cron job not running",
|
|
195
|
+
"labelAlternates": [
|
|
196
|
+
"flarepath cron exit 126 permission denied fix",
|
|
197
|
+
"Fix missing execute bit on flarepath nightly report script"
|
|
198
|
+
],
|
|
199
|
+
"entities": [
|
|
200
|
+
"flarepath"
|
|
201
|
+
],
|
|
202
|
+
"decisions": [
|
|
203
|
+
"Add chmod to deploy script to re-apply execute bit after git checkout"
|
|
204
|
+
],
|
|
205
|
+
"expectLowConfidence": false
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"id": "13-research-vector-db-comparison",
|
|
209
|
+
"label": "Vector database comparison and Qdrant selection for pulsearch",
|
|
210
|
+
"labelAlternates": [
|
|
211
|
+
"pulsearch vector DB evaluation: pgvector vs Qdrant vs Weaviate",
|
|
212
|
+
"Choose Qdrant for pulsearch vector search"
|
|
213
|
+
],
|
|
214
|
+
"entities": [
|
|
215
|
+
"pulsearch",
|
|
216
|
+
"Qdrant",
|
|
217
|
+
"pgvector",
|
|
218
|
+
"Weaviate"
|
|
219
|
+
],
|
|
220
|
+
"decisions": [
|
|
221
|
+
"Use Qdrant for pulsearch instead of pgvector due to stronger ANN performance and pre-filter payload filtering"
|
|
222
|
+
],
|
|
223
|
+
"expectLowConfidence": false
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"id": "14-research-oauth-provider",
|
|
227
|
+
"label": "OAuth provider evaluation and Keycloak selection for irongate",
|
|
228
|
+
"labelAlternates": [
|
|
229
|
+
"irongate auth provider comparison: Keycloak vs Auth0 vs SuperTokens",
|
|
230
|
+
"Choose Keycloak self-hosted for irongate customer auth"
|
|
231
|
+
],
|
|
232
|
+
"entities": [
|
|
233
|
+
"irongate",
|
|
234
|
+
"Keycloak",
|
|
235
|
+
"Auth0",
|
|
236
|
+
"SuperTokens"
|
|
237
|
+
],
|
|
238
|
+
"decisions": [
|
|
239
|
+
"Use Keycloak self-hosted backed by Postgres for irongate auth because it covers LDAP federation without additional cost"
|
|
240
|
+
],
|
|
241
|
+
"expectLowConfidence": false
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"id": "15-writing-api-docs",
|
|
245
|
+
"label": "Draft OpenAPI 3.0 documentation for cobalt-api with Swagger UI",
|
|
246
|
+
"labelAlternates": [
|
|
247
|
+
"cobalt-api OpenAPI spec and Swagger UI setup",
|
|
248
|
+
"Set up OpenAPI docs for cobalt-api served at /api-docs"
|
|
249
|
+
],
|
|
250
|
+
"entities": [
|
|
251
|
+
"cobalt-api",
|
|
252
|
+
"OpenAPI",
|
|
253
|
+
"Swagger UI"
|
|
254
|
+
],
|
|
255
|
+
"decisions": [
|
|
256
|
+
"Use OpenAPI 3.0 format and host docs publicly at /api-docs via swagger-ui-express"
|
|
257
|
+
],
|
|
258
|
+
"expectLowConfidence": false
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"id": "16-meeting-sprint-planning",
|
|
262
|
+
"label": "Sprint 5 planning notes for orbitflow team",
|
|
263
|
+
"labelAlternates": [
|
|
264
|
+
"orbitflow sprint 5 planning: webhook retry, rate limiting, deferrals",
|
|
265
|
+
"orbitflow sprint 5 commitments and deferrals"
|
|
266
|
+
],
|
|
267
|
+
"entities": [
|
|
268
|
+
"orbitflow"
|
|
269
|
+
],
|
|
270
|
+
"decisions": [
|
|
271
|
+
"Webhook retry is top backend priority for sprint 5",
|
|
272
|
+
"Analytics dashboard deferred to sprint 6",
|
|
273
|
+
"Rate limiting assigned to backend as secondary sprint 5 item",
|
|
274
|
+
"Mobile push notifications deferred to sprint 6 backlog"
|
|
275
|
+
],
|
|
276
|
+
"expectLowConfidence": false
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"id": "17-trivial-list-files",
|
|
280
|
+
"label": "List files in src directory",
|
|
281
|
+
"labelAlternates": [
|
|
282
|
+
"Directory listing of src folder"
|
|
283
|
+
],
|
|
284
|
+
"entities": [],
|
|
285
|
+
"decisions": [],
|
|
286
|
+
"expectLowConfidence": true
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"id": "18-trivial-version-check",
|
|
290
|
+
"label": "Check Node.js and npm versions",
|
|
291
|
+
"labelAlternates": [
|
|
292
|
+
"Node.js npm version query"
|
|
293
|
+
],
|
|
294
|
+
"entities": [],
|
|
295
|
+
"decisions": [],
|
|
296
|
+
"expectLowConfidence": true
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"id": "19-trivial-syntax-help",
|
|
300
|
+
"label": "TypeScript array iteration syntax question",
|
|
301
|
+
"labelAlternates": [
|
|
302
|
+
"How to iterate array with index in TypeScript"
|
|
303
|
+
],
|
|
304
|
+
"entities": [],
|
|
305
|
+
"decisions": [],
|
|
306
|
+
"expectLowConfidence": true
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"id": "20-trivial-read-package-json",
|
|
310
|
+
"label": "Read package.json and confirm dependencies installed",
|
|
311
|
+
"labelAlternates": [
|
|
312
|
+
"Check installed npm dependencies from package.json"
|
|
313
|
+
],
|
|
314
|
+
"entities": [],
|
|
315
|
+
"decisions": [],
|
|
316
|
+
"expectLowConfidence": true
|
|
317
|
+
}
|
|
318
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
User: The POST /events endpoint in corvid-api is throwing a TypeError in production. Stack trace says "Cannot read properties of undefined (reading 'source')" on line 47 of src/routes/events.ts. Can you find the bug?
|
|
2
|
+
|
|
3
|
+
Agent: Let me read the route handler.
|
|
4
|
+
|
|
5
|
+
[Read: src/routes/events.ts]
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export async function postEvent(req: Request, res: Response) {
|
|
9
|
+
const { source, payload } = req.body;
|
|
10
|
+
// line 47
|
|
11
|
+
const normalized = source.toLowerCase();
|
|
12
|
+
await db.events.insert({ source: normalized, payload, createdAt: new Date() });
|
|
13
|
+
res.status(201).json({ ok: true });
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The bug is on line 47. When the request body omits the `source` field, `source` is `undefined` and calling `.toLowerCase()` throws. There is no guard before the property access.
|
|
18
|
+
|
|
19
|
+
User: Right. What's the fix?
|
|
20
|
+
|
|
21
|
+
Agent: Two options: reject requests missing `source` with a 400, or default it to "unknown". Given this is an event ingestion endpoint where source identity matters for routing downstream, rejecting is safer.
|
|
22
|
+
|
|
23
|
+
User: Agreed. Reject with 400.
|
|
24
|
+
|
|
25
|
+
Agent: Updating the handler now.
|
|
26
|
+
|
|
27
|
+
[Edit: src/routes/events.ts]
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
export async function postEvent(req: Request, res: Response) {
|
|
31
|
+
const { source, payload } = req.body;
|
|
32
|
+
if (typeof source !== "string" || source.trim().length === 0) {
|
|
33
|
+
res.status(400).json({ error: "source is required" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const normalized = source.toLowerCase().trim();
|
|
37
|
+
await db.events.insert({ source: normalized, payload, createdAt: new Date() });
|
|
38
|
+
res.status(201).json({ ok: true });
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
User: Good. Add a test for the missing-source case.
|
|
43
|
+
|
|
44
|
+
Agent: Adding to tests/routes/events.test.ts.
|
|
45
|
+
|
|
46
|
+
[Read: tests/routes/events.test.ts]
|
|
47
|
+
|
|
48
|
+
[Edit: tests/routes/events.test.ts]
|
|
49
|
+
|
|
50
|
+
Added a test case:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
it("returns 400 when source is missing", async () => {
|
|
54
|
+
const res = await request(app).post("/events").send({ payload: { x: 1 } });
|
|
55
|
+
expect(res.status).toBe(400);
|
|
56
|
+
expect(res.body.error).toBe("source is required");
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
User: Run the tests to confirm.
|
|
61
|
+
|
|
62
|
+
Agent: [Bash: npm test -- --testPathPattern=events]
|
|
63
|
+
|
|
64
|
+
Output:
|
|
65
|
+
```
|
|
66
|
+
PASS tests/routes/events.test.ts
|
|
67
|
+
POST /events
|
|
68
|
+
✓ returns 201 for valid event (42ms)
|
|
69
|
+
✓ returns 400 when source is missing (8ms)
|
|
70
|
+
✓ returns 400 when source is empty string (6ms)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
All passing. The fix guards on both undefined and empty-string cases since `trim().length === 0` catches whitespace-only inputs too.
|
|
74
|
+
|
|
75
|
+
User: Perfect. Ship it.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
User: We're seeing duplicate job execution in taskvine. Two workers occasionally pick up the same job from the queue table. The claim logic is in src/worker/claim.py.
|
|
2
|
+
|
|
3
|
+
Agent: [Read: src/worker/claim.py]
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
def claim_next_job(conn):
|
|
7
|
+
with conn.transaction():
|
|
8
|
+
row = conn.execute(
|
|
9
|
+
"SELECT id FROM jobs WHERE status = 'pending' LIMIT 1"
|
|
10
|
+
).fetchone()
|
|
11
|
+
if row is None:
|
|
12
|
+
return None
|
|
13
|
+
conn.execute(
|
|
14
|
+
"UPDATE jobs SET status = 'running', worker_id = %s WHERE id = %s",
|
|
15
|
+
(WORKER_ID, row["id"]),
|
|
16
|
+
)
|
|
17
|
+
return row["id"]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This is a classic race condition. The SELECT and UPDATE are in the same transaction, but between the SELECT and UPDATE another worker can read the same row before either has committed. The advisory lock approach you're using doesn't appear here, so there's no row-level locking.
|
|
21
|
+
|
|
22
|
+
The fix is to use SELECT FOR UPDATE SKIP LOCKED. This atomically locks the selected row for the duration of the transaction and skips rows already locked by another worker.
|
|
23
|
+
|
|
24
|
+
User: Yes, let's switch to SELECT FOR UPDATE SKIP LOCKED.
|
|
25
|
+
|
|
26
|
+
Agent: [Edit: src/worker/claim.py]
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
def claim_next_job(conn):
|
|
30
|
+
with conn.transaction():
|
|
31
|
+
row = conn.execute(
|
|
32
|
+
"""
|
|
33
|
+
SELECT id FROM jobs
|
|
34
|
+
WHERE status = 'pending'
|
|
35
|
+
ORDER BY created_at
|
|
36
|
+
LIMIT 1
|
|
37
|
+
FOR UPDATE SKIP LOCKED
|
|
38
|
+
"""
|
|
39
|
+
).fetchone()
|
|
40
|
+
if row is None:
|
|
41
|
+
return None
|
|
42
|
+
conn.execute(
|
|
43
|
+
"UPDATE jobs SET status = 'running', worker_id = %s WHERE id = %s",
|
|
44
|
+
(WORKER_ID, row["id"]),
|
|
45
|
+
)
|
|
46
|
+
return row["id"]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Added ORDER BY created_at for FIFO fairness while I was in there.
|
|
50
|
+
|
|
51
|
+
User: Does PostgreSQL actually guarantee no duplicates with this pattern?
|
|
52
|
+
|
|
53
|
+
Agent: Yes. FOR UPDATE acquires a row-level lock at SELECT time. SKIP LOCKED means any row already locked by another transaction is skipped entirely rather than waited on. Within a single transaction, any row your SELECT returns is guaranteed not to be selected by another concurrent transaction using the same pattern. This is the canonical pattern for Postgres-backed job queues.
|
|
54
|
+
|
|
55
|
+
User: What about the existing advisory lock code I thought we had?
|
|
56
|
+
|
|
57
|
+
Agent: [Bash: grep -r "advisory\|pg_try_advisory" src/]
|
|
58
|
+
|
|
59
|
+
No results. There is no advisory lock code in the codebase. The previous SELECT without FOR UPDATE had no locking at all, which is why duplicates occurred.
|
|
60
|
+
|
|
61
|
+
User: Got it. Let's also add a test to confirm two concurrent workers cannot claim the same job.
|
|
62
|
+
|
|
63
|
+
Agent: [Read: tests/worker/test_claim.py]
|
|
64
|
+
|
|
65
|
+
[Edit: tests/worker/test_claim.py]
|
|
66
|
+
|
|
67
|
+
Added a test that spawns two threads, each calling claim_next_job with a shared connection pool containing a single pending job, and asserts only one gets a non-None return and the other gets None.
|
|
68
|
+
|
|
69
|
+
User: Run it.
|
|
70
|
+
|
|
71
|
+
Agent: [Bash: pytest tests/worker/test_claim.py -v]
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
tests/worker/test_claim.py::test_concurrent_claim_no_duplicate PASSED
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Fixed. The SELECT FOR UPDATE SKIP LOCKED pattern eliminates the race.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
User: Users are reporting random login failures in bridgeauth. The error in logs is "Token is not yet valid" and "Signature has expired". It's intermittent and seems worse around the token boundary. Here's the validation code in src/auth/token.py.
|
|
2
|
+
|
|
3
|
+
Agent: [Read: src/auth/token.py]
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import jwt
|
|
7
|
+
|
|
8
|
+
SECRET = os.environ["JWT_SECRET"]
|
|
9
|
+
ALGORITHM = "HS256"
|
|
10
|
+
|
|
11
|
+
def validate_token(token: str) -> dict:
|
|
12
|
+
return jwt.decode(token, SECRET, algorithms=[ALGORITHM])
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The validation call has no leeway parameter. JWT tokens have an `exp` claim and PyJWT validates it against the current time. When there is a small clock difference between the server that issued the token and the server validating it, tokens can appear expired seconds before their actual expiry from the issuer's perspective.
|
|
16
|
+
|
|
17
|
+
The standard fix is to add a leeway value, typically 10-30 seconds, which tells PyJWT to accept tokens that expired within that window.
|
|
18
|
+
|
|
19
|
+
User: How big is the clock drift between our services likely to be?
|
|
20
|
+
|
|
21
|
+
Agent: For services within the same datacenter using NTP, drift is usually under 1 second. But network-synchronized clocks under load can drift up to 5 seconds. A 10-second leeway covers realistic cases without meaningfully extending token validity from a security perspective.
|
|
22
|
+
|
|
23
|
+
User: Let's add 10 seconds of leeway.
|
|
24
|
+
|
|
25
|
+
Agent: [Edit: src/auth/token.py]
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import jwt
|
|
29
|
+
from datetime import timedelta
|
|
30
|
+
|
|
31
|
+
SECRET = os.environ["JWT_SECRET"]
|
|
32
|
+
ALGORITHM = "HS256"
|
|
33
|
+
CLOCK_SKEW_LEEWAY = timedelta(seconds=10)
|
|
34
|
+
|
|
35
|
+
def validate_token(token: str) -> dict:
|
|
36
|
+
return jwt.decode(
|
|
37
|
+
token,
|
|
38
|
+
SECRET,
|
|
39
|
+
algorithms=[ALGORITHM],
|
|
40
|
+
leeway=CLOCK_SKEW_LEEWAY,
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
User: Are there tests for the expiry behavior?
|
|
45
|
+
|
|
46
|
+
Agent: [Bash: grep -n "expir\|leeway\|exp" tests/auth/test_token.py]
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
12: def make_token(exp_offset_seconds=300):
|
|
50
|
+
14: payload = {"sub": "user-1", "exp": now + timedelta(seconds=exp_offset_seconds)}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
No test covers the boundary case. I'll add one that creates a token with `exp` set 5 seconds in the past and confirms it is now accepted with leeway.
|
|
54
|
+
|
|
55
|
+
[Edit: tests/auth/test_token.py]
|
|
56
|
+
|
|
57
|
+
Added:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
def test_validates_token_within_leeway():
|
|
61
|
+
# Token expired 5 seconds ago; should pass with 10s leeway
|
|
62
|
+
payload = {"sub": "user-1", "exp": datetime.utcnow() - timedelta(seconds=5)}
|
|
63
|
+
token = jwt.encode(payload, SECRET, algorithm="HS256")
|
|
64
|
+
result = validate_token(token)
|
|
65
|
+
assert result["sub"] == "user-1"
|
|
66
|
+
|
|
67
|
+
def test_rejects_token_outside_leeway():
|
|
68
|
+
# Token expired 60 seconds ago; must fail even with leeway
|
|
69
|
+
payload = {"sub": "user-1", "exp": datetime.utcnow() - timedelta(seconds=60)}
|
|
70
|
+
token = jwt.encode(payload, SECRET, algorithm="HS256")
|
|
71
|
+
with pytest.raises(jwt.ExpiredSignatureError):
|
|
72
|
+
validate_token(token)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
User: Run the tests.
|
|
76
|
+
|
|
77
|
+
Agent: [Bash: pytest tests/auth/test_token.py -v]
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
tests/auth/test_token.py::test_validates_valid_token PASSED
|
|
81
|
+
tests/auth/test_token.py::test_rejects_expired_token PASSED
|
|
82
|
+
tests/auth/test_token.py::test_validates_token_within_leeway PASSED
|
|
83
|
+
tests/auth/test_token.py::test_rejects_token_outside_leeway PASSED
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
All green. The 10-second leeway resolves the clock-skew login failures.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
User: taskstream needs exponential backoff retry for failed webhook deliveries. Right now if a delivery fails it just drops the event. We need up to 5 retry attempts with exponential backoff, delays stored in Redis so retries survive restarts.
|
|
2
|
+
|
|
3
|
+
Agent: I'll design this before writing code. The approach: use a Redis sorted set where the score is the Unix timestamp of the next attempt. A background worker polls for jobs whose score is <= now, attempts delivery, and either marks the job done or reschedules it with a doubled delay. After 5 attempts the job is moved to a dead-letter key.
|
|
4
|
+
|
|
5
|
+
Does this fit the existing architecture? Let me check what's already in place.
|
|
6
|
+
|
|
7
|
+
[Read: src/delivery/deliver.ts]
|
|
8
|
+
[Read: src/worker/retry-worker.ts]
|
|
9
|
+
|
|
10
|
+
The deliver.ts module handles HTTP POST; retry-worker.ts exists but is a stub. Good starting point.
|
|
11
|
+
|
|
12
|
+
User: That architecture sounds right. Go ahead.
|
|
13
|
+
|
|
14
|
+
Agent: [Edit: src/delivery/retry-queue.ts] (new file)
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { createClient } from "redis";
|
|
18
|
+
|
|
19
|
+
const RETRY_KEY = "taskstream:retry";
|
|
20
|
+
const DEAD_KEY = "taskstream:dead";
|
|
21
|
+
const MAX_ATTEMPTS = 5;
|
|
22
|
+
const BASE_DELAY_MS = 5_000;
|
|
23
|
+
|
|
24
|
+
export async function enqueueRetry(
|
|
25
|
+
redis: ReturnType<typeof createClient>,
|
|
26
|
+
jobId: string,
|
|
27
|
+
attempt: number,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
if (attempt >= MAX_ATTEMPTS) {
|
|
30
|
+
await redis.lPush(DEAD_KEY, jobId);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
34
|
+
const score = Date.now() + delayMs;
|
|
35
|
+
await redis.zAdd(RETRY_KEY, { score, value: jobId });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function pollDueRetries(
|
|
39
|
+
redis: ReturnType<typeof createClient>,
|
|
40
|
+
): Promise<string[]> {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
return redis.zRangeByScore(RETRY_KEY, "-inf", now);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function removeFromQueue(
|
|
46
|
+
redis: ReturnType<typeof createClient>,
|
|
47
|
+
jobId: string,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
await redis.zRem(RETRY_KEY, jobId);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
[Edit: src/worker/retry-worker.ts]
|
|
54
|
+
|
|
55
|
+
Updated the stub to poll every 5 seconds, attempt delivery for each due job, and call enqueueRetry with incremented attempt count on failure or removeFromQueue on success.
|
|
56
|
+
|
|
57
|
+
User: What's the delay schedule for 5 attempts?
|
|
58
|
+
|
|
59
|
+
Agent: With BASE_DELAY_MS = 5000 and doubling per attempt:
|
|
60
|
+
- Attempt 0 -> 1: 5 seconds
|
|
61
|
+
- Attempt 1 -> 2: 10 seconds
|
|
62
|
+
- Attempt 2 -> 3: 20 seconds
|
|
63
|
+
- Attempt 3 -> 4: 40 seconds
|
|
64
|
+
- Attempt 4 -> 5: dead letter (no 6th attempt)
|
|
65
|
+
|
|
66
|
+
Total window before dead-letter: ~75 seconds. If you want longer, we can increase BASE_DELAY_MS or switch from 2x to a larger multiplier.
|
|
67
|
+
|
|
68
|
+
User: 75 seconds is fine for v1. What key format is used for job IDs?
|
|
69
|
+
|
|
70
|
+
Agent: [Read: src/jobs/job-id.ts]
|
|
71
|
+
|
|
72
|
+
Job IDs are UUIDs generated at event creation time, e.g. `evt_a3f7c290`. The retry queue stores these as ZADD values directly.
|
|
73
|
+
|
|
74
|
+
User: Add a unit test for the delay schedule.
|
|
75
|
+
|
|
76
|
+
Agent: [Write: tests/unit/delivery/retry-queue.test.ts]
|
|
77
|
+
|
|
78
|
+
Test covers: first enqueue sets correct score within 100ms tolerance, fifth attempt goes to dead letter, pollDueRetries returns jobs whose score is in the past, removeFromQueue removes only the target job.
|
|
79
|
+
|
|
80
|
+
User: Run it.
|
|
81
|
+
|
|
82
|
+
Agent: [Bash: npm test -- tests/unit/delivery/retry-queue.test.ts]
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
PASS tests/unit/delivery/retry-queue.test.ts
|
|
86
|
+
enqueueRetry
|
|
87
|
+
✓ schedules retry with exponential delay (12ms)
|
|
88
|
+
✓ moves to dead letter after MAX_ATTEMPTS (8ms)
|
|
89
|
+
pollDueRetries
|
|
90
|
+
✓ returns only due jobs (5ms)
|
|
91
|
+
removeFromQueue
|
|
92
|
+
✓ removes target job only (4ms)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
All passing.
|