clawmem 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -105,7 +105,7 @@ All other retrieval is handled by Tier 2 hooks. **Do NOT call MCP tools speculat
105
105
  | `kg_query` | Entity SPO triples with temporal validity. Entity facts, NOT causal "why". |
106
106
  | `session_log` | "last time" / "yesterday" / "what did we do". Do NOT use `query` for cross-session. |
107
107
  | `profile` | User profile (static facts + dynamic context). |
108
- | `memory_pin` | +0.3 composite boost. PROACTIVELY for constraints, architecture decisions, corrections. |
108
+ | `memory_pin` | Lifecycle retention + priority among relevance-equivalent results (+0.3 composite boost on composite surfaces; exact-tie precedence on raw vector routes). PROACTIVELY for constraints, architecture decisions, corrections. |
109
109
  | `memory_snooze` | PROACTIVELY when `<vault-context>` surfaces noise — snooze 30 days. |
110
110
  | `memory_forget` | Deactivate a memory by closest match. Sparingly — prefer snooze. |
111
111
  | `build_graphs` | Temporal backbone + semantic graph after bulk ingestion. NOT after every reindex. |
@@ -135,14 +135,14 @@ The pipeline autonomously generates lex/vec/hyde variants, fuses BM25 + vector v
135
135
 
136
136
  ## Composite scoring
137
137
 
138
- Applied automatically to all search results.
138
+ Applied on the composite surfaces: hooks, `query`, `search`, and `memory_retrieve`'s keyword/hybrid/causal/complex modes. **v0.22.0: MCP `vsearch` and `memory_retrieve` semantic/discovery rank non-recency queries by RAW cosine instead** (`scoreBasis: "vector-cosine"`; metadata breaks exact ties only; `minScore` filters raw with no default); recency-intent queries keep composite everywhere.
139
139
 
140
140
  ```
141
141
  compositeScore = (0.50·searchScore + 0.25·recencyScore + 0.25·confidenceScore) × qualityMultiplier × coActivationBoost
142
142
  ```
143
143
 
144
- - `qualityMultiplier = 0.7 + 0.6·qualityScore` (0.7× … 1.3×); `coActivationBoost` up to +15%; length-normalized (floor 30%); frequency boost capped +10%; **pinned docs +0.3 additive**.
145
- - **`query` tool (v0.13.0+):** non-recency queries use **0.70·search + 0.15·recency + 0.15·confidence**. `search`/`vsearch`/`memory_retrieve`/context-surfacing keep the 0.50/0.25/0.25 default.
144
+ - `qualityMultiplier = 0.7 + 0.6·qualityScore` (0.7× … 1.3×); `coActivationBoost` up to +15%; length-normalized (floor 30%); frequency boost capped +10%; **pinned docs +0.3 additive on composite surfaces** (raw vector routes: pin = exact-tie precedence only).
145
+ - **`query` tool (v0.13.0+):** non-recency queries use **0.70·search + 0.15·recency + 0.15·confidence**. `search`, `memory_retrieve`'s composite modes, and context-surfacing keep the 0.50/0.25/0.25 default. (`vsearch` + `memory_retrieve` semantic/discovery use RAW cosine for non-recency queries as of v0.22.0 — no composite weights at all.)
146
146
  - **Recency intent** ("latest"/"recent"/"last session") switches all to **0.10·search + 0.70·recency + 0.20·confidence**.
147
147
  - **Half-lives:** decision/deductive/preference/hub/antipattern = ∞ · project 120d · research 90d · problem/milestone/note 60d · conversation/progress 45d · handoff 30d (extend up to 3× for frequently-accessed).
148
148
 
@@ -162,7 +162,7 @@ compositeScore = (0.50·searchScore + 0.25·recencyScore + 0.25·confidenceScore
162
162
 
163
163
  ## Memory lifecycle (pin / snooze / forget — manual tools)
164
164
 
165
- - **`memory_pin`** (+0.3 boost, persistent surfacing) — PROACTIVELY when: user says "remember this"/"important"; an architecture/critical decision was just made; a user preference/constraint should persist. Do NOT pin routine/session-specific items.
165
+ - **`memory_pin`** (lifecycle retention + priority among relevance-equivalent results; +0.3 boost on composite surfaces, exact-tie precedence on raw vector routes) — PROACTIVELY when: user says "remember this"/"important"; an architecture/critical decision was just made; a user preference/constraint should persist. Do NOT pin routine/session-specific items.
166
166
  - **`memory_snooze`** — PROACTIVELY when a memory keeps surfacing but isn't relevant now, user says "not now"/"later", or content is time-boxed.
167
167
  - **`memory_forget`** — only when genuinely wrong or permanently obsolete. Prefer snooze for temporary suppression.
168
168
  - **Contradiction auto-resolution:** when `decision-extractor` detects a new decision contradicting an old one, the old one's confidence is lowered automatically — no manual action needed.
package/README.md CHANGED
@@ -543,7 +543,7 @@ Registered by `clawmem setup mcp`. Available to any MCP-compatible client.
543
543
  |---|---|
544
544
  | `memory_retrieve` | **Preferred entry point.** Auto-classifies query and routes to optimal backend (query, intent_search, session_log, find_similar, or query_plan). Use instead of manually choosing a search tool. |
545
545
  | `search` | BM25 keyword search — for exact terms, config names, error codes, filenames. Composite scoring + co-activation boost + compact mode. Collection filter supports comma-separated values. Prefer `memory_retrieve` for auto-routing. |
546
- | `vsearch` | Vector semantic search — for conceptual/fuzzy matching when exact keywords are unknown. Composite scoring + co-activation boost + compact mode. Collection filter supports comma-separated values. Prefer `memory_retrieve` for auto-routing. |
546
+ | `vsearch` | Vector semantic search — for conceptual/fuzzy matching when exact keywords are unknown. **v0.22.0: non-recency queries rank by RAW cosine (`scoreBasis: "vector-cosine"`; metadata breaks exact ties only; `minScore` filters raw with no default)**; recency-intent queries keep composite. Collection filter supports comma-separated values. Prefer `memory_retrieve` for auto-routing. |
547
547
  | `query` | Full hybrid pipeline (BM25 + vector + rerank) — general-purpose when query type is unclear. WRONG for "why" questions (use `intent_search`) or cross-session queries (use `session_log`). Prefer `memory_retrieve` for auto-routing. Intent hint, strong-signal bypass, chunk dedup, candidateLimit, MMR diversity, compact mode. |
548
548
  | `get` | Retrieve single document by path or docid |
549
549
  | `multi_get` | Retrieve multiple docs by glob or comma-separated list |
@@ -595,7 +595,7 @@ Registered by `clawmem setup mcp`. Available to any MCP-compatible client.
595
595
  | Tool | Description |
596
596
  |---|---|
597
597
  | `memory_forget` | Search → deactivate closest match (with audit trail) |
598
- | `memory_pin` | Pin a memory for +0.3 composite boost. USE PROACTIVELY when: user states a persistent constraint, makes an architecture decision, or corrects a misconception. Don't wait for curator — pin critical decisions immediately. |
598
+ | `memory_pin` | Pin a memory: lifecycle retention + priority among relevance-equivalent results (+0.3 composite boost on hook/`query`/`search` surfaces; exact-tie precedence on the raw vector routes). USE PROACTIVELY when: user states a persistent constraint, makes an architecture decision, or corrects a misconception. Don't wait for curator — pin critical decisions immediately. |
599
599
  | `memory_snooze` | Temporarily hide a memory from context surfacing until a date. USE PROACTIVELY when `<vault-context>` repeatedly surfaces irrelevant content — snooze for 30 days instead of ignoring it. |
600
600
  | `status` | Index health with content type distribution |
601
601
  | `reindex` | Trigger vault re-scan |
@@ -695,7 +695,7 @@ Content types are inferred from frontmatter or file path patterns. Half-lives ex
695
695
 
696
696
  **Frequency boost:** Documents with higher revision counts or duplicate counts get a durability signal: `freqSignal = (revisions - 1) × 2 + (duplicates - 1)`, `freqBoost = min(0.10, log1p(freqSignal) × 0.03)`. Revision count (content evolution) is weighted 2× vs duplicate count (ingest repetition). Capped at 10%.
697
697
 
698
- **Pin boost:** Pinned documents get +0.3 additive boost (capped at 1.0). Use `memory_pin` to pin critical memories.
698
+ **Pin boost:** Pin = lifecycle retention + priority among relevance-equivalent results. On composite surfaces pinned documents get a +0.3 additive boost (capped at 1.0); on the raw vector routes (v0.22.0) pin breaks exact raw-score ties only. Use `memory_pin` to pin critical memories.
699
699
 
700
700
  **Snooze:** Snoozed documents are filtered out of context surfacing until their snooze date. Use `memory_snooze` for temporary suppression.
701
701
 
@@ -791,7 +791,7 @@ Notes referenced by the agent during a session get boosted (`access_count++`). U
791
791
  | `CLAWMEM_RERANK_URL` | `http://localhost:8090` | Reranker server URL. Without it, falls to `node-llama-cpp` (if allowed). |
792
792
  | `CLAWMEM_RERANK_API_KEY` | (none) | Bearer token for an authenticated remote reranker endpoint. Independent of the embed and LLM keys. |
793
793
  | `CLAWMEM_NO_LOCAL_MODELS` | `false` | Block `node-llama-cpp` from auto-downloading GGUF models. Set `true` for remote-only setups where you want fail-fast on unreachable endpoints. |
794
- | `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS` | `false` | **v0.21.0.** When `true`, the MCP direct tools (`search`, `vsearch`, `memory_retrieve`) score non-recency queries with the retrieval-tuned `query`-tool weights. Ships off the eval evidence covered only the hybrid `query` pipeline. YAML equivalent: `retrieval.mcp_direct_tuned_weights` (env wins). |
794
+ | `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS` | (superseded) | **No effect since v0.22.0** the direct-pipeline eval this knob was gated on measured tuned weights at 1/19 hit@1; the direct vector routes rank by raw cosine instead. Still parsed for backward compatibility; setting it logs a once-per-process warning. |
795
795
  | `CLAWMEM_MERGE_SCORE_NORMAL` | `0.93` | **v0.7.1.** Phase 2 consolidation merge-safety threshold when candidate and existing anchors align. Merges above this normalized 3-gram cosine score are allowed. |
796
796
  | `CLAWMEM_MERGE_SCORE_STRICT` | `0.98` | **v0.7.1.** Strictest merge-safety threshold — fallback when anchor sets are ambiguous. |
797
797
  | `CLAWMEM_MERGE_GUARD_DRY_RUN` | `false` | **v0.7.1.** When `true`, Phase 2 merge-safety rejections are logged but not enforced — use for calibration before enabling the gate. |
package/SKILL.md CHANGED
@@ -92,7 +92,7 @@ All other retrieval is handled by Tier 2 hooks. **Do NOT call MCP tools speculat
92
92
  | `kg_query` | Entity SPO triples with temporal validity. Entity facts, NOT causal "why" (use `intent_search`). |
93
93
  | `session_log` | "last time" / "yesterday" / "what did we do". Do NOT use `query` for cross-session. |
94
94
  | `profile` | User profile (static facts + dynamic context). |
95
- | `memory_pin` | +0.3 composite boost. Use PROACTIVELY for constraints, architecture decisions, corrections. |
95
+ | `memory_pin` | Lifecycle retention + priority among relevance-equivalent results (+0.3 composite boost on composite surfaces; exact-tie precedence on raw vector routes). Use PROACTIVELY for constraints, architecture decisions, corrections. |
96
96
  | `memory_snooze` | Use PROACTIVELY when `<vault-context>` surfaces noise — snooze 30 days. |
97
97
  | `memory_forget` | Deactivate a memory by closest match. Sparingly — prefer snooze. |
98
98
  | `build_graphs` | Temporal backbone + semantic graph after bulk ingestion. NOT after every reindex. |
@@ -189,7 +189,7 @@ Query -> intent classification (WHY/WHEN/ENTITY/WHAT)
189
189
 
190
190
  ## Composite scoring (how ranking works)
191
191
 
192
- Applied automatically to all search results.
192
+ Applied on the composite surfaces: hooks, `query`, `search`, and `memory_retrieve`'s keyword/hybrid/causal/complex modes. **v0.22.0: MCP `vsearch` and `memory_retrieve` semantic/discovery rank non-recency queries by RAW cosine instead** (`scoreBasis: "vector-cosine"`; metadata breaks exact ties only; `minScore` filters raw with no default); recency-intent queries keep composite everywhere.
193
193
 
194
194
  ```
195
195
  compositeScore = (0.50·searchScore + 0.25·recencyScore + 0.25·confidenceScore) × qualityMultiplier × coActivationBoost
@@ -198,8 +198,8 @@ compositeScore = (0.50·searchScore + 0.25·recencyScore + 0.25·confidenceScore
198
198
  - `qualityMultiplier = 0.7 + 0.6·qualityScore` (0.7× penalty … 1.3× boost).
199
199
  - `coActivationBoost = 1 + min(coCount/10, 0.15)` (docs surfaced together get up to +15%).
200
200
  - Length normalization penalizes verbose entries (floor 30%); frequency boost capped at +10%.
201
- - **Pinned docs: +0.3 additive** (capped at 1.0).
202
- - **`query` tool (v0.13.0+):** non-recency queries use retrieval-tuned **0.70·search + 0.15·recency + 0.15·confidence**. `search`/`vsearch`/`memory_retrieve`/`context-surfacing` keep the 0.50/0.25/0.25 default.
201
+ - **Pinned docs: +0.3 additive on composite surfaces** (capped at 1.0); on the raw vector routes pin = exact-tie precedence only.
202
+ - **`query` tool (v0.13.0+):** non-recency queries use retrieval-tuned **0.70·search + 0.15·recency + 0.15·confidence**. `search`, `memory_retrieve`'s composite modes, and `context-surfacing` keep the 0.50/0.25/0.25 default. (`vsearch` + `memory_retrieve` semantic/discovery use RAW cosine for non-recency queries as of v0.22.0 — no composite weights at all.)
203
203
  - **Recency intent** ("latest"/"recent"/"last session") switches all to **0.10·search + 0.70·recency + 0.20·confidence**.
204
204
 
205
205
  **Content-type half-lives:** decision / deductive / preference / hub / antipattern = ∞ (never decay) · project 120d · research 90d · problem / milestone / note 60d · conversation / progress 45d · handoff 30d. Half-lives extend up to 3× for frequently-accessed memories. Attention decay: non-durable types (handoff, progress, conversation, note, project) lose 5% confidence/week without access; decision / deductive / preference / hub / research / antipattern are exempt.
@@ -210,7 +210,7 @@ compositeScore = (0.50·searchScore + 0.25·recencyScore + 0.25·confidenceScore
210
210
 
211
211
  ## Memory lifecycle (pin / snooze / forget — manual tools)
212
212
 
213
- - **`memory_pin`** (+0.3 boost, persistent surfacing) — PROACTIVELY when: user says "remember this"/"important"; an architecture/critical decision was just made; a user preference/constraint should persist across sessions. Do NOT pin routine/session-specific items.
213
+ - **`memory_pin`** (lifecycle retention + priority among relevance-equivalent results; +0.3 boost on composite surfaces, exact-tie precedence on raw vector routes) — PROACTIVELY when: user says "remember this"/"important"; an architecture/critical decision was just made; a user preference/constraint should persist across sessions. Do NOT pin routine/session-specific items.
214
214
  - **`memory_snooze`** — PROACTIVELY when a memory keeps surfacing but isn't relevant now, user says "not now"/"later", or content is time-boxed.
215
215
  - **`memory_forget`** — only when genuinely wrong or permanently obsolete. Prefer snooze for temporary suppression.
216
216
  - **Contradiction auto-resolution:** when `decision-extractor` detects a new decision contradicting an old one, the old one's confidence is lowered automatically — no manual action needed.
@@ -56,7 +56,7 @@ Each indexed file becomes a document with:
56
56
  | `content_type` | Auto-detected: decision, deductive, preference, note, handoff, conversation, progress, research, hub, antipattern, project, milestone, problem |
57
57
  | `quality_score` | 0.0-1.0 based on length, structure, headings, lists, decision keywords, frontmatter |
58
58
  | `confidence` | Starts at 0.5, adjusted by contradiction detection and feedback |
59
- | `pinned` | Manual boost flag (+0.3 composite score) |
59
+ | `pinned` | Lifecycle retention + priority among relevance-equivalent results (+0.3 on composite surfaces; exact-tie precedence on the raw vector routes) |
60
60
  | `snoozed_until` | Temporarily hidden from context surfacing |
61
61
 
62
62
  ## Fragments
@@ -1,6 +1,6 @@
1
1
  # Composite Scoring
2
2
 
3
- Every search result in ClawMem is scored using a composite formula that blends multiple signals beyond raw search relevance.
3
+ Composite scoring blends multiple signals beyond raw search relevance. **It applies to the hook pipeline (context surfacing), the `query` hybrid pipeline, `search`, and `memory_retrieve`'s keyword/hybrid/causal/complex modes.** Since v0.22.0 the direct vector routes — MCP `vsearch` and `memory_retrieve` semantic/discovery — do NOT use it for non-recency queries: they rank by raw vector cosine, with metadata (including pin) breaking exact score ties only, and report `scoreBasis: "vector-cosine"`. That split is measured, not aesthetic: on a judged set against the live vault, raw cosine ranked 16/19 targets #1 (MRR 0.912) while this composite ranked 1/19 (MRR 0.307) and pushed 14/19 correct answers below the old minScore floor — in the compressed-high similarity band of modern embedding models, every multiplier below is larger than the raw margins separating right answers from wrong ones. Recency-intent queries keep composite (the Recency column) on every route that had it.
4
4
 
5
5
  ## Formula
6
6
 
@@ -19,7 +19,7 @@ compositeScore = (w_search * searchScore + w_recency * recencyScore + w_confiden
19
19
 
20
20
  Recency intent is detected automatically when queries contain "latest", "recent", "last session", etc. — and **takes precedence over the `query`-tool weights**: a recency-phrased `query` call still uses the Recency-intent column.
21
21
 
22
- The **`query` tool** uses retrieval-tuned weights (search 0.70) derived from a held-out judged-relevance eval (v0.13.0) — more weight on topical relevance measurably improves graded NDCG@10 without demoting the newest-correct version of evolving docs. `search`, `vsearch`, `memory_retrieve`, and the context-surfacing hook **keep the Normal column by default**. Since v0.21.0 a config knob `retrieval.mcp_direct_tuned_weights: true` (or env `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS=true`)switches the three MCP direct tools' non-recency scoring to the `query`-tool column; it ships **off** because the eval evidence covered only the hybrid `query` pipeline, and the flip is gated on a direct-pipeline eval (see [configuration](../reference/configuration.md)).
22
+ The **`query` tool** uses retrieval-tuned weights (search 0.70) derived from a held-out judged-relevance eval (v0.13.0) — more weight on topical relevance measurably improves graded NDCG@10 without demoting the newest-correct version of evolving docs. `search`, the composite `memory_retrieve` modes, and the context-surfacing hook **keep the Normal column**. The v0.21.0 `retrieval.mcp_direct_tuned_weights` knob is **superseded as of v0.22.0 and has no effect** — the direct-pipeline eval it was gated on measured tuned weights at 1/19 hit@1, and the direct vector routes moved to raw ordering instead. The key is still parsed (a once-per-process warning is logged if set) so existing configs don't break.
23
23
 
24
24
  Threshold note: EOS-anchored last-token embedding models (e.g. zembed-1 served correctly) produce a **compressed-high similarity band** — unrelated pairs sit near ~0.4 rather than ~0.2. Relative ordering is what matters; treat absolute `minScore` cutoffs as model-dependent rather than universal.
25
25
 
@@ -92,13 +92,13 @@ coActivationBoost = 1 + min(coCount / 10, 0.15)
92
92
 
93
93
  Documents frequently surfaced together in the same session get up to 15% boost.
94
94
 
95
- **Where co-activation is applied depends on the caller.** MCP tools (`query`, `search`, `vsearch`) pass a co-activation function into `applyCompositeScoring()`, so the boost is part of the composite score used for ranking and `minScore` filtering. The context-surfacing hook does NOT pass co-activation into composite scoring — instead it applies a separate spreading-activation step *after* adaptive threshold filtering. This means the hook's threshold decisions are based on scores without co-activation, and co-activation only boosts results that already passed the threshold. This is intentional: it prevents relationship boosts from rescuing otherwise-weak results into the surfaced set.
95
+ **Where co-activation is applied depends on the caller.** The composite MCP surfaces (`query`, `search`, and `vsearch` on recency-intent queries only) pass a co-activation function into `applyCompositeScoring()`, so the boost is part of the composite score used for ranking and — on `vsearch` and `search`, the tools that expose `minScore` — for `minScore` filtering. On `vsearch`'s non-recency (raw) regime, co-activation contributes only to the exact-tie key, never to ranking or filtering. The context-surfacing hook does NOT pass co-activation into composite scoring — instead it applies a separate spreading-activation step *after* adaptive threshold filtering. This means the hook's threshold decisions are based on scores without co-activation, and co-activation only boosts results that already passed the threshold. This is intentional: it prevents relationship boosts from rescuing otherwise-weak results into the surfaced set.
96
96
 
97
97
  ## Additional modifiers
98
98
 
99
99
  ### Pin boost
100
100
 
101
- Pinned documents receive +0.3 additive boost (capped at 1.0 total).
101
+ On composite surfaces, pinned documents receive a +0.3 additive boost (capped at 1.0 total). Pin's contract is **lifecycle retention + prioritization among relevance-equivalent results** — on the raw vector routes (v0.22.0) it therefore breaks exact raw-score ties only and never lifts a document over a more relevant one.
102
102
 
103
103
  ### Length normalization
104
104
 
@@ -1,6 +1,6 @@
1
1
  # Upgrading ClawMem
2
2
 
3
- Guide for upgrading between released versions. Current: **v0.21.0**.
3
+ Guide for upgrading between released versions. Current: **v0.22.0**.
4
4
 
5
5
  ClawMem upgrades are designed to be drop-in: pull the new version, restart any long-lived processes, and the SQLite schema auto-migrates on first open. This guide documents per-version specifics for upgrades that have additional considerations beyond the quick path below.
6
6
 
@@ -59,6 +59,18 @@ docker compose up -d reranker # /v1/rerank on :8090
59
59
 
60
60
  ---
61
61
 
62
+ ## v0.22.0: raw-cosine ranking on the direct vector routes
63
+
64
+ No migration command, no schema change, no re-embed. Restart long-lived processes per the quick path.
65
+
66
+ **Behavior change — MCP `vsearch` and `memory_retrieve` semantic/discovery rank non-recency queries by RAW cosine.** Reported scores on those routes are raw cosine (`scoreBasis: "vector-cosine"`), not composite values — expect a different numeric scale (unrelated results sit near ~0.4–0.55 on compressed-band embedding models, not near 0). `vsearch`'s `minScore` filters that raw scale and no longer defaults to 0.3 — omitted means no filter. If a workflow passed `minScore` tuned to composite values, re-tune it to your embedding model's band or omit it. Recency-phrased queries ("latest…", "recently…") behave exactly as before on every route.
67
+
68
+ **Pinned documents** no longer float above more relevant results on those two routes — pin now means lifecycle retention plus winning exact-score ties. Hooks, `query`, and `search` keep the +0.3 composite pin boost.
69
+
70
+ **`retrieval.mcp_direct_tuned_weights` / `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS` no longer has any effect** (superseded by its own gating eval). Configs that set it keep working; a once-per-process warning is logged — remove the key at your convenience.
71
+
72
+ ---
73
+
62
74
  ## v0.21.0: MCP internal-collection exclusion + embedding-geometry canary
63
75
 
64
76
  No migration command required. Two things to know:
@@ -71,7 +83,7 @@ No migration command required. Two things to know:
71
83
 
72
84
  **The canary baseline seeds itself.** The first `clawmem embed` run after upgrade (including a timer-fired one) probes the embedding server with a pair-separation battery and persists a first-healthy baseline; subsequent runs alert relative to it, and a broken-geometry server now aborts `embed --force` BEFORE anything is cleared. `--force-geometry` (proceed despite a failed probe; the vault is flagged tainted until a verified rebuild) and `--force --recalibrate-canary` (replace the baseline after a deliberate model/server change) are operator overrides, not upgrade steps.
73
85
 
74
- **Opt-in knob:** `retrieval.mcp_direct_tuned_weights` (config) / `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS` (env), default `false` — scores the MCP direct tools' non-recency queries with the retrieval-tuned `query`-tool weights. Leave it off unless you have direct-pipeline eval evidence for your vault.
86
+ **Opt-in knob:** `retrieval.mcp_direct_tuned_weights` (config) / `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS` (env), default `false` — scored the MCP direct tools' non-recency queries with the retrieval-tuned `query`-tool weights. *(Superseded in v0.22.0 the knob no longer has any effect; see the v0.22.0 section above.)*
75
87
 
76
88
  Upgrades from v0.13 → v0.20 shipped no steps beyond the [quick path](#quick-path) — schema changes auto-apply; see [RELEASE_NOTES.md](../../RELEASE_NOTES.md) for what each version changed.
77
89
 
@@ -6,7 +6,7 @@ ClawMem is an open-source memory engine for Claude Code and AI agents. It runs o
6
6
 
7
7
  - **Indexes** markdown documents into a local SQLite vault with full-text search (BM25) and vector embeddings
8
8
  - **Retrieves** relevant context automatically via hooks or on-demand via MCP tools / REST API
9
- - **Scores** results using composite scoring (relevance, recency, confidence, quality, co-activation)
9
+ - **Scores** results per route: raw vector cosine on the direct vector tools (v0.22.0), composite scoring (relevance, recency, confidence, quality, co-activation) on the hook and hybrid pipelines
10
10
  - **Tracks** decisions, handoffs, and session history across conversations
11
11
  - **Traverses** causal and semantic graphs to answer "why" and "what led to" questions
12
12
 
@@ -38,7 +38,7 @@ Full provider matrix and behavior: [../guides/cloud-embedding.md](../guides/clou
38
38
  |---|---|---|
39
39
  | `CLAWMEM_PROFILE` | `balanced` | `speed` / `balanced` / `deep`. Sets the kept-score ratio (65% / 55% / 45%), vector timeout, max results, and the `factsTokens` sub-budget. Only `deep` adds query expansion + reranking to the hook path. `speed` makes hooks BM25-only (sub-500ms). |
40
40
  | `CLAWMEM_NUDGE_INTERVAL` | `15` | Prompts between lifecycle tool use before a `<vault-nudge>` is injected. `0` to disable. |
41
- | `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS` | `false` | When `true`, the MCP direct tools (`search`, `vsearch`, `memory_retrieve`) score non-recency queries with the retrieval-tuned `query`-tool weights (search 0.70) instead of the Normal weights. Ships **off**: the eval evidence covered only the hybrid `query` pipeline; flipping the default is gated on a direct-pipeline eval. YAML equivalent in `config.yaml`: `retrieval.mcp_direct_tuned_weights: true` (env wins). |
41
+ | `CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS` | (superseded) | **No effect since v0.22.0.** The direct-pipeline eval this knob was gated on measured tuned weights at 1/19 hit@1; the direct vector routes now rank by raw cosine instead (see [mcp-tools](mcp-tools.md) Scoring regimes). Still parsed for backward compatibility setting it (env or `retrieval.mcp_direct_tuned_weights` in `config.yaml`) logs a once-per-process warning. |
42
42
 
43
43
  The context-surfacing hook `timeout` is **not** an env var — it lives in `~/.claude/settings.json` (8s default). See [../troubleshooting.md](../troubleshooting.md) → *Tuning the context-surfacing hook timeout*.
44
44
 
@@ -6,6 +6,8 @@ Complete reference for ClawMem's MCP server tools. These let AI agents search, r
6
6
 
7
7
  **Internal-collection visibility (v0.21.0):** the retrieval tools `search`, `vsearch`, `query`, `query_plan`, `memory_retrieve`, and `find_similar` exclude the system-internal `_clawmem` collection (observations/deductions/handoffs) by default. Opt-ins: pass `includeInternal: true` (all six tools), or — on the tools that expose a `collection` parameter (`search`, `vsearch`, `query`) — name `_clawmem` explicitly in the filter. `find_similar` auto-includes internal results when the REFERENCE document is itself internal. `intent_search`, `find_causal_links`, `kg_query`, `session_log`, and `timeline` are NOT filtered — system memory is their substrate by design.
8
8
 
9
+ **Scoring regimes (v0.22.0):** the direct vector routes — `vsearch` and `memory_retrieve`'s semantic/discovery modes — rank non-recency queries by RAW vector cosine. Their `structuredContent` carries `scoreBasis: "vector-cosine"`; raw cosine values are specific to the embedding model that produced them and are NOT comparable across models or to composite scores. Document metadata — including pin — participates only inside groups of exactly-equal raw scores. On these routes `minScore` filters the raw score and has NO default (omitted = no filter; an explicit `0` is honored). Recency-intent queries ("latest…", "recently…", "yesterday…") keep the composite regime with its 0.3 default floor and report `scoreBasis: "composite"`, as do `search`, `query`, `query_plan`, and `memory_retrieve`'s keyword/hybrid/causal/complex modes. `find_similar` has always ranked by raw cosine. Rationale: on the measured vault, raw cosine ranked 16/19 judged targets #1 (MRR 0.912) while the composite stack ranked 1/19 and filtered 14/19 out entirely.
10
+
9
11
  **Degraded vector results (v0.21.0):** under default exclusion the vector scan escalates its depth to fill `limit` with allowed documents, up to a hard cap. When the cap prevents an exhaustive scan and the result is under-filled, `structuredContent` carries `degraded: true` with `degradedReason`: `"excluded-dominant"` (distinct excluded docs account for the shortfall — the guidance line suggests `includeInternal: true` or a refined query) or `"cap-truncation"` (shortfall driven by fragment dedup, neutral guidance). Multi-leg routes (`query`, `query_plan`, `memory_retrieve` complex mode) aggregate `degraded = any(leg)` and list per-leg reasons in `structuredContent.degradedLegs`; single-vector routes (`vsearch`, `find_similar`, `memory_retrieve`'s other modes) report the flat `degraded` + `degradedReason` pair. A small vault whose whole index is scanned without hitting the cap returns a plain short list with NO marker.
10
12
 
11
13
  ### memory_retrieve
@@ -60,12 +62,13 @@ BM25 only. Zero GPU cost.
60
62
 
61
63
  ### vsearch
62
64
 
63
- Vector only. Semantic similarity.
65
+ Vector only. Semantic similarity. Non-recency queries rank by RAW cosine (`scoreBasis: "vector-cosine"`, v0.22.0); recency-intent queries use the composite regime.
64
66
 
65
67
  | Param | Type | Default | Description |
66
68
  |-------|------|---------|-------------|
67
69
  | `query` | string | required | Search query |
68
70
  | `limit` | number | 10 | Max results |
71
+ | `minScore` | number | — | Raw-cosine floor on non-recency queries (no default — omitted means no filter; explicit `0` honored). Recency-intent queries keep the composite-scale 0.3 default. |
69
72
  | `compact` | boolean | true | Compact output |
70
73
  | `collection` | string | — | Filter by collection |
71
74
  | `includeInternal` | boolean | false | Include system-internal `_clawmem` docs |
@@ -178,7 +181,7 @@ Recent session history with handoffs and file changes.
178
181
 
179
182
  ### memory_pin
180
183
 
181
- Pin a memory for permanent prioritization (+0.3 composite boost).
184
+ Pin a memory: lifecycle retention plus prioritization among relevance-equivalent results. On composite surfaces (hooks, `query`, `search`) pinned docs get the +0.3 composite boost; on the raw vector routes (v0.22.0) pin wins exact raw-score ties but never overrides a relevance difference.
182
185
 
183
186
  | Param | Type | Default | Description |
184
187
  |-------|------|---------|-------------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmem",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "On-device memory layer for AI agents. Claude Code, OpenClaw, and Hermes. Hooks + MCP server + hybrid RAG search.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.ts CHANGED
@@ -54,11 +54,11 @@ export interface LifecyclePolicy {
54
54
 
55
55
  export interface RetrievalConfig {
56
56
  /**
57
- * Option-B knob (VSEARCH-TRUST-HARDENING (a)): when true, the MCP direct tools
58
- * (search / vsearch / memory_retrieve) score non-recency queries with the
59
- * retrieval-tuned QUERY_WEIGHTS instead of DEFAULT_WEIGHTS. Default FALSE
60
- * the flip is gated on a direct-pipeline eval (the n=199 evidence covered only
61
- * the hybrid `query` pipeline).
57
+ * SUPERSEDED as of v0.22.0 and no longer consumed anywhere: the direct-pipeline
58
+ * eval this knob was gated on measured tuned weights at 1/19 hit@1 (BACKLOG
59
+ * Source 48), and the direct vector routes now rank by raw similarity instead
60
+ * (VSEARCH-RAW-PRIMARY-DESIGN.md). Still parsed so existing configs don't break;
61
+ * setting it warns once per process.
62
62
  */
63
63
  mcp_direct_tuned_weights: boolean;
64
64
  }
@@ -126,6 +126,8 @@ export function getActiveProfile(): ProfileConfig {
126
126
  // ---------------------------------------------------------------------------
127
127
 
128
128
  let _cachedConfig: ClawMemConfig | null = null;
129
+ // Once-per-process compatibility warning for the superseded tuned-weights knob (v0.22.0).
130
+ let _warnedTunedWeightsKnob = false;
129
131
 
130
132
  /**
131
133
  * Load vault configuration from env vars and config file.
@@ -186,13 +188,18 @@ export function loadVaultConfig(): ClawMemConfig {
186
188
  };
187
189
  }
188
190
 
189
- // 4. Retrieval knobs (optional). Env override for tests / staged rollout.
191
+ // 4. Retrieval knobs (optional). Parsed for backward compatibility only
192
+ // mcp_direct_tuned_weights is superseded as of v0.22.0 and has no effect.
190
193
  let retrieval: RetrievalConfig | undefined;
191
194
  const envTuned = process.env.CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS;
192
- const yamlTuned = parsedYaml?.retrieval && typeof parsedYaml.retrieval === "object"
193
- ? parsedYaml.retrieval.mcp_direct_tuned_weights === true
194
- : false;
195
+ const yamlHasTuned = !!(parsedYaml?.retrieval && typeof parsedYaml.retrieval === "object"
196
+ && parsedYaml.retrieval.mcp_direct_tuned_weights !== undefined);
197
+ const yamlTuned = yamlHasTuned ? parsedYaml.retrieval.mcp_direct_tuned_weights === true : false;
195
198
  retrieval = { mcp_direct_tuned_weights: envTuned !== undefined ? envTuned === "true" : yamlTuned };
199
+ if ((envTuned !== undefined || yamlHasTuned) && !_warnedTunedWeightsKnob) {
200
+ _warnedTunedWeightsKnob = true;
201
+ console.warn("[clawmem] retrieval.mcp_direct_tuned_weights / CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS is superseded as of v0.22.0 and has no effect — the direct vector routes rank by raw similarity.");
202
+ }
196
203
 
197
204
  _cachedConfig = { vaults, lifecycle, retrieval };
198
205
  return _cachedConfig;
package/src/mcp.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  type CoActivationFn,
35
35
  } from "./memory.ts";
36
36
  import { enrichResults, reciprocalRankFusion, toRanked, blendRerank, type RankedResult } from "./search-utils.ts";
37
+ import { selectScoringRegime, rankRawPrimary, VECTOR_SCORE_BASIS, COMPOSITE_SCORE_BASIS } from "./scoring-regime.ts";
37
38
  import { applyMMRDiversity } from "./mmr.ts";
38
39
  import { indexCollection, type IndexStats } from "./indexer.ts";
39
40
  import { listCollections } from "./collections.ts";
@@ -140,12 +141,6 @@ function degradedGuidanceText(legs: DegradedLeg[]): string {
140
141
  : "Note: vector results truncated at the scan cap.";
141
142
  }
142
143
 
143
- // Option-B weights knob (design (a)): default-off; flips search/vsearch/memory_retrieve
144
- // non-recency scoring to the retrieval-tuned weights only when explicitly enabled.
145
- function mcpDirectWeightsOption(): { weights: typeof QUERY_WEIGHTS } | undefined {
146
- return loadVaultConfig().retrieval?.mcp_direct_tuned_weights ? { weights: QUERY_WEIGHTS } : undefined;
147
- }
148
-
149
144
  function classifyRetrievalMode(query: string): "keyword" | "semantic" | "causal" | "timeline" | "discovery" | "complex" | "hybrid" {
150
145
  const q = query.toLowerCase();
151
146
 
@@ -388,7 +383,7 @@ This is the recommended entry point for ALL memory queries.`,
388
383
  }
389
384
 
390
385
  const enriched = enrichResults(store, fused, query);
391
- const scored = applyCompositeScoring(enriched, query, undefined, mcpDirectWeightsOption()).slice(0, lim);
386
+ const scored = applyCompositeScoring(enriched, query).slice(0, lim);
392
387
  const items = scored.map(r => ({
393
388
  docid: `#${r.docid}`, path: r.displayPath, title: r.title,
394
389
  score: Math.round(r.compositeScore * 100) / 100,
@@ -426,7 +421,7 @@ This is the recommended entry point for ALL memory queries.`,
426
421
  const seen = new Set<string>();
427
422
  const deduped = allResults.filter(r => { if (seen.has(r.filepath)) return false; seen.add(r.filepath); return true; });
428
423
  const enriched = enrichResults(store, deduped, query);
429
- const scored = applyCompositeScoring(enriched, query, undefined, mcpDirectWeightsOption()).slice(0, lim);
424
+ const scored = applyCompositeScoring(enriched, query).slice(0, lim);
430
425
  const items = scored.map(r => ({
431
426
  docid: `#${r.docid}`, path: r.displayPath, title: r.title,
432
427
  score: Math.round(r.compositeScore * 100) / 100,
@@ -442,12 +437,16 @@ This is the recommended entry point for ALL memory queries.`,
442
437
  // --- Keyword / Semantic / Discovery / Hybrid modes ---
443
438
  let results: SearchResult[] = [];
444
439
  let singleLegDegraded: DegradedLeg | undefined;
440
+ // semantic/discovery: the raw regime applies ONLY to results the vector leg actually
441
+ // served — the FTS fallback's scores are not cosine, so it keeps composite scoring.
442
+ let vectorLegServed = false;
445
443
  if (effectiveMode === "keyword") {
446
444
  results = store.searchFTS(query, lim, undefined, undefined, undefined, excl);
447
445
  } else if (effectiveMode === "semantic" || effectiveMode === "discovery") {
448
446
  try {
449
447
  const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, lim, { excludeCollections: excl });
450
448
  results = det.results;
449
+ vectorLegServed = true;
451
450
  if (det.degraded && det.degradedReason) singleLegDegraded = { leg: `${effectiveMode}:vector`, reason: det.degradedReason };
452
451
  } catch (e) { rethrowIfFatalVectorError(e); results = store.searchFTS(query, lim, undefined, undefined, undefined, excl); }
453
452
  } else {
@@ -472,7 +471,17 @@ This is the recommended entry point for ALL memory queries.`,
472
471
  }
473
472
 
474
473
  const enriched = enrichResults(store, results, query);
475
- const scored = applyCompositeScoring(enriched, query, undefined, mcpDirectWeightsOption()).slice(0, lim);
474
+ // v0.22.0 two-regime scoring (VSEARCH-RAW-PRIMARY-DESIGN.md R1/R4): vector-served
475
+ // semantic/discovery results on non-recency queries rank by raw cosine (metadata
476
+ // breaks exact ties only). Keyword/hybrid modes and the FTS fallback keep composite —
477
+ // their scores are not cosine, so the raw contract cannot hold there.
478
+ const rawRegime = vectorLegServed
479
+ && (effectiveMode === "semantic" || effectiveMode === "discovery")
480
+ && selectScoringRegime(query) === "raw";
481
+ const retrieveScoreBasis = rawRegime ? VECTOR_SCORE_BASIS : COMPOSITE_SCORE_BASIS;
482
+ const scored = rawRegime
483
+ ? rankRawPrimary(enriched, query).slice(0, lim)
484
+ : applyCompositeScoring(enriched, query).slice(0, lim);
476
485
  const modeDegraded = singleLegDegraded ? { degraded: true as const, degradedReason: singleLegDegraded.reason } : {};
477
486
  const modeDegradedNote = singleLegDegraded ? `\n${degradedGuidanceText([singleLegDegraded])}` : "";
478
487
  if (compact) {
@@ -483,7 +492,7 @@ This is the recommended entry point for ALL memory queries.`,
483
492
  }));
484
493
  return {
485
494
  content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${modeDegradedNote}` }],
486
- structuredContent: { mode: effectiveMode, results: items, ...modeDegraded },
495
+ structuredContent: { mode: effectiveMode, results: items, scoreBasis: retrieveScoreBasis, ...modeDegraded },
487
496
  };
488
497
  }
489
498
  const items: SearchResultItem[] = scored.map(r => {
@@ -497,7 +506,7 @@ This is the recommended entry point for ALL memory queries.`,
497
506
  });
498
507
  return {
499
508
  content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items, query)}${modeDegradedNote}` }],
500
- structuredContent: { mode: effectiveMode, results: items, ...modeDegraded },
509
+ structuredContent: { mode: effectiveMode, results: items, scoreBasis: retrieveScoreBasis, ...modeDegraded },
501
510
  };
502
511
  }
503
512
  );
@@ -585,7 +594,7 @@ This is the recommended entry point for ALL memory queries.`,
585
594
 
586
595
  const coFn = (path: string) => store.getCoActivated(path);
587
596
  const enriched = enrichResults(store, results, query);
588
- const scored = applyCompositeScoring(enriched, query, coFn, mcpDirectWeightsOption())
597
+ const scored = applyCompositeScoring(enriched, query, coFn)
589
598
  .filter(r => r.compositeScore >= (minScore || 0));
590
599
 
591
600
  if (compact) {
@@ -631,7 +640,7 @@ This is the recommended entry point for ALL memory queries.`,
631
640
  inputSchema: {
632
641
  query: z.string().describe("Natural language query"),
633
642
  limit: z.number().optional().default(10),
634
- minScore: z.number().optional().default(0.3),
643
+ minScore: z.number().optional().describe("Score floor. Non-recency queries filter the RAW cosine score (default: no filter; explicit 0 honored; cosine values are embedding-model-specific). Recency-intent queries keep the composite-scale default 0.3."),
635
644
  collection: z.string().optional().describe("Filter to collection (single name or comma-separated)"),
636
645
  compact: z.boolean().optional().default(false).describe("Return compact results (id, path, title, score, snippet) instead of full content"),
637
646
  includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
@@ -656,8 +665,17 @@ This is the recommended entry point for ALL memory queries.`,
656
665
 
657
666
  const coFn = (path: string) => store.getCoActivated(path);
658
667
  const enriched = enrichResults(store, results, query);
659
- const scored = applyCompositeScoring(enriched, query, coFn, mcpDirectWeightsOption())
660
- .filter(r => r.compositeScore >= (minScore || 0.3));
668
+ // v0.22.0 two-regime scoring (VSEARCH-RAW-PRIMARY-DESIGN.md R1–R4): non-recency
669
+ // queries rank by raw cosine — metadata (incl. pin) breaks exact score ties only,
670
+ // and minScore (if given) filters the raw score with NO default floor. Recency-intent
671
+ // queries keep the pre-v0.22.0 composite behavior including its 0.3 default.
672
+ const regime = selectScoringRegime(query);
673
+ const vsScoreBasis = regime === "raw" ? VECTOR_SCORE_BASIS : COMPOSITE_SCORE_BASIS;
674
+ const scored = regime === "raw"
675
+ ? rankRawPrimary(enriched, query, coFn).filter(r => minScore === undefined || r.compositeScore >= minScore)
676
+ // Recency branch preserves v0.21 semantics EXACTLY, including `||`: an explicit
677
+ // minScore of 0 still applies the 0.3 composite floor (R4 unchanged-recency contract).
678
+ : applyCompositeScoring(enriched, query, coFn).filter(r => r.compositeScore >= (minScore || 0.3));
661
679
 
662
680
  if (compact) {
663
681
  const items = scored.map(r => ({
@@ -666,7 +684,7 @@ This is the recommended entry point for ALL memory queries.`,
666
684
  snippet: (r.body || "").substring(0, 150), content_type: r.contentType, modified_at: r.modifiedAt,
667
685
  fragment: r.fragmentType ? { type: r.fragmentType, label: r.fragmentLabel } : undefined,
668
686
  }));
669
- return { content: [{ type: "text", text: `${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${vsDegradedNote}` }], structuredContent: { results: items, ...vsDegraded } };
687
+ return { content: [{ type: "text", text: `${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${vsDegradedNote}` }], structuredContent: { results: items, scoreBasis: vsScoreBasis, ...vsDegraded } };
670
688
  }
671
689
 
672
690
  const items: SearchResultItem[] = scored.map(r => {
@@ -685,7 +703,7 @@ This is the recommended entry point for ALL memory queries.`,
685
703
 
686
704
  return {
687
705
  content: [{ type: "text", text: `${formatSearchSummary(items, query)}${vsDegradedNote}` }],
688
- structuredContent: { results: items, ...vsDegraded },
706
+ structuredContent: { results: items, scoreBasis: vsScoreBasis, ...vsDegraded },
689
707
  };
690
708
  }
691
709
  );
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Centralized scoring-regime selection for the MCP direct vector routes (v0.22.0).
3
+ *
4
+ * Two regimes:
5
+ *
6
+ * "raw" — non-recency queries on the evidenced raw routes (`vsearch`,
7
+ * `memory_retrieve` semantic/discovery): results rank by RAW
8
+ * vector cosine DESC. Document metadata — including pin —
9
+ * participates ONLY inside groups of exactly-equal raw scores.
10
+ * The reported score IS the raw cosine (scoreBasis
11
+ * "vector-cosine"): specific to the embedding model that
12
+ * produced it, and not comparable to composite scores.
13
+ *
14
+ * "recency-composite" — hasRecencyIntent(query): the pre-v0.22.0 composite behavior,
15
+ * unchanged (RECENCY_WEIGHTS blend, multipliers, pin boost,
16
+ * contentType priority sort, composite-scale minScore default).
17
+ *
18
+ * Measured basis (BACKLOG Source 48): raw cosine ranked 16/19 judged targets #1
19
+ * (MRR 0.912) where the composite stack ranked 1/19 (MRR 0.307) and filtered 14/19
20
+ * below the old composite minScore — every metadata signal large enough to matter is
21
+ * larger than the 0.03–0.10 raw margins that separate right answers from wrong ones,
22
+ * so metadata may only ever break exact raw-score ties on these routes.
23
+ */
24
+ import {
25
+ applyCompositeScoring,
26
+ hasRecencyIntent,
27
+ type EnrichedResult,
28
+ type ScoredResult,
29
+ type CoActivationFn,
30
+ type CompositeScoringOptions,
31
+ } from "./memory.ts";
32
+
33
+ /** Reported score basis for raw-regime results: raw vector cosine. */
34
+ export const VECTOR_SCORE_BASIS = "vector-cosine" as const;
35
+ /** Reported score basis for composite-scored results. */
36
+ export const COMPOSITE_SCORE_BASIS = "composite" as const;
37
+
38
+ export type ScoringRegime = "raw" | "recency-composite";
39
+
40
+ /** One switch point for the raw-route regime decision (design R4). */
41
+ export function selectScoringRegime(query: string): ScoringRegime {
42
+ return hasRecencyIntent(query) ? "recency-composite" : "raw";
43
+ }
44
+
45
+ /**
46
+ * Raw-primary ranking (design R1/R2): raw score DESC; within a group of exactly-equal
47
+ * raw scores the deterministic tie order is pinned DESC, then legacy composite DESC,
48
+ * then displayPath ASC. `compositeScore` on the returned rows carries the RAW score —
49
+ * the reported score is the raw cosine.
50
+ *
51
+ * Callers gate on selectScoringRegime() first — this ranker is only meaningful for
52
+ * non-recency queries (the legacy composite computed here for tie keys therefore never
53
+ * takes the RECENCY_WEIGHTS branch). Inputs are document-unique on these routes
54
+ * (searchVecDetailed hydrates one row per document).
55
+ *
56
+ * `options.now` flows to the tie-key composite so frozen-clock evaluation (the v0.22.0
57
+ * acceptance bundle) is bit-reproducible.
58
+ */
59
+ export function rankRawPrimary(
60
+ results: EnrichedResult[],
61
+ query: string,
62
+ coActivationFn?: CoActivationFn,
63
+ options?: CompositeScoringOptions
64
+ ): ScoredResult[] {
65
+ const legacy = applyCompositeScoring(results, query, coActivationFn, options);
66
+ const legacyByPath = new Map(legacy.map(s => [s.filepath, s]));
67
+ const ranked = results.map(r => {
68
+ const l = legacyByPath.get(r.filepath) ?? ({ ...r, compositeScore: r.score, recencyScore: 0 } as ScoredResult);
69
+ return { row: { ...l, compositeScore: r.score } as ScoredResult, legacyComposite: l.compositeScore };
70
+ });
71
+ ranked.sort((a, b) => {
72
+ if (b.row.compositeScore !== a.row.compositeScore) return b.row.compositeScore - a.row.compositeScore;
73
+ const pinDelta = (b.row.pinned ? 1 : 0) - (a.row.pinned ? 1 : 0);
74
+ if (pinDelta !== 0) return pinDelta;
75
+ if (b.legacyComposite !== a.legacyComposite) return b.legacyComposite - a.legacyComposite;
76
+ return a.row.displayPath < b.row.displayPath ? -1 : a.row.displayPath > b.row.displayPath ? 1 : 0;
77
+ });
78
+ return ranked.map(r => r.row);
79
+ }