clawmem 0.11.2 → 0.12.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 +12 -12
- package/CLAUDE.md +12 -12
- package/README.md +18 -16
- package/SKILL.md +8 -9
- package/extras/rerankers/zerank-2-seq/Dockerfile +26 -0
- package/extras/rerankers/zerank-2-seq/README.md +107 -0
- package/extras/rerankers/zerank-2-seq/build_and_verify.py +177 -0
- package/extras/rerankers/zerank-2-seq/docker-compose.yml +49 -0
- package/extras/rerankers/zerank-2-seq/server.py +49 -0
- package/extras/rerankers/zerank-2-seq/zr_common.py +43 -0
- package/package.json +2 -1
- package/src/mcp.ts +14 -12
- package/src/search-utils.ts +45 -0
package/AGENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Inference Services
|
|
4
4
|
|
|
5
|
-
ClawMem uses three `llama-server` instances for
|
|
5
|
+
ClawMem uses three inference services (embedding, LLM, reranker). The **default** stack runs all three as `llama-server` instances; the **SOTA** stack swaps the reranker for a transformers sidecar (same `/v1/rerank` contract). The `bin/clawmem` wrapper points at `localhost:8088/8089/8090`.
|
|
6
6
|
|
|
7
7
|
**Default (QMD native combo, any GPU or in-process):**
|
|
8
8
|
|
|
@@ -14,15 +14,15 @@ ClawMem uses three `llama-server` instances for neural inference. By default, th
|
|
|
14
14
|
|
|
15
15
|
All three models auto-download via `node-llama-cpp` if no server is running (Metal on Apple Silicon, Vulkan where available, CPU as last resort). Fast with GPU acceleration (Metal/Vulkan); significantly slower on CPU-only.
|
|
16
16
|
|
|
17
|
-
**SOTA upgrade (
|
|
17
|
+
**SOTA upgrade (16GB+ GPU):** CC-BY-NC-4.0 — non-commercial only.
|
|
18
18
|
|
|
19
19
|
| Service | Port | Model | VRAM | Protocol |
|
|
20
20
|
|---|---|---|---|---|
|
|
21
21
|
| Embedding | 8088 | zembed-1-Q4_K_M | ~4.4GB | `/v1/embeddings` |
|
|
22
22
|
| LLM | 8089 | qmd-query-expansion-1.7B-q4_k_m | ~2.2GB | `/v1/chat/completions` |
|
|
23
|
-
| Reranker | 8090 | zerank-2-
|
|
23
|
+
| Reranker | 8090 | zerank-2 seq-cls sidecar | ~9GB (bf16) | `/v1/rerank` |
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
zembed-1 (2560d, 32K context, SOTA retrieval) distilled from zerank-2 via zELO — optimal pairing. The SOTA reranker is the **zerank-2 seq-cls sidecar** at `extras/rerankers/zerank-2-seq/` (transformers, bf16, ~9GB VRAM) — **not** a GGUF. The older `zerank-2-Q4_K_M` GGUF is **deprecated**: llama.cpp drops zerank's score head, so its scores are near-zero and uninformative (final ordering stays RRF-dominated). Full SOTA stack ≈ 16GB VRAM.
|
|
26
26
|
|
|
27
27
|
**Remote option:** Set `CLAWMEM_EMBED_URL`, `CLAWMEM_LLM_URL`, `CLAWMEM_RERANK_URL` to the remote host. Set `CLAWMEM_NO_LOCAL_MODELS=true` to prevent surprise fallback downloads.
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ Total ~10GB VRAM. zembed-1 (2560d, 32K context, SOTA retrieval) distilled from z
|
|
|
36
36
|
|---|---|---|---|
|
|
37
37
|
| Embedding | [EmbeddingGemma-300M-Q8_0](https://huggingface.co/ggml-org/embeddinggemma-300M-GGUF) (314MB, 768d) | [zembed-1-Q4_K_M](https://huggingface.co/Abhiray/zembed-1-Q4_K_M-GGUF) (2.4GB, 2560d) | zembed-1: 32K context, SOTA retrieval. `-ub` must match `-b`. |
|
|
38
38
|
| LLM | [qmd-query-expansion-1.7B-q4_k_m](https://huggingface.co/tobil/qmd-query-expansion-1.7B-gguf) (~1.1GB) | Same | QMD's Qwen3-1.7B finetune for query expansion. |
|
|
39
|
-
| Reranker | [qwen3-reranker-0.6B-Q8_0](https://huggingface.co/ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF) (~600MB) | [zerank-2-
|
|
39
|
+
| Reranker | [qwen3-reranker-0.6B-Q8_0](https://huggingface.co/ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF) (~600MB) | [zerank-2 seq-cls sidecar](extras/rerankers/zerank-2-seq/) (bf16, ~9GB) | zerank-2: NDCG@10 ahead of Cohere rerank-3.5. Served via transformers sidecar, **not** GGUF (the GGUF drops the score head → inert). CC-BY-NC. |
|
|
40
40
|
|
|
41
41
|
### Server Setup (all three use llama-server)
|
|
42
42
|
|
|
@@ -55,15 +55,15 @@ llama-server -m qmd-query-expansion-1.7B-q4_k_m.gguf \
|
|
|
55
55
|
llama-server -m Qwen3-Reranker-0.6B-Q8_0.gguf \
|
|
56
56
|
--reranking --port 8090 --host 0.0.0.0 -ngl 99 -c 2048 --batch-size 512
|
|
57
57
|
|
|
58
|
-
# === SOTA upgrade (
|
|
58
|
+
# === SOTA upgrade (16GB+ GPU) ===
|
|
59
59
|
|
|
60
|
-
# Embedding
|
|
60
|
+
# Embedding (zembed-1) — -ub must match -b for non-causal attention
|
|
61
61
|
llama-server -m zembed-1-Q4_K_M.gguf \
|
|
62
62
|
--embeddings --port 8088 --host 0.0.0.0 -ngl 99 -c 8192 -b 2048 -ub 2048
|
|
63
63
|
|
|
64
|
-
# Reranker
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
# Reranker (zerank-2) — seq-cls SIDECAR (transformers, bf16), NOT a llama-server GGUF:
|
|
65
|
+
# cd extras/rerankers/zerank-2-seq && docker compose run --rm convert && docker compose up -d reranker
|
|
66
|
+
# The zerank-2 GGUF is deprecated: llama.cpp drops its score head -> near-zero, uninformative scores.
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
### Verify Endpoints
|
|
@@ -553,7 +553,7 @@ User Query + optional intent hint
|
|
|
553
553
|
→ Reciprocal Rank Fusion (k=60, top candidateLimit, up to 4-way parallel legs)
|
|
554
554
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
555
555
|
→ Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
556
|
-
→
|
|
556
|
+
→ Rerank/RRF Blend (blendRerank: 0.9·reranker + 0.1·normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
557
557
|
→ SAME Composite Scoring
|
|
558
558
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted, not removed)
|
|
559
559
|
```
|
|
@@ -734,7 +734,7 @@ clawmem focus clear --session-id abc123
|
|
|
734
734
|
- **Entity resolution (v0.2.0+):** A-MEM enrichment extracts named entities via LLM, resolves to canonical forms using FTS5 + Levenshtein fuzzy matching with **type-agnostic compatibility buckets** (person, org, location stay separate; project/service/tool/concept merge freely as "tech" bucket). Quality filters reject title-as-entity, long names, template placeholders, and invalid locations. Entity edges use IDF-based specificity scoring (rare entities create edges; ubiquitous entities alone cannot). See `docs/internals/entity-resolution.md` for customization (extending type vocabulary and buckets).
|
|
735
735
|
- QMD retrieval (BM25, vector, RRF, rerank, query expansion) is forked into ClawMem. Do not call standalone QMD tools.
|
|
736
736
|
- SAME (composite scoring), MAGMA (intent + graph), A-MEM (self-evolving notes) layer on top of QMD substrate.
|
|
737
|
-
- Three
|
|
737
|
+
- Three inference services (embedding, LLM, reranker) on local or remote GPU — `llama-server` for all three in the default stack; the SOTA stack serves the reranker as a transformers sidecar. Wrapper defaults to `localhost:8088/8089/8090`.
|
|
738
738
|
- `CLAWMEM_NO_LOCAL_MODELS=false` (default) allows in-process LLM/reranker fallback via `node-llama-cpp`. Set `true` for remote-only setups to fail fast on unreachable endpoints.
|
|
739
739
|
- Consolidation worker (`CLAWMEM_ENABLE_CONSOLIDATION=true`) backfills unenriched docs with A-MEM notes + links. Only runs if the MCP process stays alive long enough to tick (every 5min).
|
|
740
740
|
- Beads integration: `syncBeadsIssues()` queries `bd` CLI (Dolt backend, v0.58.0+) for live issue data, creates markdown docs in `beads` collection, maps all dependency edge types into `memory_relations`, and triggers A-MEM enrichment for new docs. Watcher auto-triggers on `.beads/` directory changes; `beads_sync` MCP tool for manual sync. Requires `bd` binary on PATH or at `~/go/bin/bd`.
|
package/CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Inference Services
|
|
4
4
|
|
|
5
|
-
ClawMem uses three `llama-server` instances for
|
|
5
|
+
ClawMem uses three inference services (embedding, LLM, reranker). The **default** stack runs all three as `llama-server` instances; the **SOTA** stack swaps the reranker for a transformers sidecar (same `/v1/rerank` contract). The `bin/clawmem` wrapper points at `localhost:8088/8089/8090`.
|
|
6
6
|
|
|
7
7
|
**Default (QMD native combo, any GPU or in-process):**
|
|
8
8
|
|
|
@@ -14,15 +14,15 @@ ClawMem uses three `llama-server` instances for neural inference. By default, th
|
|
|
14
14
|
|
|
15
15
|
All three models auto-download via `node-llama-cpp` if no server is running (Metal on Apple Silicon, Vulkan where available, CPU as last resort). Fast with GPU acceleration (Metal/Vulkan); significantly slower on CPU-only.
|
|
16
16
|
|
|
17
|
-
**SOTA upgrade (
|
|
17
|
+
**SOTA upgrade (16GB+ GPU):** CC-BY-NC-4.0 — non-commercial only.
|
|
18
18
|
|
|
19
19
|
| Service | Port | Model | VRAM | Protocol |
|
|
20
20
|
|---|---|---|---|---|
|
|
21
21
|
| Embedding | 8088 | zembed-1-Q4_K_M | ~4.4GB | `/v1/embeddings` |
|
|
22
22
|
| LLM | 8089 | qmd-query-expansion-1.7B-q4_k_m | ~2.2GB | `/v1/chat/completions` |
|
|
23
|
-
| Reranker | 8090 | zerank-2-
|
|
23
|
+
| Reranker | 8090 | zerank-2 seq-cls sidecar | ~9GB (bf16) | `/v1/rerank` |
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
zembed-1 (2560d, 32K context, SOTA retrieval) distilled from zerank-2 via zELO — optimal pairing. The SOTA reranker is the **zerank-2 seq-cls sidecar** at `extras/rerankers/zerank-2-seq/` (transformers, bf16, ~9GB VRAM) — **not** a GGUF. The older `zerank-2-Q4_K_M` GGUF is **deprecated**: llama.cpp drops zerank's score head, so its scores are near-zero and uninformative (final ordering stays RRF-dominated). Full SOTA stack ≈ 16GB VRAM.
|
|
26
26
|
|
|
27
27
|
**Remote option:** Set `CLAWMEM_EMBED_URL`, `CLAWMEM_LLM_URL`, `CLAWMEM_RERANK_URL` to the remote host. Set `CLAWMEM_NO_LOCAL_MODELS=true` to prevent surprise fallback downloads.
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ Total ~10GB VRAM. zembed-1 (2560d, 32K context, SOTA retrieval) distilled from z
|
|
|
36
36
|
|---|---|---|---|
|
|
37
37
|
| Embedding | [EmbeddingGemma-300M-Q8_0](https://huggingface.co/ggml-org/embeddinggemma-300M-GGUF) (314MB, 768d) | [zembed-1-Q4_K_M](https://huggingface.co/Abhiray/zembed-1-Q4_K_M-GGUF) (2.4GB, 2560d) | zembed-1: 32K context, SOTA retrieval. `-ub` must match `-b`. |
|
|
38
38
|
| LLM | [qmd-query-expansion-1.7B-q4_k_m](https://huggingface.co/tobil/qmd-query-expansion-1.7B-gguf) (~1.1GB) | Same | QMD's Qwen3-1.7B finetune for query expansion. |
|
|
39
|
-
| Reranker | [qwen3-reranker-0.6B-Q8_0](https://huggingface.co/ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF) (~600MB) | [zerank-2-
|
|
39
|
+
| Reranker | [qwen3-reranker-0.6B-Q8_0](https://huggingface.co/ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF) (~600MB) | [zerank-2 seq-cls sidecar](extras/rerankers/zerank-2-seq/) (bf16, ~9GB) | zerank-2: NDCG@10 ahead of Cohere rerank-3.5. Served via transformers sidecar, **not** GGUF (the GGUF drops the score head → inert). CC-BY-NC. |
|
|
40
40
|
|
|
41
41
|
### Server Setup (all three use llama-server)
|
|
42
42
|
|
|
@@ -55,15 +55,15 @@ llama-server -m qmd-query-expansion-1.7B-q4_k_m.gguf \
|
|
|
55
55
|
llama-server -m Qwen3-Reranker-0.6B-Q8_0.gguf \
|
|
56
56
|
--reranking --port 8090 --host 0.0.0.0 -ngl 99 -c 2048 --batch-size 512
|
|
57
57
|
|
|
58
|
-
# === SOTA upgrade (
|
|
58
|
+
# === SOTA upgrade (16GB+ GPU) ===
|
|
59
59
|
|
|
60
|
-
# Embedding
|
|
60
|
+
# Embedding (zembed-1) — -ub must match -b for non-causal attention
|
|
61
61
|
llama-server -m zembed-1-Q4_K_M.gguf \
|
|
62
62
|
--embeddings --port 8088 --host 0.0.0.0 -ngl 99 -c 8192 -b 2048 -ub 2048
|
|
63
63
|
|
|
64
|
-
# Reranker
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
# Reranker (zerank-2) — seq-cls SIDECAR (transformers, bf16), NOT a llama-server GGUF:
|
|
65
|
+
# cd extras/rerankers/zerank-2-seq && docker compose run --rm convert && docker compose up -d reranker
|
|
66
|
+
# The zerank-2 GGUF is deprecated: llama.cpp drops its score head -> near-zero, uninformative scores.
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
### Verify Endpoints
|
|
@@ -553,7 +553,7 @@ User Query + optional intent hint
|
|
|
553
553
|
→ Reciprocal Rank Fusion (k=60, top candidateLimit, up to 4-way parallel legs)
|
|
554
554
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
555
555
|
→ Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
556
|
-
→
|
|
556
|
+
→ Rerank/RRF Blend (blendRerank: 0.9·reranker + 0.1·normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
557
557
|
→ SAME Composite Scoring
|
|
558
558
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted, not removed)
|
|
559
559
|
```
|
|
@@ -734,7 +734,7 @@ clawmem focus clear --session-id abc123
|
|
|
734
734
|
- **Entity resolution (v0.2.0+):** A-MEM enrichment extracts named entities via LLM, resolves to canonical forms using FTS5 + Levenshtein fuzzy matching with **type-agnostic compatibility buckets** (person, org, location stay separate; project/service/tool/concept merge freely as "tech" bucket). Quality filters reject title-as-entity, long names, template placeholders, and invalid locations. Entity edges use IDF-based specificity scoring (rare entities create edges; ubiquitous entities alone cannot). See `docs/internals/entity-resolution.md` for customization (extending type vocabulary and buckets).
|
|
735
735
|
- QMD retrieval (BM25, vector, RRF, rerank, query expansion) is forked into ClawMem. Do not call standalone QMD tools.
|
|
736
736
|
- SAME (composite scoring), MAGMA (intent + graph), A-MEM (self-evolving notes) layer on top of QMD substrate.
|
|
737
|
-
- Three
|
|
737
|
+
- Three inference services (embedding, LLM, reranker) on local or remote GPU — `llama-server` for all three in the default stack; the SOTA stack serves the reranker as a transformers sidecar. Wrapper defaults to `localhost:8088/8089/8090`.
|
|
738
738
|
- `CLAWMEM_NO_LOCAL_MODELS=false` (default) allows in-process LLM/reranker fallback via `node-llama-cpp`. Set `true` for remote-only setups to fail fast on unreachable endpoints.
|
|
739
739
|
- Consolidation worker (`CLAWMEM_ENABLE_CONSOLIDATION=true`) backfills unenriched docs with A-MEM notes + links. Only runs if the MCP process stays alive long enough to tick (every 5min).
|
|
740
740
|
- Beads integration: `syncBeadsIssues()` queries `bd` CLI (Dolt backend, v0.58.0+) for live issue data, creates markdown docs in `beads` collection, maps all dependency edge types into `memory_relations`, and triggers A-MEM enrichment for new docs. Watcher auto-triggers on `.beads/` directory changes; `beads_sync` MCP tool for manual sync. Requires `bd` binary on PATH or at `~/go/bin/bd`.
|
package/README.md
CHANGED
|
@@ -114,8 +114,8 @@ After installing, here's the full journey from zero to working memory:
|
|
|
114
114
|
| Step | What | How | Details |
|
|
115
115
|
|------|------|-----|---------|
|
|
116
116
|
| **1. Bootstrap** | Create a vault, index your first collection, embed, install hooks and MCP | `clawmem bootstrap ~/notes --name notes` | One command does it all. Or run each step manually (see below). |
|
|
117
|
-
| **2. Choose models** | Pick embedding + reranker models based on your hardware |
|
|
118
|
-
| **3. Download models** | Get the
|
|
117
|
+
| **2. Choose models** | Pick embedding + reranker models based on your hardware | 16GB+ VRAM → SOTA stack (zembed-1 + zerank-2 sidecar). Less → QMD native combo. No GPU → cloud embedding or CPU fallback. | [GPU Services](#gpu-services) |
|
|
118
|
+
| **3. Download models** | Get the model files for your chosen stack (GGUFs for embedding/LLM/default-reranker; the zerank-2 SOTA reranker builds its own sidecar artifact) | `wget` from HuggingFace, let `node-llama-cpp` auto-download the QMD native models, or run the sidecar recipe | [Embedding](#embedding), [LLM Server](#llm-server), [Reranker Server](#reranker-server) |
|
|
119
119
|
| **4. Start services** | Run GPU servers (if using dedicated GPU) and background services. Optionally enable the v0.8.2 background maintenance workers in the watcher unit so consolidation + deductive synthesis run automatically. | `llama-server` for each model. systemd units for watcher + embed timer. Drop-in for the watcher to enable workers + tune intervals + set the quiet window. | [systemd services](docs/guides/systemd-services.md), [background workers](docs/guides/systemd-services.md#background-maintenance-workers-v082) |
|
|
120
120
|
| **5. Decide what to index** | Add collections for your projects, notes, research, and domain docs | `clawmem collection add ~/project --name project` | The more relevant markdown you index, the better retrieval works. See [building a rich context field](docs/introduction.md#building-a-rich-context-field). |
|
|
121
121
|
| **6. Connect your agent** | Hook into Claude Code, OpenClaw, Hermes, or any MCP client | `clawmem setup hooks && clawmem setup mcp` for Claude Code. `clawmem setup openclaw` for OpenClaw. Copy `src/hermes/` to Hermes plugins for Hermes. | [Integration](#integration) |
|
|
@@ -305,17 +305,19 @@ vault_sync(vault="work", content_root="~/work/docs")
|
|
|
305
305
|
|
|
306
306
|
### GPU Services
|
|
307
307
|
|
|
308
|
-
ClawMem uses three `llama-server` (llama.cpp) instances
|
|
308
|
+
ClawMem uses three inference services — embedding, LLM, and reranker. In the **default** stack all three run as `llama-server` (llama.cpp) instances, each with an in-process `node-llama-cpp` fallback (auto-downloads on first use), so ClawMem works without a dedicated GPU. (The **SOTA** reranker is the exception — it is a transformers sidecar, see [Reranker Server](#reranker-server).) `node-llama-cpp` auto-detects the best available backend — Metal on Apple Silicon, Vulkan where available, CPU as last resort. With GPU acceleration (Metal/Vulkan), in-process inference is fast for these small models (0.3B–1.7B); on CPU-only systems it is significantly slower. For production use, run the servers via [systemd services](docs/guides/systemd-services.md) to prevent silent fallback.
|
|
309
309
|
|
|
310
|
-
**GPU with VRAM to spare (
|
|
310
|
+
**GPU with VRAM to spare (16GB+, recommended):** ZeroEntropy's distillation-paired stack delivers best retrieval quality — total ~16GB VRAM.
|
|
311
311
|
|
|
312
312
|
| Service | Port | Model | VRAM | Purpose |
|
|
313
313
|
|---|---|---|---|---|
|
|
314
314
|
| Embedding | 8088 | [zembed-1-Q4_K_M](https://huggingface.co/Abhiray/zembed-1-Q4_K_M-GGUF) | ~4.4GB | SOTA embedding (2560d, 32K context). Distilled from zerank-2 via zELO. |
|
|
315
315
|
| LLM | 8089 | [qmd-query-expansion-1.7B-q4_k_m](https://huggingface.co/tobil/qmd-query-expansion-1.7B-gguf) | ~2.2GB | Intent classification, query expansion, A-MEM |
|
|
316
|
-
| Reranker | 8090 | [zerank-2-
|
|
316
|
+
| Reranker | 8090 | [zerank-2 seq-cls sidecar](extras/rerankers/zerank-2-seq/) | ~9GB (bf16) | SOTA reranker. NDCG@10 ahead of Cohere rerank-3.5. Optimal pairing with zembed-1. |
|
|
317
317
|
|
|
318
|
-
**
|
|
318
|
+
**The SOTA reranker is a sidecar, not a GGUF.** It is served by the [zerank-2 seq-cls sidecar](extras/rerankers/zerank-2-seq/) (transformers, bf16). The previously-listed `zerank-2-Q4_K_M` GGUF is **deprecated** — llama.cpp's converter drops zerank's score head, so under `--reranking` it produces near-zero, uninformative scores (final ordering stays RRF-dominated). See the [sidecar README](extras/rerankers/zerank-2-seq/) and [Reranker Server](#reranker-server).
|
|
319
|
+
|
|
320
|
+
**Important:** zembed-1 uses non-causal attention — `-ub` must equal `-b` on llama-server (e.g. `-b 2048 -ub 2048`). See [Embedding](#embedding) for details.
|
|
319
321
|
|
|
320
322
|
**License:** zembed-1 and zerank-2 are released under **CC-BY-NC-4.0** — non-commercial only. The QMD native models below have no such restriction.
|
|
321
323
|
|
|
@@ -466,15 +468,15 @@ Cross-encoder reranking for `query` and `intent_search` pipelines on port 8090.
|
|
|
466
468
|
|
|
467
469
|
Scores each candidate against the original query (cross-encoder architecture). `query` pipeline: 4000 char context per doc (deep reranking); `intent_search`: 200 char context per doc (fast reranking).
|
|
468
470
|
|
|
469
|
-
**GPU with VRAM to spare (recommended):** [zerank-2-
|
|
471
|
+
**GPU with VRAM to spare (recommended):** the **[zerank-2 seq-cls sidecar](extras/rerankers/zerank-2-seq/)** (bf16, ~9GB VRAM). zerank-2's NDCG@10 is ahead of Cohere rerank-3.5 and Gemini 2.5 Flash. Optimal pairing with zembed-1 (same distillation architecture via zELO). **CC-BY-NC-4.0** — non-commercial only.
|
|
470
472
|
|
|
471
|
-
|
|
472
|
-
wget https://huggingface.co/keisuke-miyako/zerank-2-gguf-q4_k_m/resolve/main/zerank-2-Q4_k_m.gguf
|
|
473
|
+
zerank-2 is **not** servable as a llama.cpp GGUF — its CrossEncoder/LogitScore head is dropped by the GGUF converter (the model becomes a headless causal LM that produces near-zero, uninformative scores under `--reranking`, leaving final ordering RRF-dominated). The sidecar serves the real head via transformers, with a reproducible correctness gate:
|
|
473
474
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
475
|
+
```bash
|
|
476
|
+
cd extras/rerankers/zerank-2-seq
|
|
477
|
+
docker compose build
|
|
478
|
+
HF_TOKEN=hf_xxx docker compose run --rm convert # download + convert + verify (all gates must pass)
|
|
479
|
+
docker compose up -d reranker # serves /v1/rerank on :8090
|
|
478
480
|
```
|
|
479
481
|
|
|
480
482
|
**CPU / GPU without VRAM to spare:** [qwen3-reranker-0.6B-Q8_0](https://huggingface.co/ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF) (~600MB, ~1.3GB VRAM). The QMD native reranker — auto-downloaded by `node-llama-cpp` if no server is running.
|
|
@@ -487,7 +489,7 @@ llama-server -m Qwen3-Reranker-0.6B-Q8_0.gguf \
|
|
|
487
489
|
-ngl 99 -c 2048 --batch-size 512
|
|
488
490
|
```
|
|
489
491
|
|
|
490
|
-
**Note:**
|
|
492
|
+
**Note:** zembed-1 (embedding) uses non-causal attention — `-ub` (ubatch) must equal `-b` (batch); omitting `-ub` or setting it lower causes assertion crashes. The qwen3-reranker-0.6B GGUF does not have this requirement, and the zerank-2 sidecar is served via transformers (no llama.cpp batch flags). See [llama.cpp#12836](https://github.com/ggml-org/llama.cpp/issues/12836).
|
|
491
493
|
|
|
492
494
|
### MCP Server
|
|
493
495
|
|
|
@@ -811,7 +813,7 @@ User Query + optional intent hint
|
|
|
811
813
|
→ Reciprocal Rank Fusion → slice to candidateLimit (default 30)
|
|
812
814
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
813
815
|
→ Cross-Encoder Reranking (4000 char context; intent prepended; chunk dedup; batch cap=4)
|
|
814
|
-
→
|
|
816
|
+
→ Rerank/RRF Blend (blendRerank: 0.9·reranker + 0.1·normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
815
817
|
→ SAME Composite Scoring ((search × 0.5 + recency × 0.25 + confidence × 0.25) × qualityMultiplier × lengthNorm × coActivationBoost + pinBoost)
|
|
816
818
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted)
|
|
817
819
|
→ Ranked Results
|
|
@@ -1151,7 +1153,7 @@ clawmem serve --port 7438 &
|
|
|
1151
1153
|
|
|
1152
1154
|
## Deployment
|
|
1153
1155
|
|
|
1154
|
-
Three-tier retrieval architecture: infrastructure (watcher + embed timer) → hooks (~90%) → agent MCP (~10%). Works out of the box without a dedicated GPU (all models auto-download via `node-llama-cpp`, uses Metal on Apple Silicon). For best performance, run three `llama-server` instances — see [GPU Services](#gpu-services) for model tiers (SOTA vs QMD native) and [Cloud Embedding](#option-c-cloud-embedding-api) for cloud embedding alternatives.
|
|
1156
|
+
Three-tier retrieval architecture: infrastructure (watcher + embed timer) → hooks (~90%) → agent MCP (~10%). Works out of the box without a dedicated GPU (all models auto-download via `node-llama-cpp`, uses Metal on Apple Silicon). For best performance, run the inference services on GPU (three `llama-server` instances in the default stack; the SOTA reranker is a transformers sidecar) — see [GPU Services](#gpu-services) for model tiers (SOTA vs QMD native) and [Cloud Embedding](#option-c-cloud-embedding-api) for cloud embedding alternatives.
|
|
1155
1157
|
|
|
1156
1158
|
Key services: `clawmem-watcher` (auto-index on file change + beads sync), `clawmem-embed` timer (daily embedding sweep), 7 Claude Code hooks installed by default (context injection, curator nudge, compaction support, decision extraction, handoffs, feedback). Optional `clawmem-curator` agent for on-demand lifecycle triage, retrieval health checks, and maintenance (`clawmem setup curator`).
|
|
1157
1159
|
|
package/SKILL.md
CHANGED
|
@@ -18,7 +18,7 @@ Two tiers: **hooks** handle automatic context flow (surfacing, extraction, compa
|
|
|
18
18
|
|
|
19
19
|
## Inference Services
|
|
20
20
|
|
|
21
|
-
Three `llama-server` instances
|
|
21
|
+
Three inference services (embedding, LLM, reranker). The **default** stack runs all three as `llama-server` instances; the **SOTA** stack serves the reranker as a transformers sidecar (same `/v1/rerank` contract). The `bin/clawmem` wrapper defaults to `localhost:8088/8089/8090`.
|
|
22
22
|
|
|
23
23
|
**Default (QMD native combo, any GPU or in-process):**
|
|
24
24
|
|
|
@@ -30,7 +30,7 @@ Three `llama-server` instances for neural inference. The `bin/clawmem` wrapper d
|
|
|
30
30
|
|
|
31
31
|
All three models auto-download via `node-llama-cpp` if no server is running (Metal on Apple Silicon, Vulkan where available, CPU as last resort). Fast with GPU acceleration (Metal/Vulkan); significantly slower on CPU-only.
|
|
32
32
|
|
|
33
|
-
**SOTA upgrade (
|
|
33
|
+
**SOTA upgrade (16GB+ GPU):** zembed-1-Q4_K_M (embedding, 2560d, ~4.4GB) + the **zerank-2 seq-cls sidecar** (reranker, bf16 ~9GB — see `extras/rerankers/zerank-2-seq/`). Total ~16GB with LLM. Distillation-paired via zELO. zembed-1's `-ub` must match `-b`. **CC-BY-NC-4.0** — non-commercial only. The older `zerank-2-Q4_K_M` GGUF reranker is **deprecated** (llama.cpp drops zerank's score head → inert reranking); use the sidecar.
|
|
34
34
|
|
|
35
35
|
**Remote option:** Set `CLAWMEM_EMBED_URL`, `CLAWMEM_LLM_URL`, `CLAWMEM_RERANK_URL` to remote host. Set `CLAWMEM_NO_LOCAL_MODELS=true` to prevent fallback downloads.
|
|
36
36
|
|
|
@@ -53,15 +53,14 @@ llama-server -m qmd-query-expansion-1.7B-q4_k_m.gguf \
|
|
|
53
53
|
llama-server -m Qwen3-Reranker-0.6B-Q8_0.gguf \
|
|
54
54
|
--reranking --port 8090 --host 0.0.0.0 -ngl 99 -c 2048 --batch-size 512
|
|
55
55
|
|
|
56
|
-
# === SOTA upgrade (
|
|
56
|
+
# === SOTA upgrade (16GB+ GPU) ===
|
|
57
57
|
|
|
58
|
-
# Embedding (zembed-1)
|
|
58
|
+
# Embedding (zembed-1) — -ub must match -b
|
|
59
59
|
llama-server -m zembed-1-Q4_K_M.gguf \
|
|
60
60
|
--embeddings --port 8088 --host 0.0.0.0 -ngl 99 -c 8192 -b 2048 -ub 2048
|
|
61
61
|
|
|
62
|
-
# Reranker (zerank-2)
|
|
63
|
-
|
|
64
|
-
--reranking --port 8090 --host 0.0.0.0 -ngl 99 -c 2048 -b 2048 -ub 2048
|
|
62
|
+
# Reranker (zerank-2) — seq-cls SIDECAR (transformers, bf16), NOT GGUF:
|
|
63
|
+
# see extras/rerankers/zerank-2-seq/ (the zerank-2 GGUF is deprecated — drops the score head)
|
|
65
64
|
```
|
|
66
65
|
|
|
67
66
|
### Verify Endpoints
|
|
@@ -390,7 +389,7 @@ User Query + optional intent hint
|
|
|
390
389
|
-> Reciprocal Rank Fusion (k=60, top candidateLimit)
|
|
391
390
|
-> Intent-Aware Chunk Selection (intent terms at 0.5x weight alongside query terms at 1.0x)
|
|
392
391
|
-> Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
393
|
-
->
|
|
392
|
+
-> Rerank/RRF Blend (blendRerank: 0.9*reranker + 0.1*normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
394
393
|
-> Composite Scoring
|
|
395
394
|
-> MMR Diversity Filter (Jaccard bigram similarity > 0.6 -> demoted, not removed)
|
|
396
395
|
```
|
|
@@ -791,7 +790,7 @@ clawmem focus clear --session-id abc123
|
|
|
791
790
|
|
|
792
791
|
- QMD retrieval (BM25, vector, RRF, rerank, query expansion) is forked into ClawMem. Do not call standalone QMD tools.
|
|
793
792
|
- SAME (composite scoring), MAGMA (intent + graph), A-MEM (self-evolving notes) layer on top of QMD substrate.
|
|
794
|
-
- Three `llama-server`
|
|
793
|
+
- Three inference services on local or remote GPU — `llama-server` for all three in the default stack; the SOTA stack serves the reranker as a transformers sidecar. Wrapper defaults to `localhost:8088/8089/8090`.
|
|
795
794
|
- `CLAWMEM_NO_LOCAL_MODELS=false` (default) allows in-process fallback. Set `true` for remote-only to fail fast.
|
|
796
795
|
- Consolidation worker (`CLAWMEM_ENABLE_CONSOLIDATION=true`) backfills unenriched docs and runs Phase 2 merge / Phase 3 deductive synthesis. **v0.8.2:** hosted by either `clawmem watch` (long-lived, canonical) or `clawmem mcp` (per-session fallback); every tick acquires a `light-consolidation` `worker_leases` row before doing work, so dual-hosting against the same vault is safe.
|
|
797
796
|
- Beads integration: `syncBeadsIssues()` queries `bd` CLI (Dolt backend, v0.58.0+), creates markdown docs, maps dependency edges into `memory_relations`. Watcher auto-triggers on `.beads/` changes; `beads_sync` MCP for manual sync.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ClawMem zerank-2 seq-cls reranker sidecar.
|
|
2
|
+
# Serves zeroentropy/zerank-2-reranker as a faithful Qwen3ForSequenceClassification cross-encoder
|
|
3
|
+
# (the llama.cpp GGUF route drops zerank's score head; this serves the real one via transformers).
|
|
4
|
+
#
|
|
5
|
+
# LICENSE: this image bundles only the MIT recipe code. zerank-2's WEIGHTS are CC-BY-NC-4.0
|
|
6
|
+
# (non-commercial) and are downloaded at convert time for YOUR own use — never redistributed here.
|
|
7
|
+
FROM pytorch/pytorch:2.6.0-cuda12.4-cudnn9-runtime
|
|
8
|
+
|
|
9
|
+
RUN pip install --no-cache-dir \
|
|
10
|
+
"transformers>=4.51" \
|
|
11
|
+
"huggingface_hub>=0.24" \
|
|
12
|
+
"uvicorn[standard]" \
|
|
13
|
+
fastapi \
|
|
14
|
+
safetensors \
|
|
15
|
+
accelerate
|
|
16
|
+
|
|
17
|
+
WORKDIR /app
|
|
18
|
+
COPY zr_common.py build_and_verify.py server.py /app/
|
|
19
|
+
|
|
20
|
+
ENV ZR_OUT=/models/zerank-2-seq \
|
|
21
|
+
ZR_PORT=8090 \
|
|
22
|
+
ZR_TEMP=5.0 \
|
|
23
|
+
ZR_MAXLEN=8192
|
|
24
|
+
|
|
25
|
+
EXPOSE 8090
|
|
26
|
+
CMD ["python3", "server.py"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# zerank-2 seq-cls reranker sidecar (SOTA, non-commercial)
|
|
2
|
+
|
|
3
|
+
A drop-in `/v1/rerank` sidecar that serves **ZeroEntropy [zerank-2](https://huggingface.co/zeroentropy/zerank-2-reranker)** — a state-of-the-art cross-encoder (NDCG@10 ≈ 0.671, ahead of Cohere rerank-3.5 and Gemini-2.5-Flash listwise) — **faithfully**, via transformers, with a reproducible correctness gate.
|
|
4
|
+
|
|
5
|
+
Point ClawMem at it and nothing else changes:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export CLAWMEM_RERANK_URL=http://<this-host>:8090
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
ClawMem's `query` and `intent_search` pipelines call `/v1/rerank` on that URL exactly as before.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why this exists (and why the GGUF is deprecated)
|
|
16
|
+
|
|
17
|
+
The previously-recommended `zerank-2-Q4_K_M.gguf` reranker is **broken** and is deprecated. zerank-2 is a `Qwen3ForCausalLM` that scores a `(query, document)` pair on the logit of a single relevance token (`"Yes"`, id `9454`) via a sentence-transformers `LogitScore` head. llama.cpp's `convert_hf_to_gguf.py` only synthesizes a rerank head when it finds the literal string `# Qwen3-Reranker` in the model card — zerank-2's card lacks it, so the previously-recommended GGUF (and anything built by the current/standard llama.cpp converter path) is a **headless causal LM**. Served under `--reranking` it produces near-zero, uninformative scores → reranking degrades to an inert RRF-dominated passthrough, silently.
|
|
18
|
+
|
|
19
|
+
This recipe sidesteps GGUF entirely. Because zerank-2 uses **tied embeddings**, the relevance logit `hidden · embed_tokens.weight[9454]` is reproduced **exactly** by a standard `Qwen3ForSequenceClassification` whose `num_labels=1` score head is that one embedding row. We convert to that form, **prove** the conversion is bit-exact, and serve it with the model's real chat template and `sigmoid(logit/5)` calibration.
|
|
20
|
+
|
|
21
|
+
> **License:** zerank-2 is **CC-BY-NC-4.0 (non-commercial)**. This recipe is MIT code; it **never bundles the weights** — `build_and_verify.py` downloads them for *your own* non-commercial use. ClawMem's default reranker stays the permissively-licensed `qwen3-reranker-0.6B`; this is an opt-in upgrade.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- An NVIDIA GPU with **~9 GiB free VRAM** (bf16) for the reranker. A 12 GB card is comfortable for the sidecar **alone**; budget **16 GB+** for the full co-located SOTA stack (embedding + LLM + reranker).
|
|
28
|
+
- **NVIDIA Container Toolkit** (for `--gpus`/`docker compose` GPU access).
|
|
29
|
+
- A Hugging Face account that has **accepted the zerank-2 license**, and an `HF_TOKEN` for the one-off convert step.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick start (Docker Compose)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd extras/rerankers/zerank-2-seq
|
|
37
|
+
docker compose build
|
|
38
|
+
|
|
39
|
+
# One-off: download + convert + run ALL correctness gates into ./models/zerank-2-seq
|
|
40
|
+
HF_TOKEN=hf_xxx docker compose run --rm convert
|
|
41
|
+
|
|
42
|
+
# Serve on :8090 (boot-persistent)
|
|
43
|
+
docker compose up -d reranker
|
|
44
|
+
|
|
45
|
+
# Verify
|
|
46
|
+
curl -s localhost:8090/health
|
|
47
|
+
curl -s -X POST localhost:8090/v1/rerank -H 'Content-Type: application/json' \
|
|
48
|
+
-d '{"query":"capital of France","documents":["Paris is the capital of France.","Bananas grow in the tropics."]}'
|
|
49
|
+
# -> relevant ~0.96, irrelevant ~0.08
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then set `CLAWMEM_RERANK_URL=http://<this-host>:8090` for ClawMem (see the main README's *GPU Services*).
|
|
53
|
+
|
|
54
|
+
The convert step **refuses to finish unless every gate passes** (`RESULT PASS`, exit 0). If a gate fails it exits non-zero and does not leave a half-baked model serving.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## What the gate proves
|
|
59
|
+
|
|
60
|
+
`build_and_verify.py` is the correctness proof — it converts, then asserts:
|
|
61
|
+
|
|
62
|
+
| Gate | Asserts |
|
|
63
|
+
|---|---|
|
|
64
|
+
| **GATE 1** | the score-head row equals `lm_head[9454]` in fp32, saved as bf16 (no silent fp16 downcast) |
|
|
65
|
+
| **GATE 1.5** | the served tokenizer == the source tokenizer (formatted strings + token ids + `encode_pair` through truncation); the transformers `fix_mistral_regex` warning is benign |
|
|
66
|
+
| **GATE-AP** | the assistant-generation prefix survives even a near-`MAXLEN` input (truncating it would move the scored position) |
|
|
67
|
+
| **GATE 2** | the seq-cls logit equals the causal model's token-9454 logit **bit-exactly** over the real served path, incl. batched right-padded pooling and empty/whitespace-doc edges |
|
|
68
|
+
|
|
69
|
+
It runs against the source model, so it verifies **any** copy of the weights — see the two source paths below.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Two ways to get the weights
|
|
74
|
+
|
|
75
|
+
1. **Reproduce (trustless, default).** `build_and_verify.py` downloads `zeroentropy/zerank-2-reranker` and performs the conversion itself, then gates it. You trust only ZeroEntropy's official weights + this MIT code.
|
|
76
|
+
2. **Pull a pre-converted upload, then verify it.** A community conversion exists at [`baseten-admin/zerank-2-reranker-seq`](https://huggingface.co/baseten-admin/zerank-2-reranker-seq). Download it into `./models/zerank-2-seq`, then run the gate in **verify-only** mode — it skips conversion but still downloads the official `ZR_SRC` to prove the seq-cls logits match: `ZR_VERIFY_ONLY=1 HF_TOKEN=hf_xxx docker compose run --rm convert`. The gate verifies whatever you point `ZR_OUT` at, and **fails a bad upload** (e.g. one converted on the wrong token).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## How it works
|
|
81
|
+
|
|
82
|
+
- **Conversion** (`build_and_verify.py`): copy `embed_tokens.weight[9454]` into a `Linear(hidden, 1, bias=False)` score head of `Qwen3ForSequenceClassification(num_labels=1)`, in **bf16**. Tied embeddings make this logit identical to the native causal score by construction.
|
|
83
|
+
- **Serving** (`server.py`): `batch=1` (deterministic, no padding drift), applies zerank's chat template (`query`→system, `document`→user, assistant prefix), returns `sigmoid(logit/5)` — the native calibration.
|
|
84
|
+
- **Shared encoder** (`zr_common.py`): the gate and the server import the *same* `encode_pair`, so served scores == gated scores by construction, and the assistant prefix is never truncated.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
| Env | Default | Meaning |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `ZR_SRC` | `zeroentropy/zerank-2-reranker` | source model to convert |
|
|
93
|
+
| `ZR_OUT` | `/models/zerank-2-seq` | converted seq-cls model path (mounted volume) |
|
|
94
|
+
| `ZR_PORT` | `8090` | sidecar port |
|
|
95
|
+
| `ZR_TEMP` | `5.0` | calibration temperature → `sigmoid(logit/ZR_TEMP)` |
|
|
96
|
+
| `ZR_MAXLEN` | `8192` | max tokens per pair (doc tail truncated to fit; prefix preserved) |
|
|
97
|
+
| `HF_TOKEN` | — | required for the convert download (gated model) |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Rollback
|
|
102
|
+
|
|
103
|
+
This sidecar only occupies `:8090` and speaks the standard contract, so reverting is just pointing `CLAWMEM_RERANK_URL` back at your previous reranker (e.g. a `qwen3-reranker-0.6B` `llama-server`) and stopping the container:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
docker compose down
|
|
107
|
+
```
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reproduce zerank-2 -> Qwen3ForSequenceClassification(num_labels=1), bf16, token 9454, then GATE.
|
|
3
|
+
GATE1 : fp32 weight-equality (score row == lm_head[9454], bf16 preserved).
|
|
4
|
+
GATE1.5 : served-tokenizer path identity -> OUT tok == SRC tok: (a) formatted-string + ids,
|
|
5
|
+
(b) encode_pair(SRC)==encode_pair(OUT) INCLUDING the truncation path,
|
|
6
|
+
(c) fix_mistral_regex default-vs-flag token-id identity (persisted).
|
|
7
|
+
GATE-AP : assistant-prefix preserved under near-MAXLEN via encode_pair.
|
|
8
|
+
GATE2 : runtime FIDELITY (seq-cls logit == causal token-9454 logit) over the EXACT served path
|
|
9
|
+
(OUT tokenizer + encode_pair, batch=1) + a batched right-padded pooling check + edge cases.
|
|
10
|
+
Exit 0 only if every gate passes. Runs in the provided Dockerfile image (torch + transformers + CUDA)."""
|
|
11
|
+
import os, sys, gc, math
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
import torch
|
|
14
|
+
from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer
|
|
15
|
+
from zr_common import format_pair, encode_pair, ASSISTANT_PREFIX
|
|
16
|
+
|
|
17
|
+
SRC = os.environ.get("ZR_SRC", "zeroentropy/zerank-2-reranker")
|
|
18
|
+
OUT = os.environ.get("ZR_OUT", "/models/zerank-2-seq")
|
|
19
|
+
TOKEN_ID = 9454
|
|
20
|
+
TEMP = 5.0
|
|
21
|
+
MAXLEN = int(os.environ.get("ZR_MAXLEN", "8192"))
|
|
22
|
+
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
|
23
|
+
VERIFY_ONLY = os.environ.get("ZR_VERIFY_ONLY", "").lower() in ("1", "true", "yes") # gate an existing OUT (e.g. a pulled upload) without re-converting
|
|
24
|
+
def log(*a): print("[build]", *a, flush=True)
|
|
25
|
+
|
|
26
|
+
# ---- Phase A: convert (CPU, bf16, explicit token 9454) — skipped under ZR_VERIFY_ONLY ----
|
|
27
|
+
if VERIFY_ONLY:
|
|
28
|
+
if not os.path.isdir(OUT):
|
|
29
|
+
log(f"ZR_VERIFY_ONLY set but {OUT} does not exist — nothing to verify"); sys.exit(2)
|
|
30
|
+
log(f"ZR_VERIFY_ONLY: skipping conversion; gating the existing model at {OUT} against {SRC}")
|
|
31
|
+
else:
|
|
32
|
+
log(f"loading causal {SRC} (bf16, cpu)")
|
|
33
|
+
causal = AutoModelForCausalLM.from_pretrained(SRC, torch_dtype=torch.bfloat16)
|
|
34
|
+
assert causal.lm_head.bias is None
|
|
35
|
+
assert getattr(causal.config, "tie_word_embeddings", False)
|
|
36
|
+
hidden = causal.lm_head.in_features
|
|
37
|
+
row = causal.lm_head.weight[TOKEN_ID].detach().clone()
|
|
38
|
+
log(f"extracted lm_head row {TOKEN_ID}: shape={tuple(row.shape)} dtype={row.dtype}")
|
|
39
|
+
del causal; gc.collect()
|
|
40
|
+
|
|
41
|
+
log("loading seq-cls backbone (bf16, cpu, num_labels=1)")
|
|
42
|
+
seq = AutoModelForSequenceClassification.from_pretrained(SRC, num_labels=1, torch_dtype=torch.bfloat16)
|
|
43
|
+
lin = torch.nn.Linear(hidden, 1, bias=False, dtype=torch.bfloat16)
|
|
44
|
+
with torch.no_grad():
|
|
45
|
+
lin.weight.copy_(row.unsqueeze(0))
|
|
46
|
+
seq.score = lin
|
|
47
|
+
seq.config.num_labels = 1
|
|
48
|
+
seq.config.id2label = {0: "relevant"}; seq.config.label2id = {"relevant": 0}
|
|
49
|
+
tok = AutoTokenizer.from_pretrained(SRC)
|
|
50
|
+
pad_id = tok.pad_token_id if tok.pad_token_id is not None else tok.convert_tokens_to_ids("<|endoftext|>")
|
|
51
|
+
seq.config.pad_token_id = pad_id
|
|
52
|
+
tok.padding_side = "right"
|
|
53
|
+
log(f"pad_token_id={pad_id} padding_side=right score.weight.dtype={seq.score.weight.dtype}")
|
|
54
|
+
os.makedirs(OUT, exist_ok=True)
|
|
55
|
+
seq.save_pretrained(OUT); tok.save_pretrained(OUT)
|
|
56
|
+
log(f"saved seq-cls -> {OUT}")
|
|
57
|
+
del seq; gc.collect()
|
|
58
|
+
|
|
59
|
+
# ---- GATE 1: fp32 weight equality ----
|
|
60
|
+
log("GATE1: reload + fp32 weight equality")
|
|
61
|
+
c2 = AutoModelForCausalLM.from_pretrained(SRC, torch_dtype=torch.bfloat16)
|
|
62
|
+
src_row = c2.lm_head.weight[TOKEN_ID].float().cpu(); del c2; gc.collect()
|
|
63
|
+
s2 = AutoModelForSequenceClassification.from_pretrained(OUT, num_labels=1, torch_dtype=torch.bfloat16)
|
|
64
|
+
saved_dtype = s2.score.weight.dtype
|
|
65
|
+
saved_row = s2.score.weight[0].float().cpu(); del s2; gc.collect()
|
|
66
|
+
maxd1 = (src_row - saved_row).abs().max().item()
|
|
67
|
+
gate1 = maxd1 < 1e-7
|
|
68
|
+
log(f"GATE1 saved_score_dtype={saved_dtype} weight_maxdiff(fp32)={maxd1:.3e} -> {'PASS' if gate1 else 'FAIL'}")
|
|
69
|
+
if not gate1:
|
|
70
|
+
log("GATE1 FAIL — aborting."); sys.exit(2)
|
|
71
|
+
|
|
72
|
+
PAIRS = [
|
|
73
|
+
("What is the capital of France?", "The capital of France is Paris."),
|
|
74
|
+
("What is the capital of France?", "Bananas are rich in potassium and grow in tropical climates."),
|
|
75
|
+
("How do I configure TLS in nginx?", "In nginx set ssl_certificate and ssl_certificate_key in the server block on 443, then reload."),
|
|
76
|
+
("python async database connection pooling", "asyncpg exposes create_pool; set min_size/max_size and acquire via 'async with pool.acquire()'."),
|
|
77
|
+
("short q", "x"),
|
|
78
|
+
("distributed consensus and leader election in raft", ("Raft elects a leader via randomized election timeouts. " * 12)[:400]),
|
|
79
|
+
("empty doc edge", ""),
|
|
80
|
+
("whitespace doc edge", " "),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# ---- GATE 1.5: served-tokenizer path identity (OUT == SRC) ----
|
|
84
|
+
src_tok = AutoTokenizer.from_pretrained(SRC); src_tok.padding_side = "right"
|
|
85
|
+
out_tok = AutoTokenizer.from_pretrained(OUT); out_tok.padding_side = "right"
|
|
86
|
+
pad_id = out_tok.pad_token_id if out_tok.pad_token_id is not None else out_tok.convert_tokens_to_ids("<|endoftext|>") # set here so GATE2 has it under ZR_VERIFY_ONLY too
|
|
87
|
+
# (a) formatted-string + token-id identity
|
|
88
|
+
tokpath_ok = True
|
|
89
|
+
for q, d in PAIRS:
|
|
90
|
+
s_str, o_str = format_pair(src_tok, q, d), format_pair(out_tok, q, d)
|
|
91
|
+
if (s_str != o_str) or (src_tok(s_str, add_special_tokens=False)["input_ids"] != out_tok(o_str, add_special_tokens=False)["input_ids"]):
|
|
92
|
+
tokpath_ok = False; log(f" GATE1.5a MISMATCH q={q!r}")
|
|
93
|
+
log(f"GATE1.5a served==source tokenizer (string+ids) -> {'PASS' if tokpath_ok else 'FAIL'}")
|
|
94
|
+
# (b) encode_pair(SRC) == encode_pair(OUT) INCLUDING the truncation path (gate path == served path)
|
|
95
|
+
ep_ok = True
|
|
96
|
+
ep_pairs = PAIRS + [("very long query " + "stress " * 3000, "document body " + "lorem ipsum " * 6000)]
|
|
97
|
+
for q, d in ep_pairs:
|
|
98
|
+
a = encode_pair(src_tok, q, d, max_total_tokens=MAXLEN)["input_ids"].tolist()
|
|
99
|
+
b = encode_pair(out_tok, q, d, max_total_tokens=MAXLEN)["input_ids"].tolist()
|
|
100
|
+
if a != b: ep_ok = False; log(f" GATE1.5b encode_pair MISMATCH q[:30]={q[:30]!r} lens {len(a[0])} vs {len(b[0])}")
|
|
101
|
+
log(f"GATE1.5b encode_pair(SRC)==encode_pair(OUT) incl truncation -> {'PASS' if ep_ok else 'FAIL'}")
|
|
102
|
+
# (c) fix_mistral_regex default-vs-flag token-id identity (the transformers warning is benign if 0 diffs)
|
|
103
|
+
try:
|
|
104
|
+
fix_tok = AutoTokenizer.from_pretrained(OUT, fix_mistral_regex=True); fix_flag = "supported"
|
|
105
|
+
except TypeError as e:
|
|
106
|
+
fix_tok = None; fix_flag = f"UNSUPPORTED({type(e).__name__})"
|
|
107
|
+
regex_diffs = 0
|
|
108
|
+
for q, d in PAIRS:
|
|
109
|
+
base = out_tok(format_pair(out_tok, q, d), add_special_tokens=False)["input_ids"]
|
|
110
|
+
cmp = (fix_tok or out_tok)(format_pair((fix_tok or out_tok), q, d), add_special_tokens=False)["input_ids"]
|
|
111
|
+
if base != cmp: regex_diffs += 1
|
|
112
|
+
mixed = "cafe naive 2026 x=1;y=2 https://a.b/c?q=1 path/to::thing emoji"
|
|
113
|
+
mixed_diff = out_tok(mixed, add_special_tokens=False)["input_ids"] != (fix_tok or out_tok)(mixed, add_special_tokens=False)["input_ids"]
|
|
114
|
+
regex_ok = (regex_diffs == 0) and (not mixed_diff)
|
|
115
|
+
log(f"GATE1.5c fix_mistral_regex flag={fix_flag} pair_id_diffs={regex_diffs} mixed_diff={mixed_diff} -> {'BENIGN/PASS' if regex_ok else 'MATERIAL/FAIL'}")
|
|
116
|
+
|
|
117
|
+
# ---- GATE-AP: assistant-prefix preserved under near-MAXLEN input ----
|
|
118
|
+
ap_ids = out_tok(ASSISTANT_PREFIX, add_special_tokens=False)["input_ids"]
|
|
119
|
+
long_doc = "lorem ipsum dolor sit amet consectetur " * 4000
|
|
120
|
+
enc_long = encode_pair(out_tok, "very long reranking stress-test query " * 200, long_doc, max_total_tokens=MAXLEN, device="cpu")
|
|
121
|
+
long_ids = enc_long["input_ids"][0].tolist()
|
|
122
|
+
ap_ok = (len(long_ids) <= MAXLEN) and (long_ids[-len(ap_ids):] == ap_ids)
|
|
123
|
+
log(f"GATE-AP near-MAXLEN: len={len(long_ids)}<= {MAXLEN} and tail==assistant_prefix -> {'PASS' if ap_ok else 'FAIL'}")
|
|
124
|
+
|
|
125
|
+
# ---- GATE 2: runtime fidelity over the EXACT served path (OUT tokenizer + encode_pair) + batched pooling ----
|
|
126
|
+
log("GATE2: loading causal on GPU")
|
|
127
|
+
c3 = AutoModelForCausalLM.from_pretrained(SRC, torch_dtype=torch.bfloat16).to(DEV).eval()
|
|
128
|
+
causal_served, causal_single, causal_batched = [], [], []
|
|
129
|
+
with torch.no_grad():
|
|
130
|
+
for q, d in PAIRS:
|
|
131
|
+
enc = encode_pair(out_tok, q, d, max_total_tokens=MAXLEN, device=DEV) # EXACT served path (OUT tok)
|
|
132
|
+
li = int(enc["attention_mask"][0].sum().item()) - 1
|
|
133
|
+
causal_served.append(c3(**enc).logits[0, li, TOKEN_ID].float().item())
|
|
134
|
+
texts = [format_pair(out_tok, q, d) for q, d in PAIRS]
|
|
135
|
+
for t in texts:
|
|
136
|
+
e = out_tok(t, return_tensors="pt", add_special_tokens=False).to(DEV)
|
|
137
|
+
causal_single.append(c3(**e).logits[0, -1, TOKEN_ID].float().item())
|
|
138
|
+
eb = out_tok(texts, return_tensors="pt", padding=True, add_special_tokens=False).to(DEV)
|
|
139
|
+
ob = c3(**eb).logits; last = eb["attention_mask"].sum(1) - 1
|
|
140
|
+
causal_batched = [ob[i, last[i], TOKEN_ID].float().item() for i in range(len(texts))]
|
|
141
|
+
del c3; gc.collect(); torch.cuda.empty_cache()
|
|
142
|
+
|
|
143
|
+
log("GATE2: loading seq-cls on GPU")
|
|
144
|
+
s3 = AutoModelForSequenceClassification.from_pretrained(OUT, num_labels=1, torch_dtype=torch.bfloat16).to(DEV).eval()
|
|
145
|
+
s3.config.pad_token_id = pad_id
|
|
146
|
+
seq_served, seq_single, seq_batched = [], [], []
|
|
147
|
+
with torch.no_grad():
|
|
148
|
+
for q, d in PAIRS:
|
|
149
|
+
enc = encode_pair(out_tok, q, d, max_total_tokens=MAXLEN, device=DEV) # EXACT served path (OUT tok)
|
|
150
|
+
seq_served.append(s3(**enc).logits.reshape(-1)[0].float().item())
|
|
151
|
+
for t in texts:
|
|
152
|
+
e = out_tok(t, return_tensors="pt", add_special_tokens=False).to(DEV)
|
|
153
|
+
seq_single.append(s3(**e).logits.reshape(-1)[0].float().item())
|
|
154
|
+
eb = out_tok(texts, return_tensors="pt", padding=True, add_special_tokens=False).to(DEV)
|
|
155
|
+
seq_batched = s3(**eb).logits.reshape(-1).float().tolist()
|
|
156
|
+
del s3; gc.collect(); torch.cuda.empty_cache()
|
|
157
|
+
|
|
158
|
+
def sig(x): return 1.0 / (1.0 + math.exp(-x / TEMP))
|
|
159
|
+
max_served = max_eqB = max_eqS = max_dS = max_dC = 0.0
|
|
160
|
+
log("idx servd(prod) seqServd eqServed eqB|seqB-causalB| eqS|seq1-causal1| driftSeq driftCausal score")
|
|
161
|
+
for i, (q, d) in enumerate(PAIRS):
|
|
162
|
+
eqSrv = abs(seq_served[i] - causal_served[i]) # PRODUCTION path fidelity (OUT tok + encode_pair, batch=1)
|
|
163
|
+
eqB = abs(seq_batched[i] - causal_batched[i]); eqS = abs(seq_single[i] - causal_single[i])
|
|
164
|
+
dS = abs(seq_batched[i] - seq_single[i]); dC = abs(causal_batched[i] - causal_single[i])
|
|
165
|
+
max_served = max(max_served, eqSrv); max_eqB = max(max_eqB, eqB); max_eqS = max(max_eqS, eqS)
|
|
166
|
+
max_dS = max(max_dS, dS); max_dC = max(max_dC, dC)
|
|
167
|
+
log(f"{i:>3} {causal_served[i]:+8.3f} {seq_served[i]:+8.3f} {eqSrv:.1e} {eqB:.1e} {eqS:.1e} {dS:.1e} {dC:.1e} {sig(seq_served[i]):.4f}")
|
|
168
|
+
|
|
169
|
+
TOL = 1e-3
|
|
170
|
+
gate2 = (max_served < TOL) and (max_eqB < TOL) and (max_eqS < TOL)
|
|
171
|
+
log(f"GATE2 FIDELITY served={max_served:.2e} batched={max_eqB:.2e} single={max_eqS:.2e} TOL={TOL} -> {'PASS' if gate2 else 'FAIL'}")
|
|
172
|
+
log(f"INFO backbone padding drift max|seqB-seq1|={max_dS:.2e} ~= max|causalB-causal1|={max_dC:.2e} (shared bf16 right-pad)")
|
|
173
|
+
log(f"SANITY relevant>irrelevant: {sig(seq_served[0]):.4f} > {sig(seq_served[1]):.4f} -> {'OK' if seq_served[0] > seq_served[1] else 'WARN'}")
|
|
174
|
+
allpass = gate1 and tokpath_ok and ep_ok and regex_ok and ap_ok and gate2
|
|
175
|
+
log(f"GATES gate1={gate1} tokpath={tokpath_ok} encode_pair_id={ep_ok} fix_mistral_regex={regex_ok} assistant_prefix={ap_ok} gate2={gate2}")
|
|
176
|
+
print("RESULT", "PASS" if allpass else "FAIL", flush=True)
|
|
177
|
+
sys.exit(0 if allpass else 3)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ClawMem zerank-2 seq-cls reranker sidecar.
|
|
2
|
+
#
|
|
3
|
+
# 1. Build: docker compose build
|
|
4
|
+
# 2. Convert + verify: HF_TOKEN=hf_xxx docker compose run --rm convert
|
|
5
|
+
# 3. Serve on :8090: docker compose up -d reranker
|
|
6
|
+
#
|
|
7
|
+
# Then point ClawMem at it: export CLAWMEM_RERANK_URL=http://<this-host>:8090
|
|
8
|
+
#
|
|
9
|
+
# Requires the NVIDIA Container Toolkit. zerank-2 weights are CC-BY-NC-4.0 (non-commercial);
|
|
10
|
+
# accept the model license on Hugging Face and pass HF_TOKEN for the one-off convert step.
|
|
11
|
+
x-gpu: &gpu
|
|
12
|
+
deploy:
|
|
13
|
+
resources:
|
|
14
|
+
reservations:
|
|
15
|
+
devices:
|
|
16
|
+
- driver: nvidia
|
|
17
|
+
count: all
|
|
18
|
+
capabilities: [gpu]
|
|
19
|
+
|
|
20
|
+
services:
|
|
21
|
+
reranker: # the live sidecar — serves the verified model on :8090
|
|
22
|
+
build: .
|
|
23
|
+
image: clawmem-zerank-2-seq
|
|
24
|
+
restart: unless-stopped
|
|
25
|
+
ports:
|
|
26
|
+
- "8090:8090"
|
|
27
|
+
volumes:
|
|
28
|
+
- ./models:/models
|
|
29
|
+
environment:
|
|
30
|
+
ZR_OUT: /models/zerank-2-seq
|
|
31
|
+
ZR_PORT: "8090"
|
|
32
|
+
<<: *gpu
|
|
33
|
+
|
|
34
|
+
convert: # one-off: download + convert + run all gates into ./models
|
|
35
|
+
build: .
|
|
36
|
+
image: clawmem-zerank-2-seq
|
|
37
|
+
profiles: ["tools"]
|
|
38
|
+
environment:
|
|
39
|
+
ZR_SRC: zeroentropy/zerank-2-reranker
|
|
40
|
+
ZR_OUT: /models/zerank-2-seq
|
|
41
|
+
HF_TOKEN: ${HF_TOKEN:-}
|
|
42
|
+
volumes:
|
|
43
|
+
- ./models:/models
|
|
44
|
+
- hf-cache:/root/.cache/huggingface
|
|
45
|
+
command: ["python3", "build_and_verify.py"]
|
|
46
|
+
<<: *gpu
|
|
47
|
+
|
|
48
|
+
volumes:
|
|
49
|
+
hf-cache:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ClawMem zerank-2 seq-cls reranker sidecar. Drop-in for CLAWMEM_RERANK_URL.
|
|
3
|
+
POST /v1/rerank {query, documents:[...]} -> {results:[{index, relevance_score}]}.
|
|
4
|
+
Uses zr_common.encode_pair (the SAME path the build gate verified) -> served scores == gated scores by
|
|
5
|
+
construction, and the assistant prefix is never truncated. batch=1 -> deterministic, drift-free scores."""
|
|
6
|
+
import os, sys, math, torch
|
|
7
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
|
11
|
+
from zr_common import encode_pair
|
|
12
|
+
|
|
13
|
+
MODEL = os.environ.get("ZR_OUT", "/models/zerank-2-seq")
|
|
14
|
+
TEMP = float(os.environ.get("ZR_TEMP", "5.0"))
|
|
15
|
+
MAXLEN = int(os.environ.get("ZR_MAXLEN", "8192"))
|
|
16
|
+
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
|
17
|
+
|
|
18
|
+
tok = AutoTokenizer.from_pretrained(MODEL); tok.padding_side = "right"
|
|
19
|
+
model = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=1, torch_dtype=torch.bfloat16).to(DEV).eval()
|
|
20
|
+
if model.config.pad_token_id is None:
|
|
21
|
+
model.config.pad_token_id = tok.pad_token_id or tok.convert_tokens_to_ids("<|endoftext|>")
|
|
22
|
+
|
|
23
|
+
app = FastAPI()
|
|
24
|
+
|
|
25
|
+
class RerankReq(BaseModel):
|
|
26
|
+
query: str
|
|
27
|
+
documents: list[str]
|
|
28
|
+
|
|
29
|
+
@app.post("/v1/rerank")
|
|
30
|
+
@torch.no_grad()
|
|
31
|
+
def rerank(req: RerankReq):
|
|
32
|
+
results = []
|
|
33
|
+
for i, d in enumerate(req.documents): # batch=1: deterministic, no padding drift
|
|
34
|
+
enc = encode_pair(tok, req.query, d, max_total_tokens=MAXLEN, device=DEV)
|
|
35
|
+
lg = model(**enc).logits.reshape(-1)[0].float().item()
|
|
36
|
+
results.append({"index": i, "relevance_score": 1.0 / (1.0 + math.exp(-lg / TEMP))})
|
|
37
|
+
return {"results": results}
|
|
38
|
+
|
|
39
|
+
@app.get("/v1/models")
|
|
40
|
+
def models():
|
|
41
|
+
return {"models": [{"id": "zerank-2-seq", "object": "model"}]}
|
|
42
|
+
|
|
43
|
+
@app.get("/health")
|
|
44
|
+
def health():
|
|
45
|
+
return {"status": "ok", "model": MODEL, "device": DEV}
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
import uvicorn
|
|
49
|
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("ZR_PORT", "8090")))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared formatting + token-budgeting for the zerank-2 seq-cls reranker.
|
|
3
|
+
Imported by BOTH build_and_verify.py (the gate) and server.py (serving) so the gated path and the
|
|
4
|
+
served path are identical BY CONSTRUCTION.
|
|
5
|
+
|
|
6
|
+
Guarantees the assistant-generation prefix is NEVER truncated: truncating it would move the scored/pooled
|
|
7
|
+
position off the assistant prefix -> wrong score. We right-truncate the DOCUMENT
|
|
8
|
+
tail (then the QUERY tail if needed) BEFORE templating, leaving room for the template scaffold + assistant
|
|
9
|
+
prefix, with a final left-truncation belt so the prefix (always at the end) survives any re-encode overshoot."""
|
|
10
|
+
|
|
11
|
+
ASSISTANT_PREFIX = "<|im_start|>assistant\n"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_pair(tok, query, document):
|
|
15
|
+
return tok.apply_chat_template(
|
|
16
|
+
[{"role": "query", "content": query}, {"role": "document", "content": document}],
|
|
17
|
+
tokenize=False, add_generation_prompt=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _ids(tok, text):
|
|
21
|
+
return tok(text, add_special_tokens=False)["input_ids"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def encode_pair(tok, query, document, max_total_tokens=8192, device="cpu", safety=16):
|
|
25
|
+
"""Return a tokenized (query, document) pair whose total length <= max_total_tokens and whose tail is
|
|
26
|
+
always the assistant prefix. Truncates document tail first, then query tail, then (belt) left-truncates."""
|
|
27
|
+
scaffold = len(_ids(tok, format_pair(tok, "", "")))
|
|
28
|
+
avail = max(2, max_total_tokens - scaffold - safety)
|
|
29
|
+
q_ids = _ids(tok, query)
|
|
30
|
+
if len(q_ids) > avail - 1: # cap query so a document always has room
|
|
31
|
+
q_ids = q_ids[:avail - 1]
|
|
32
|
+
query = tok.decode(q_ids)
|
|
33
|
+
d_budget = max(1, avail - len(q_ids))
|
|
34
|
+
d_ids = _ids(tok, document)
|
|
35
|
+
if len(d_ids) > d_budget: # truncate the document TAIL (least-important part)
|
|
36
|
+
d_ids = d_ids[:d_budget]
|
|
37
|
+
document = tok.decode(d_ids)
|
|
38
|
+
text = format_pair(tok, query, document)
|
|
39
|
+
enc = tok(text, return_tensors="pt", add_special_tokens=False)
|
|
40
|
+
if enc["input_ids"].shape[1] > max_total_tokens: # belt: keep the LAST tokens -> assistant prefix survives
|
|
41
|
+
enc = {k: v[:, -max_total_tokens:] for k, v in enc.items()}
|
|
42
|
+
return {k: v.to(device) for k, v in enc.items()}
|
|
43
|
+
return enc.to(device)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"src/",
|
|
12
|
+
"extras/",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE",
|
|
14
15
|
"CLAUDE.md",
|
package/src/mcp.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
type EnrichedResult,
|
|
32
32
|
type CoActivationFn,
|
|
33
33
|
} from "./memory.ts";
|
|
34
|
-
import { enrichResults, reciprocalRankFusion, toRanked, type RankedResult } from "./search-utils.ts";
|
|
34
|
+
import { enrichResults, reciprocalRankFusion, toRanked, blendRerank, type RankedResult } from "./search-utils.ts";
|
|
35
35
|
import { applyMMRDiversity } from "./mmr.ts";
|
|
36
36
|
import { indexCollection, type IndexStats } from "./indexer.ts";
|
|
37
37
|
import { listCollections } from "./collections.ts";
|
|
@@ -753,19 +753,21 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
753
753
|
return { file: c.file, text };
|
|
754
754
|
});
|
|
755
755
|
|
|
756
|
-
|
|
756
|
+
let reranked: { file: string; score: number }[] = [];
|
|
757
|
+
try {
|
|
758
|
+
reranked = await store.rerank(query, chunksToRerank, DEFAULT_RERANK_MODEL, intent);
|
|
759
|
+
} catch {
|
|
760
|
+
reranked = []; // reranker unavailable → blendRerank falls back to pure RRF order
|
|
761
|
+
}
|
|
757
762
|
|
|
758
763
|
const candidateMap = new Map(candidates.map(c => [c.file, c]));
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return { file: r.file, score: blendedScore };
|
|
767
|
-
});
|
|
768
|
-
blended.sort((a, b) => b.score - a.score);
|
|
764
|
+
|
|
765
|
+
// Blend the reranker (dominant signal) with a thin normalized-RRF tiebreaker; falls back
|
|
766
|
+
// to pure RRF order when the reranker is unavailable / all-zero. See blendRerank: the
|
|
767
|
+
// prior w·(1/rrfRank) blend made RRF rank-1 immovable by the reranker. Harness-validated
|
|
768
|
+
// 2026-06-25 (NL+KW known-item recall): lifts recall@1-5 + MRR@10 with no material pooled
|
|
769
|
+
// recall@10 regression.
|
|
770
|
+
const blended = blendRerank(candidates, reranked);
|
|
769
771
|
|
|
770
772
|
// Map to SearchResults for composite scoring — hydrate from DB when needed
|
|
771
773
|
const allSearchResults = [...store.searchFTS(query, 30)];
|
package/src/search-utils.ts
CHANGED
|
@@ -113,6 +113,51 @@ export function reciprocalRankFusion(
|
|
|
113
113
|
.map(v => ({ ...v.result, score: v.score }));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Rerank / RRF Score Blend
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Blend a cross-encoder reranker's scores with the upstream RRF ranking.
|
|
122
|
+
*
|
|
123
|
+
* The reranker is the dominant relevance signal; normalized RRF contributes a thin tiebreaker.
|
|
124
|
+
* An earlier blend used `w·(1/rrfRank)` with `w≥0.75` on the top tier, which made RRF rank-1
|
|
125
|
+
* mathematically immovable by the reranker (0.75·(1/1) exceeds any rank-2 ceiling) — the
|
|
126
|
+
* reranker could never promote the best document to the top. This blend normalizes the RRF
|
|
127
|
+
* score to [0,1] and gives the reranker the dominant weight, so a strong rerank score CAN
|
|
128
|
+
* promote a doc over RRF #1. Harness-validated 2026-06-25 against NL+KW known-item recall:
|
|
129
|
+
* lifts recall@1-5 and MRR@10 with no material pooled recall@10 regression.
|
|
130
|
+
*
|
|
131
|
+
* Falls back to pure RRF order when the reranker is unavailable or returned no usable signal
|
|
132
|
+
* (empty, or all-zero — e.g. a total remote+local failure). Maps over `candidates` (not the
|
|
133
|
+
* rerank output) so partial rerank coverage can never drop a candidate; an unscored doc takes
|
|
134
|
+
* rerank score 0 (so it sorts on its thin `(1-rerankWeight)·rrfNorm` term) and unscored docs
|
|
135
|
+
* preserve their relative RRF order among themselves.
|
|
136
|
+
*
|
|
137
|
+
* @param candidates - RRF-ordered candidates; `score` is the RRF fusion score (positive).
|
|
138
|
+
* @param reranked - reranker output `{file, score in [0,1]}`; may be empty/partial/all-zero.
|
|
139
|
+
* @param rerankWeight - weight on the reranker term (default 0.9; `1-rerankWeight` on RRF).
|
|
140
|
+
* @returns candidates re-scored and sorted by blended score descending.
|
|
141
|
+
*/
|
|
142
|
+
export function blendRerank(
|
|
143
|
+
candidates: { file: string; score: number }[],
|
|
144
|
+
reranked: { file: string; score: number }[],
|
|
145
|
+
rerankWeight: number = 0.9
|
|
146
|
+
): { file: string; score: number }[] {
|
|
147
|
+
const rerankScoreMap = new Map(reranked.map(r => [r.file, r.score]));
|
|
148
|
+
const rerankUsable = reranked.length > 0 && reranked.some(r => Number.isFinite(r.score) && r.score > 0);
|
|
149
|
+
const maxRrf = candidates.reduce((m, c) => Math.max(m, c.score), 0) || 1;
|
|
150
|
+
return candidates
|
|
151
|
+
.map(c => {
|
|
152
|
+
const rrfNorm = c.score / maxRrf; // [0,1]
|
|
153
|
+
if (!rerankUsable) return { file: c.file, score: rrfNorm };
|
|
154
|
+
const rr = rerankScoreMap.get(c.file);
|
|
155
|
+
const rerankScore = Number.isFinite(rr) ? (rr as number) : 0;
|
|
156
|
+
return { file: c.file, score: (1 - rerankWeight) * rrfNorm + rerankWeight * rerankScore };
|
|
157
|
+
})
|
|
158
|
+
.sort((a, b) => b.score - a.score);
|
|
159
|
+
}
|
|
160
|
+
|
|
116
161
|
/**
|
|
117
162
|
* Convert a SearchResult to a RankedResult for use in RRF.
|
|
118
163
|
*/
|