@tznthou/ccrecall 0.9.2 → 0.10.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/README.md +22 -8
- package/README_ZH.md +22 -8
- package/package.json +1 -1
- package/scripts/backfill-extract.sh +35 -7
- package/scripts/extraction-prompt.md +3 -2
- package/scripts/post-session-extract.sh +123 -15
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
[中文版](README_ZH.md)
|
|
9
9
|
|
|
10
|
-
A local memory service for Claude Code — indexes your conversation history, recalls relevant context on demand, and injects it into future sessions. Zero API cost for the core service
|
|
10
|
+
A local memory service for Claude Code — indexes your conversation history, recalls relevant context on demand, and injects it into future sessions. Zero API cost for the core service. Optional post-session extraction runs on Sonnet: plan usage on a Claude subscription, or $0.21–0.39 per extraction (measured) with an API key.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ Five stances, non-negotiable:
|
|
|
18
18
|
1. **Local-first.** Your machine, your data. No cloud, no accounts, no services beyond localhost.
|
|
19
19
|
2. **SQLite is the API.** One `.db` file. `sqlite3` to inspect anything. Your memories are never a black box.
|
|
20
20
|
3. **FTS5, not vectors.** Code conversations search for file paths, error messages, tool names — keywords, not vibes. At hundreds of sessions, embeddings solve a problem that doesn't exist.
|
|
21
|
-
4. **LLM distills, human curates.**
|
|
21
|
+
4. **LLM distills, human curates.** Sonnet extracts post-session (plan usage on a subscription, $0.21–0.39 a run on an API key). Auto memory holds decisions you name. ccRecall holds the long tail no one writes down by hand.
|
|
22
22
|
5. **Read-only toward `~/.claude/`.** Reads your JSONL logs, never writes to Claude Code's state. Worst case: bad search results. Never: a broken setup.
|
|
23
23
|
|
|
24
24
|
---
|
|
@@ -96,7 +96,7 @@ flowchart TB
|
|
|
96
96
|
|
|
97
97
|
subgraph Consumers["Consumers"]
|
|
98
98
|
Hook["Claude Code Hooks<br/>SessionStart / UserPromptSubmit / SessionEnd"]
|
|
99
|
-
Wrapper["Extraction wrapper<br/>post-session
|
|
99
|
+
Wrapper["Extraction wrapper<br/>post-session Sonnet pass"]
|
|
100
100
|
MCP["MCP Server<br/>recall_query / recall_save"]
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -119,7 +119,7 @@ sequenceDiagram
|
|
|
119
119
|
participant H as Hooks
|
|
120
120
|
participant R as ccRecall
|
|
121
121
|
participant W as Wrapper
|
|
122
|
-
participant X as
|
|
122
|
+
participant X as Sonnet extractor
|
|
123
123
|
|
|
124
124
|
rect rgb(235, 244, 255)
|
|
125
125
|
Note over CC,R: Session start
|
|
@@ -163,7 +163,7 @@ The `notBefore` gate keeps a stale session from being extracted twice; the subag
|
|
|
163
163
|
| FTS5 | Full-text search | Built into SQLite, trigram tokenizer with LIKE fallback for short CJK / mixed-script queries |
|
|
164
164
|
| Native `http` | HTTP server | No Express — minimal surface, localhost only |
|
|
165
165
|
| chokidar | Filesystem watcher | Cross-platform JSONL change detection with 2 s debounce + single-flight |
|
|
166
|
-
| vitest | Testing |
|
|
166
|
+
| vitest | Testing | 858 tests across 54 files, integration-style |
|
|
167
167
|
| `@modelcontextprotocol/sdk` | MCP server | stdio transport, shared SQLite via WAL |
|
|
168
168
|
|
|
169
169
|
---
|
|
@@ -281,7 +281,7 @@ ccRecall lives alongside Claude Code's built-in auto memory (`~/.claude/projects
|
|
|
281
281
|
|
|
282
282
|
| | auto memory | ccRecall |
|
|
283
283
|
|---|---|---|
|
|
284
|
-
| **Write path** | Claude curates by hand — new `.md` file + MEMORY.md index line | Two tracks: rule-based session summaries (automatic, free) + opt-in
|
|
284
|
+
| **Write path** | Claude curates by hand — new `.md` file + MEMORY.md index line | Two tracks: rule-based session summaries (automatic, free) + opt-in Sonnet extraction (plan usage, or $0.21–0.39 a run on an API key; where most searchable memories come from) |
|
|
285
285
|
| **Read path** | Always in session context (MEMORY.md loads at session start) | On-demand MCP query when auto memory has no entry |
|
|
286
286
|
| **Signal density** | High — facts worth naming | Long tail — everything the hook can extract |
|
|
287
287
|
| **Typical use** | "Remember X" / "always Y" — durable preferences, decisions | "Didn't we fix that?" / "last time" — cross-session recall |
|
|
@@ -395,7 +395,7 @@ ccRecall/
|
|
|
395
395
|
│ ├── tutorial.md # End-user walkthrough (install → MCP → usage)
|
|
396
396
|
│ ├── architecture.md # Daemon design rationale (contributor-oriented)
|
|
397
397
|
│ └── launchd.md # macOS LaunchAgent install/troubleshoot
|
|
398
|
-
├── tests/ #
|
|
398
|
+
├── tests/ # 858 tests across 54 files (parser, scanner,
|
|
399
399
|
│ │ # summarizer, database, indexer, e2e, MCP,
|
|
400
400
|
│ │ # memories, hooks, watcher, CLI, migrations,
|
|
401
401
|
│ │ # FTS5 CJK edge cases, integrity monitor, ...)
|
|
@@ -426,7 +426,7 @@ The real trigger was simpler: I kept re-explaining the same architecture to Clau
|
|
|
426
426
|
|
|
427
427
|
Each flows from the manifesto. Each was a conscious choice against a popular alternative.
|
|
428
428
|
|
|
429
|
-
**Rule-based, with a ceiling.** The daemon uses heuristic extraction (regex patterns, tool usage analysis, outcome inference) for session summaries — zero API cost. For that job, "Edit x8, 5 files, committed" is more useful than a paragraph of prose. But rule-based has a ceiling: it handles structured signals (tool calls, file edits, commit messages) well; it misses discussion-only decisions, nuanced trade-offs, and anything that lives in natural language. That ceiling is why v0.4.1 added post-session Haiku extraction (~$0.001/session) — an LLM pass that catches what heuristics can't. In practice, the memories I actually search for mostly come from
|
|
429
|
+
**Rule-based, with a ceiling.** The daemon uses heuristic extraction (regex patterns, tool usage analysis, outcome inference) for session summaries — zero API cost. For that job, "Edit x8, 5 files, committed" is more useful than a paragraph of prose. But rule-based has a ceiling: it handles structured signals (tool calls, file edits, commit messages) well; it misses discussion-only decisions, nuanced trade-offs, and anything that lives in natural language. That ceiling is why v0.4.1 added post-session Haiku extraction (~$0.001/session) — an LLM pass that catches what heuristics can't. In practice, the memories I actually search for mostly come from that post-session pass (on Sonnet since v0.9.0) or manual `recall_save`, not the rule-based layer. The daemon itself never calls an LLM; the LLM pass is opt-in and runs outside the daemon as a separate process.
|
|
430
430
|
|
|
431
431
|
**FTS5, not vector search.** Semantic search sounds better on paper, but for conversation logs — specific tools, file paths, error messages — keyword matching wins. FTS5 queries run in <10ms locally. No embedding model, no Chroma, no Docker container. At hundreds of sessions (not millions of documents), Karpathy's own analysis confirms: "plain index + keyword search is already sufficient under 500 sources."
|
|
432
432
|
|
|
@@ -438,6 +438,15 @@ Each flows from the manifesto. Each was a conscious choice against a popular alt
|
|
|
438
438
|
|
|
439
439
|
**No opinionated injection.** ccRecall doesn't decide what Claude should remember. It provides a search API — the injection layer presents results, Claude integrates them. Opinionated memory selection is a premature optimization that would be wrong in ways we can't predict.
|
|
440
440
|
|
|
441
|
+
### What the transcript doesn't say
|
|
442
|
+
|
|
443
|
+
ccRecall knows only what Claude Code writes to disk, and the transcript is not quite the conversation. Neither of the gaps v0.9.2 closed ever raised an error:
|
|
444
|
+
|
|
445
|
+
- A message typed while Claude is working usually gets folded into Claude's next API call and stored as an attachment, with no user message on record. The extraction transcript never read those — 254 of them in a month of one person's sessions — until v0.9.2.
|
|
446
|
+
- Claude Code 2.1.300 began writing a copy of each API request into the transcript, and the parser counted those records as messages until v0.9.2.
|
|
447
|
+
|
|
448
|
+
The code reading the transcript was right for the format it assumed. Both gaps turned up by counting what the file holds against what was really said.
|
|
449
|
+
|
|
441
450
|
---
|
|
442
451
|
|
|
443
452
|
## Roadmap
|
|
@@ -449,6 +458,11 @@ Each flows from the manifesto. Each was a conscious choice against a popular alt
|
|
|
449
458
|
| **v0.5.0** | The knife field: journal/scorer/harvester pipelines removed (zero promotions ever), endpoints 13 → 5, `message_uuids` dual-hash rebuild (DB 114MB → 42MB) | Released |
|
|
450
459
|
| **v0.5.2** | Recall weighting: log-compressed half-life decay, FTS relevance-first ranking via `(-rank)*sqrt(EC)` | Released |
|
|
451
460
|
| **v0.5.3** | CJK topic alignment: Han-aware topic extraction (`\p{Script=Han}` tokenizer + 32 Chinese stopwords + particle split), session-less rebuild fix | Released |
|
|
461
|
+
| **v0.5.4–v0.5.6** | Measuring injection: an `injection_log` table records every memory surfaced (#71), Tier 0 startup slots rotate instead of showing the same three memories every session, `/health` reports `mainSessionCount` | Released |
|
|
462
|
+
| **v0.6.0** | Mid-conversation recall: a `UserPromptSubmit` hook surfaces memories while you work, injected lines carry their key, and extraction flags runs that printed `recall_save` instead of calling it | Released |
|
|
463
|
+
| **v0.7.x** | CJK word segmentation via `Intl.Segmenter` (#80); the `cwd` → project id encoding drift that silently skipped extraction for whole projects (#89); English function words no longer run the topic index (#84); the telemetry log guard's leaks closed | Released |
|
|
464
|
+
| **v0.8.x** | Provenance: a memory records the message it came from and who wrote it, so extraction can't overwrite one you saved by hand; prescription-first extraction; Tier 0 picks other projects' memories per project instead of by confidence alone. Long sessions go to extraction over stdin instead of failing silently, and `backfill-extract.sh` recovers the ones already lost | Released |
|
|
465
|
+
| **v0.9.x** | Extraction on Sonnet, retried once when a run produces nothing usable; no more skipped sessions when a path's letter case differs (#131); Claude Code 2.1.300's request-replay records skipped, and messages sent mid-turn now reach extraction; extraction runs without memories injected into it; `recall_save` no longer hits "database is locked" | Released |
|
|
452
466
|
|
|
453
467
|
Tracked in [GitHub Issues](https://github.com/tznthou/ccRecall/issues).
|
|
454
468
|
|
package/README_ZH.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
[English](README.md)
|
|
9
9
|
|
|
10
|
-
Claude Code 的本地記憶服務——索引你的對話歷史,按需召回相關 context,注入到未來的 session。核心服務零 API
|
|
10
|
+
Claude Code 的本地記憶服務——索引你的對話歷史,按需召回相關 context,注入到未來的 session。核心服務零 API 成本。選配的 post-session extraction 跑在 Sonnet 上:用 Claude 訂閱方案就算進方案用量;用 API key 的話,我們實測每次抽取 0.21–0.39 美元。
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ Claude Code 的本地記憶服務——索引你的對話歷史,按需召回
|
|
|
18
18
|
1. **本地優先。** 你的機器,你的資料。不上雲、不開帳號、不信任 localhost 以外的任何服務。
|
|
19
19
|
2. **SQLite 就是 API。** 一個 `.db` 檔案。`sqlite3` 隨時可查。你的記憶從不是黑箱。
|
|
20
20
|
3. **FTS5,不用向量。** 程式碼對話搜的是檔案路徑、錯誤訊息、工具名稱——關鍵詞就夠了。幾百個 session 的規模,embedding 解決的是不存在的問題。
|
|
21
|
-
4. **LLM 蒸餾,人類策展。**
|
|
21
|
+
4. **LLM 蒸餾,人類策展。** Sonnet 在 session 結束後自動萃取(訂閱方案吃方案用量,API key 實測每次 0.21–0.39 美元)。auto memory 存你親手命名的決策。ccRecall 存沒人會手寫的長尾。
|
|
22
22
|
5. **對 `~/.claude/` 唯讀。** 讀 JSONL 記錄,從不寫入 Claude Code 的狀態。最壞情況:搜尋結果不好。永遠不會:搞壞你的設定。
|
|
23
23
|
|
|
24
24
|
---
|
|
@@ -94,7 +94,7 @@ flowchart TB
|
|
|
94
94
|
|
|
95
95
|
subgraph Consumers["使用端"]
|
|
96
96
|
Hook["Claude Code Hooks<br/>SessionStart / UserPromptSubmit / SessionEnd"]
|
|
97
|
-
Wrapper["Extraction wrapper<br/>session 結束後的
|
|
97
|
+
Wrapper["Extraction wrapper<br/>session 結束後的 Sonnet 抽取"]
|
|
98
98
|
MCP["MCP Server<br/>recall_query / recall_save"]
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -117,7 +117,7 @@ sequenceDiagram
|
|
|
117
117
|
participant H as Hooks
|
|
118
118
|
participant R as ccRecall
|
|
119
119
|
participant W as Wrapper
|
|
120
|
-
participant X as
|
|
120
|
+
participant X as Sonnet 抽取器
|
|
121
121
|
|
|
122
122
|
rect rgb(235, 244, 255)
|
|
123
123
|
Note over CC,R: Session 開始
|
|
@@ -161,7 +161,7 @@ sequenceDiagram
|
|
|
161
161
|
| FTS5 | 全文搜尋 | SQLite 內建、trigram tokenizer,短 token / 中英混合查詢透過 LIKE fallback 補齊 |
|
|
162
162
|
| 原生 `http` | HTTP 伺服器 | 不用 Express——最小表面積、僅 localhost |
|
|
163
163
|
| chokidar | 檔案系統 watcher | 跨平台 JSONL 變動偵測,2 秒 debounce + single-flight |
|
|
164
|
-
| vitest | 測試 |
|
|
164
|
+
| vitest | 測試 | 858 個測試(54 檔案)、整合式風格 |
|
|
165
165
|
| `@modelcontextprotocol/sdk` | MCP server | stdio transport,透過 WAL 共用 SQLite |
|
|
166
166
|
|
|
167
167
|
---
|
|
@@ -276,7 +276,7 @@ ccRecall 和 Claude Code 內建的 auto memory(`~/.claude/projects/*/memory/`
|
|
|
276
276
|
|
|
277
277
|
| | auto memory | ccRecall |
|
|
278
278
|
|---|---|---|
|
|
279
|
-
| **寫入路徑** | Claude 手動策展——新開一個 `.md` 檔 + 更新 MEMORY.md index | 雙軌:規則式 session 摘要(自動、免費)+ 選配
|
|
279
|
+
| **寫入路徑** | Claude 手動策展——新開一個 `.md` 檔 + 更新 MEMORY.md index | 雙軌:規則式 session 摘要(自動、免費)+ 選配 Sonnet extraction(訂閱方案算進用量,API key 實測每次 0.21–0.39 美元;可搜尋的記憶主要來自這條路) |
|
|
280
280
|
| **讀取路徑** | 永遠在 session context(MEMORY.md 啟動時就載入) | auto memory 沒答案時,才用 MCP 查詢 |
|
|
281
281
|
| **訊號密度** | 高——值得被命名的決策和偏好 | 長尾——hook 能抓到的都留著 |
|
|
282
282
|
| **適用情境** | 「記住 X」「以後都 Y」——重要偏好、明確決策 | 「上次那個怎麼修的?」——跨多個 session 的回憶 |
|
|
@@ -385,7 +385,7 @@ ccRecall/
|
|
|
385
385
|
│ ├── tutorial_zh.md # 使用者教學(安裝 → MCP → 日常使用)
|
|
386
386
|
│ ├── architecture_zh.md # Daemon 設計取捨(給 contributor 看)
|
|
387
387
|
│ └── launchd.md # macOS LaunchAgent 安裝/troubleshoot
|
|
388
|
-
├── tests/ #
|
|
388
|
+
├── tests/ # 858 個測試橫跨 54 檔案(parser、scanner、
|
|
389
389
|
│ │ # summarizer、database、indexer、e2e、MCP、
|
|
390
390
|
│ │ # memories、hooks、watcher、CLI、migrations、
|
|
391
391
|
│ │ # FTS5 CJK edge cases、integrity monitor 等)
|
|
@@ -416,7 +416,7 @@ Anthropic Claude Code 團隊的 Thariq 在 2026 年 4 月[發表了 context 管
|
|
|
416
416
|
|
|
417
417
|
每一條都從宣言延伸。每一條都是對流行做法的刻意拒絕。
|
|
418
418
|
|
|
419
|
-
**規則式,有天花板。** daemon 用 heuristic 萃取(regex 模式、工具使用分析、outcome 推斷)做 session 摘要——零 API 成本。做這件事,「Edit×8, 5 files, committed」比一段散文更有用。但 rule-based 有天花板:結構化訊號(tool call、檔案編輯、commit message)抓得到;討論中的決策、微妙的取捨、任何活在自然語言裡的東西,它抓不到。這就是 v0.4.1 加入 post-session Haiku extraction(~$0.001/session)的原因——用一次 LLM pass 補上 heuristic 的盲區。實際上,我真正會搜到的記憶大多來自
|
|
419
|
+
**規則式,有天花板。** daemon 用 heuristic 萃取(regex 模式、工具使用分析、outcome 推斷)做 session 摘要——零 API 成本。做這件事,「Edit×8, 5 files, committed」比一段散文更有用。但 rule-based 有天花板:結構化訊號(tool call、檔案編輯、commit message)抓得到;討論中的決策、微妙的取捨、任何活在自然語言裡的東西,它抓不到。這就是 v0.4.1 加入 post-session Haiku extraction(~$0.001/session)的原因——用一次 LLM pass 補上 heuristic 的盲區。實際上,我真正會搜到的記憶大多來自 session 結束後的那次抽取(v0.9.0 起改用 Sonnet)或手動 `recall_save`,不是規則式那層。daemon 本體永遠不呼叫 LLM;LLM pass 是選配的,作為獨立進程在 daemon 之外執行。
|
|
420
420
|
|
|
421
421
|
**FTS5,不用向量搜尋。** 語義搜尋聽起來更高級,但對話記錄搜的是具體的工具名、檔案路徑、錯誤訊息——關鍵詞匹配就夠了。FTS5 查詢在本地 <10ms。不需要 embedding model、不需要 Chroma、不需要 Docker container。在我們的規模(數百個 session,不是百萬文件),Karpathy 自己的分析也確認:「500 個來源以下,樸素索引 + 關鍵詞搜尋已經夠用。」
|
|
422
422
|
|
|
@@ -428,6 +428,15 @@ Anthropic Claude Code 團隊的 Thariq 在 2026 年 4 月[發表了 context 管
|
|
|
428
428
|
|
|
429
429
|
**不做帶偏見的注入。** ccRecall 不替 Claude 決定該記住什麼。它提供搜尋 API——注入層呈現結果,Claude 自己整合。帶偏見的記憶篩選是過早優化,而且會以我們無法預測的方式出錯。
|
|
430
430
|
|
|
431
|
+
### transcript 沒說的事
|
|
432
|
+
|
|
433
|
+
ccRecall 能知道的,只有 Claude Code 寫進硬碟的東西;可是 transcript 跟真正發生過的對話,並不完全是同一回事。v0.9.2 補上的兩個洞,都不曾報過錯:
|
|
434
|
+
|
|
435
|
+
- Claude 還在忙的時候打進去的訊息,多半會被併進它下一次的 API 呼叫,存成 attachment,不會留下使用者訊息的紀錄。抽取用的 transcript 一直沒讀到這些——光是一個人一個月的 session,就有 254 則——到 v0.9.2 才補上。
|
|
436
|
+
- Claude Code 2.1.300 開始把每次 API 請求的副本寫進 transcript,parser 一直把這些紀錄當成訊息算,到 v0.9.2 才擋掉。
|
|
437
|
+
|
|
438
|
+
讀 transcript 的程式碼,照它假設的格式來看並沒有寫錯;兩個洞都是拿檔案裡實際存了什麼,去對照實際說過什麼,一筆一筆數出來的。
|
|
439
|
+
|
|
431
440
|
---
|
|
432
441
|
|
|
433
442
|
## 路線圖
|
|
@@ -439,6 +448,11 @@ Anthropic Claude Code 團隊的 Thariq 在 2026 年 4 月[發表了 context 管
|
|
|
439
448
|
| **v0.5.0** | 砍刀場:journal/scorer/harvester 管線移除(史上零 promotion)、端點 13 → 5、`message_uuids` 雙 hash 重建(DB 114MB → 42MB) | 已釋出 |
|
|
440
449
|
| **v0.5.2** | 召回權重:對數壓縮 half-life decay、FTS relevance-first 排序 `(-rank)*sqrt(EC)` | 已釋出 |
|
|
441
450
|
| **v0.5.3** | CJK topic 對齊:Han-aware topic 抽取(`\p{Script=Han}` tokenizer + 中文 stopwords 32 條 + 助詞分割)、session-less rebuild 修正 | 已釋出 |
|
|
451
|
+
| **v0.5.4–v0.5.6** | 量測注入:`injection_log` 記下每一次送出的記憶(#71);Tier 0 的 startup 名額開始輪替,不再每場都是同樣三條;`/health` 改回報 `mainSessionCount` | 已釋出 |
|
|
452
|
+
| **v0.6.0** | 對話中召回:`UserPromptSubmit` hook 在對話進行中帶出相關記憶;注入的每一行都附上 key;抽取會標出「把 `recall_save` 印成文字、沒有真的呼叫」的 run | 已釋出 |
|
|
453
|
+
| **v0.7.x** | 用 `Intl.Segmenter` 做中文斷詞(#80);修掉 `cwd` → project id 的編碼漂移,這個 bug 讓整個專案的抽取被靜默跳過(#89);英文虛詞不再把持 topic 索引(#84);補上 telemetry log 防護的漏洞 | 已釋出 |
|
|
454
|
+
| **v0.8.x** | 出處與作者:記憶會記下出自哪則訊息、是誰寫的,自動抽取不會再蓋掉你手寫的記憶;抽取改成處方先行;Tier 0 挑別專案的記憶改依專案挑選,不再只看信心分數。長 session 改從 stdin 送進抽取,不再靜默失敗,另附 `backfill-extract.sh` 把已經漏掉的補回來 | 已釋出 |
|
|
455
|
+
| **v0.9.x** | 抽取改用 Sonnet,沒產出可用結果時自動重試一次;路徑大小寫不同也不會再跳過(#131);跳過 Claude Code 2.1.300 寫進 transcript 的請求副本,對話中途插打的訊息也讀得到了;抽取不再被注入記憶;`recall_save` 不再撞上「database is locked」 | 已釋出 |
|
|
442
456
|
|
|
443
457
|
追蹤於 [GitHub Issues](https://github.com/tznthou/ccRecall/issues)。
|
|
444
458
|
|
package/package.json
CHANGED
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
# --from-log-all every non-zero exit in the log — mostly ancient sessions
|
|
18
18
|
# whose transcript Claude Code has since deleted
|
|
19
19
|
#
|
|
20
|
+
# Environment:
|
|
21
|
+
# CCRECALL_EXTRACT_EFFORT effort for each run, as for ccrecall-extract:
|
|
22
|
+
# low, medium, high, xhigh or max (default high)
|
|
23
|
+
# CCRECALL_EXTRACT_MAX_BUDGET_USD spend cap per run, applied only when
|
|
24
|
+
# ANTHROPIC_API_KEY is set (default 0.50);
|
|
25
|
+
# raise it to run at max
|
|
26
|
+
#
|
|
20
27
|
# Why this exists (2026-09-15): post-session-extract.sh used to pass the whole
|
|
21
28
|
# transcript as one argv parameter (capped at 200,000 bytes), and a single argv
|
|
22
29
|
# parameter has a ceiling well below that cap:
|
|
@@ -91,9 +98,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
91
98
|
--force) FORCE=1 ;;
|
|
92
99
|
--from-log) FROM_LOG=1 ;;
|
|
93
100
|
--from-log-all) FROM_LOG=1; FROM_LOG_ALL=1 ;;
|
|
94
|
-
# Usage
|
|
95
|
-
# source; printing half of it (the range has to stop
|
|
96
|
-
|
|
101
|
+
# Usage, Options and Environment only. The rationale below them is for
|
|
102
|
+
# whoever reads the source; printing half of it (the range has to stop
|
|
103
|
+
# somewhere) helps nobody.
|
|
104
|
+
-h|--help) sed -n '2,25p' "$SELF"; exit 0 ;;
|
|
97
105
|
-*) die "unknown option: $1" ;;
|
|
98
106
|
*) SESSION_IDS+=("$1") ;;
|
|
99
107
|
esac
|
|
@@ -105,6 +113,11 @@ done
|
|
|
105
113
|
# shellcheck source=/dev/null
|
|
106
114
|
source "$WRAPPER" >/dev/null 2>&1 || die "failed to source $WRAPPER"
|
|
107
115
|
declare -F _ccrecall_log_append >/dev/null || die "_ccrecall_log_append not defined by $WRAPPER"
|
|
116
|
+
# The MCP configuration too, so a backfill run starts the same single server
|
|
117
|
+
# the wrapper's run does rather than a second copy of how to find it.
|
|
118
|
+
declare -F _ccrecall_extract_mcp_config >/dev/null || die "_ccrecall_extract_mcp_config not defined by $WRAPPER"
|
|
119
|
+
# And the effort, read from CCRECALL_EXTRACT_EFFORT the way the wrapper reads it.
|
|
120
|
+
declare -F _ccrecall_extract_effort >/dev/null || die "_ccrecall_extract_effort not defined by $WRAPPER"
|
|
108
121
|
|
|
109
122
|
[[ -f "$PROMPT_FILE" ]] || die "extraction-prompt.md not found: $PROMPT_FILE
|
|
110
123
|
(the wrapper's inline fallback is deliberately not duplicated here — a second
|
|
@@ -178,8 +191,11 @@ fi
|
|
|
178
191
|
|
|
179
192
|
[[ ${#SESSION_IDS[@]} -gt 0 ]] || die "no session ids given (pass ids, or --from-log)"
|
|
180
193
|
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
# Once for the whole batch, so a value that is not a level is warned about once.
|
|
195
|
+
EFFORT=$(_ccrecall_extract_effort)
|
|
196
|
+
|
|
197
|
+
printf '\n🧠 ccRecall backfill — %d session(s) (effort %s)%s\n\n' \
|
|
198
|
+
"${#SESSION_IDS[@]}" "$EFFORT" "$([[ $DRY_RUN -eq 1 ]] && printf ' [DRY RUN]')"
|
|
183
199
|
|
|
184
200
|
ok=0; skipped=0; failed=0
|
|
185
201
|
|
|
@@ -288,13 +304,25 @@ ${prompt}"
|
|
|
288
304
|
# same reason as in the wrapper: this is a headless Claude Code session of its
|
|
289
305
|
# own, and ccRecall's hooks would otherwise inject memories into it — and
|
|
290
306
|
# count each one as surfaced.
|
|
307
|
+
#
|
|
308
|
+
# Effort and permissions match the wrapper's run too, for the reasons given
|
|
309
|
+
# there: CLAUDE_CODE_EFFORT_LEVEL is set as a prefix because it outranks
|
|
310
|
+
# --effort (a caller exporting max ran each extraction at max, mostly
|
|
311
|
+
# thinking), and the run gets ccRecall's own MCP server, no built-in tools and
|
|
312
|
+
# an allowlist instead of --dangerously-skip-permissions.
|
|
313
|
+
mcp_config=$(_ccrecall_extract_mcp_config)
|
|
291
314
|
extract_stderr=$(cd "$session_cwd" && printf '%s' "$full_prompt" |
|
|
292
|
-
CCRECALL_SESSION_START_STRATEGY=off CCRECALL_PROMPT_RECALL=off
|
|
315
|
+
CCRECALL_SESSION_START_STRATEGY=off CCRECALL_PROMPT_RECALL=off CLAUDE_CODE_EFFORT_LEVEL="$EFFORT" \
|
|
316
|
+
command claude -p \
|
|
293
317
|
--no-session-persistence \
|
|
294
318
|
--model sonnet \
|
|
295
319
|
${budget_args[@]+"${budget_args[@]}"} \
|
|
296
320
|
--max-turns 5 \
|
|
297
|
-
--
|
|
321
|
+
--strict-mcp-config --mcp-config "$mcp_config" \
|
|
322
|
+
--tools "" \
|
|
323
|
+
--allowedTools "mcp__ccrecall__recall_save,mcp__ccrecall__recall_query" \
|
|
324
|
+
--permission-mode dontAsk \
|
|
325
|
+
--disable-slash-commands 2>&1 1>/dev/null)
|
|
298
326
|
extract_exit=$?
|
|
299
327
|
duration=$(( $(date +%s) - start ))
|
|
300
328
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
You are a memory extraction agent for ccRecall, running as an automated
|
|
2
2
|
non-interactive pipeline. There is NO human reading your text output — only
|
|
3
3
|
tool calls matter. Analyze the session transcript and save 0-5 lasting
|
|
4
|
-
insights
|
|
4
|
+
insights by calling the `mcp__ccrecall__recall_save` tool — use that exact
|
|
5
|
+
name; a bare `recall_save` does not exist in this run. Produce no other output.
|
|
5
6
|
|
|
6
7
|
## What to extract
|
|
7
8
|
|
|
@@ -129,7 +130,7 @@ expressed. Copy it verbatim out of the header.
|
|
|
129
130
|
|
|
130
131
|
## recall_save parameters
|
|
131
132
|
|
|
132
|
-
For each memory, call `
|
|
133
|
+
For each memory, call `mcp__ccrecall__recall_save` with:
|
|
133
134
|
- `content`: the self-contained insight (see rules above)
|
|
134
135
|
- `type`: one of `decision`, `discovery`, `pattern`, `preference`, `feedback`
|
|
135
136
|
- `key`: stable hyphenated slug (see above)
|
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Usage: ccrecall-extract [claude args...]
|
|
7
7
|
# Runs Claude Code with ccRecall startup injection, then extracts
|
|
8
|
-
# memories
|
|
8
|
+
# memories with Sonnet after the session ends.
|
|
9
9
|
#
|
|
10
10
|
# Environment:
|
|
11
|
-
# CCRECALL_PORT
|
|
12
|
-
# CCRECALL_SKIP_EXTRACT
|
|
13
|
-
# CCRECALL_EXTRACT_LOG
|
|
11
|
+
# CCRECALL_PORT — daemon port (default 7749)
|
|
12
|
+
# CCRECALL_SKIP_EXTRACT — set to 1 to skip post-session extraction
|
|
13
|
+
# CCRECALL_EXTRACT_LOG — telemetry log path (default ~/.ccrecall/extract.log.jsonl)
|
|
14
|
+
# CCRECALL_EXTRACT_EFFORT — effort for the extraction run: low, medium, high,
|
|
15
|
+
# xhigh or max (default high). Your interactive
|
|
16
|
+
# session keeps its own effort either way.
|
|
17
|
+
# CCRECALL_EXTRACT_MAX_BUDGET_USD — spend cap per extraction run, applied only
|
|
18
|
+
# when ANTHROPIC_API_KEY is set (default 0.50).
|
|
19
|
+
# Measured runs at max cost 0.64-0.97, so raise it
|
|
20
|
+
# along with the effort.
|
|
14
21
|
|
|
15
22
|
CCRECALL_PORT="${CCRECALL_PORT:-7749}"
|
|
16
23
|
CCRECALL_EXTRACT_LOG="${CCRECALL_EXTRACT_LOG:-$HOME/.ccrecall/extract.log.jsonl}"
|
|
@@ -178,6 +185,66 @@ fi
|
|
|
178
185
|
CCRECALL_SCRIPT_DIR="$(cd "$(dirname "$_ccrecall_src")" && pwd)"
|
|
179
186
|
unset _ccrecall_src
|
|
180
187
|
|
|
188
|
+
# The MCP configuration an extraction run starts with: ccRecall's own server
|
|
189
|
+
# and nothing else, which is what lets `--strict-mcp-config` drop every other
|
|
190
|
+
# server the user has registered — the claude.ai Gmail, Drive and Calendar
|
|
191
|
+
# connectors among them. backfill-extract.sh sources this file for it too.
|
|
192
|
+
#
|
|
193
|
+
# The server is this package's own build, found from where this file lives,
|
|
194
|
+
# not the `ccmem-mcp` bin. A checkout registered with `claude mcp add ... --
|
|
195
|
+
# <path>/dist/mcp/server.js` (the README's way) has no such bin on PATH, and
|
|
196
|
+
# naming it would leave that install's extraction without a tool to save with.
|
|
197
|
+
# The bin is the fallback only when no build sits next to this file. Either way
|
|
198
|
+
# the server inherits this shell's environment, CCRECALL_DB_PATH included —
|
|
199
|
+
# where the daemon installer and _ccrecall_count_extracted read it from too.
|
|
200
|
+
#
|
|
201
|
+
# jq builds the JSON so an install path holding a space or a quote stays one
|
|
202
|
+
# string.
|
|
203
|
+
_ccrecall_extract_mcp_config() {
|
|
204
|
+
local server="${CCRECALL_SCRIPT_DIR%/*}/dist/mcp/server.js"
|
|
205
|
+
if [ -f "$server" ]; then
|
|
206
|
+
jq -nc --arg s "$server" '{mcpServers:{ccrecall:{type:"stdio",command:"node",args:[$s]}}}'
|
|
207
|
+
else
|
|
208
|
+
jq -nc '{mcpServers:{ccrecall:{type:"stdio",command:"ccmem-mcp",args:[]}}}'
|
|
209
|
+
fi
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# The effort an extraction run starts at: the level CCRECALL_EXTRACT_EFFORT
|
|
213
|
+
# names, or high when it names none. backfill-extract.sh sources this file for
|
|
214
|
+
# it too, so a backfill starts at the level the run it stands in for would have.
|
|
215
|
+
#
|
|
216
|
+
# The extraction gets a variable of its own because CLAUDE_CODE_EFFORT_LEVEL
|
|
217
|
+
# cannot be the knob: that is the caller's interactive setting, the very value
|
|
218
|
+
# this run has to override (see the effort comment in ccrecall-extract).
|
|
219
|
+
#
|
|
220
|
+
# Checked against the levels rather than passed on as given. Claude Code takes
|
|
221
|
+
# any value without complaint: on 2.1.300 `bogus` exited 0 with nothing on
|
|
222
|
+
# stderr and ran at whatever the settings or the model default said (medium,
|
|
223
|
+
# for Sonnet 5.5 with no settings; read off the Stop hook's effort.level). What
|
|
224
|
+
# this prints is also what the start message names, so an unchecked typo would
|
|
225
|
+
# be reported as the run's level while the run got another. Anything else
|
|
226
|
+
# becomes high, with a warning that leaves the value out: it goes to a
|
|
227
|
+
# terminal, and nothing has vetted what the environment holds.
|
|
228
|
+
#
|
|
229
|
+
# `auto` is left out although Claude Code documents it for this variable as
|
|
230
|
+
# "use the model default". On 2.1.300 it ran Sonnet 5.5 at high every time,
|
|
231
|
+
# with and without settings, where the model default is medium, so a start
|
|
232
|
+
# message saying "auto" would hide the level the run actually got.
|
|
233
|
+
#
|
|
234
|
+
# Always returns 0. The callers assign its output, and this file is sourced
|
|
235
|
+
# into shells that set errexit, where a non-zero status here would abort the
|
|
236
|
+
# caller's shell at that assignment, before extraction ever ran.
|
|
237
|
+
_ccrecall_extract_effort() {
|
|
238
|
+
case "${CCRECALL_EXTRACT_EFFORT:-}" in
|
|
239
|
+
low|medium|high|xhigh|max) builtin printf '%s' "$CCRECALL_EXTRACT_EFFORT" ;;
|
|
240
|
+
'') builtin printf 'high' ;;
|
|
241
|
+
*)
|
|
242
|
+
builtin printf '\n⚠️ ccRecall: CCRECALL_EXTRACT_EFFORT must be one of low, medium, high, xhigh, max — extracting at high.\n' >&2
|
|
243
|
+
builtin printf 'high'
|
|
244
|
+
;;
|
|
245
|
+
esac
|
|
246
|
+
}
|
|
247
|
+
|
|
181
248
|
ccrecall-extract() {
|
|
182
249
|
# #89: this used to derive project_id here with `sed 's|/|-|g'`, which
|
|
183
250
|
# diverged from Claude Code's char-wise [^A-Za-z0-9] encoding for any cwd
|
|
@@ -260,7 +327,7 @@ ccrecall-extract() {
|
|
|
260
327
|
# below against token-budget.ts, in both copies. 🔴 Until 2026-09-08 this
|
|
261
328
|
# comment already said that while no test read this line's number at all:
|
|
262
329
|
# it could say 999 and stay green. Verified by mutation, both directions.
|
|
263
|
-
prompt="You are a memory extraction agent. Save 0-5 lasting insights
|
|
330
|
+
prompt="You are a memory extraction agent. Save 0-5 lasting insights by calling the mcp__ccrecall__recall_save tool, by that exact name. Each memory must be self-contained with a key slug for dedup. Only the first 149 characters survive injection into a future session, so lead with what a future reader should DO and put the evidence, file names and war story after it. Set projectId to \"${project_id}\" for project-specific knowledge; omit for cross-project knowledge. Always pass origin=\"agent-inferred\": you are reading a finished transcript with nobody watching, and that flag is what stops your save from overwriting a memory the user wrote by hand under the same key. Pass messageId: the uuid in the \"--- human [<uuid>] ---\" header of the one message a memory came from, copied verbatim — omit it when no single message is the source, and never guess a uuid. Turns headed \"--- side-agent note [<uuid>] ---\" or \"--- peer session [<uuid>] ---\" are a note the user forwarded or a message from another Claude session, not the user's own words: never save what they say as the user's decision."
|
|
264
331
|
fi
|
|
265
332
|
|
|
266
333
|
# Append runtime context (projectId) to the prompt
|
|
@@ -398,7 +465,11 @@ ${prompt}"
|
|
|
398
465
|
return $claude_exit
|
|
399
466
|
fi
|
|
400
467
|
|
|
401
|
-
|
|
468
|
+
# Named in the start message because the level is now a setting: the line
|
|
469
|
+
# that says extraction began is where a run at the wrong one shows.
|
|
470
|
+
local effort
|
|
471
|
+
effort=$(_ccrecall_extract_effort)
|
|
472
|
+
printf '\n🧠 ccRecall: extracting memories from session (effort %s)...\n' "$effort"
|
|
402
473
|
|
|
403
474
|
local extract_start
|
|
404
475
|
extract_start=$(date +%s)
|
|
@@ -555,6 +626,33 @@ ${prompt}"
|
|
|
555
626
|
# written. Prefix assignments, so they reach this command only — an export
|
|
556
627
|
# would persist in the shell this file is sourced into and switch prompt
|
|
557
628
|
# recall off for every later session.
|
|
629
|
+
#
|
|
630
|
+
# The effort is set the same way, and it has to be a prefix:
|
|
631
|
+
# CLAUDE_CODE_EFFORT_LEVEL outranks --effort, /effort and every effortLevel
|
|
632
|
+
# setting, so a user who exports `max` for their interactive sessions ran
|
|
633
|
+
# every extraction at max, and an --effort flag here would change nothing.
|
|
634
|
+
# Measured 2026-10-15 on two real transcripts: at max, 90-95% of a run was
|
|
635
|
+
# thinking — 32-46K tokens before the first save, 261-355 s end to end. At
|
|
636
|
+
# high the same two runs took 26-40 s and saved five memories each, where max
|
|
637
|
+
# saved four or five. medium and low were faster still, but saved one to
|
|
638
|
+
# three fewer per session. So $effort is high unless CCRECALL_EXTRACT_EFFORT
|
|
639
|
+
# names another level (see _ccrecall_extract_effort).
|
|
640
|
+
#
|
|
641
|
+
# And the run gets only what it uses. It reads a transcript this prompt calls
|
|
642
|
+
# untrusted, and used to do so under --dangerously-skip-permissions with every
|
|
643
|
+
# MCP server the user had registered: Bash and, on the machine this was
|
|
644
|
+
# measured on, Gmail, Google Drive and Calendar connectors, with nothing
|
|
645
|
+
# asking first. Now it gets ccRecall's own server alone (--strict-mcp-config,
|
|
646
|
+
# see _ccrecall_extract_mcp_config), no built-in tools (--tools ""), the two
|
|
647
|
+
# tools extraction calls on an allowlist, and dontAsk refusing anything else
|
|
648
|
+
# instead of prompting for it. Skills are switched off as well — their
|
|
649
|
+
# listing is tens of kilobytes the model never needs. Verified across four
|
|
650
|
+
# real extractions the same day: no permission denials, ~26K fewer input
|
|
651
|
+
# tokens per run, and the MCP tools load directly, so the ToolSearch round
|
|
652
|
+
# before the first save is gone. Without ToolSearch the model takes tool
|
|
653
|
+
# names from the prompt, which is why both copies of it spell the full name.
|
|
654
|
+
local mcp_config
|
|
655
|
+
mcp_config=$(_ccrecall_extract_mcp_config)
|
|
558
656
|
local attempt=1 extract_retried=0 extracted_count=""
|
|
559
657
|
local stdout_marker recall_save_text_count
|
|
560
658
|
while : ; do
|
|
@@ -562,12 +660,17 @@ ${prompt}"
|
|
|
562
660
|
trap 'command rm -f -- "$stdout_tmp" 2>/dev/null; exit 130' INT
|
|
563
661
|
trap 'command rm -f -- "$stdout_tmp" 2>/dev/null; exit 143' TERM
|
|
564
662
|
builtin printf '%s' "$full_prompt" |
|
|
565
|
-
CCRECALL_SESSION_START_STRATEGY=off CCRECALL_PROMPT_RECALL=off
|
|
663
|
+
CCRECALL_SESSION_START_STRATEGY=off CCRECALL_PROMPT_RECALL=off CLAUDE_CODE_EFFORT_LEVEL="$effort" \
|
|
664
|
+
command claude -p \
|
|
566
665
|
--no-session-persistence \
|
|
567
666
|
--model sonnet \
|
|
568
667
|
${budget_args[@]+"${budget_args[@]}"} \
|
|
569
668
|
--max-turns 5 \
|
|
570
|
-
--
|
|
669
|
+
--strict-mcp-config --mcp-config "$mcp_config" \
|
|
670
|
+
--tools "" \
|
|
671
|
+
--allowedTools "mcp__ccrecall__recall_save,mcp__ccrecall__recall_query" \
|
|
672
|
+
--permission-mode dontAsk \
|
|
673
|
+
--disable-slash-commands 2>&1 1>|"$stdout_tmp"
|
|
571
674
|
); then
|
|
572
675
|
extract_exit=0
|
|
573
676
|
else
|
|
@@ -599,9 +702,14 @@ ${prompt}"
|
|
|
599
702
|
# called). A false positive costs one row to check; a false negative restores
|
|
600
703
|
# the status quo of total invisibility.
|
|
601
704
|
#
|
|
705
|
+
# The `mcp__ccrecall__` prefix is optional because the prompt now teaches the
|
|
706
|
+
# full tool name, and a model that prints its calls prints the name it was
|
|
707
|
+
# taught. Matching only the bare name would have gone blind to #75 the day
|
|
708
|
+
# the prompt changed.
|
|
709
|
+
#
|
|
602
710
|
# -c yields a bare count and never the matched line, so no session content can
|
|
603
711
|
# reach the telemetry log — the same constraint that governs the stdout capture.
|
|
604
|
-
recall_save_text_count=$(grep -cE '^[[:space:]]*recall_save[[:space:]]*\(' "$stdout_tmp" 2>/dev/null) || :
|
|
712
|
+
recall_save_text_count=$(grep -cE '^[[:space:]]*(mcp__ccrecall__)?recall_save[[:space:]]*\(' "$stdout_tmp" 2>/dev/null) || :
|
|
605
713
|
# grep exits 1 with empty output on no match (and $stdout_tmp is /dev/null when
|
|
606
714
|
# mktemp failed); --argjson would abort the whole telemetry write on a non-number.
|
|
607
715
|
[[ "$recall_save_text_count" =~ ^[0-9]+$ ]] || recall_save_text_count=0
|
|
@@ -623,12 +731,12 @@ ${prompt}"
|
|
|
623
731
|
done
|
|
624
732
|
command rm -f -- "$stdout_tmp" 2>/dev/null
|
|
625
733
|
# Scrub common credential formats before stderr reaches the telemetry log.
|
|
626
|
-
# claude echoes its own key (sk-ant-) in auth errors
|
|
627
|
-
#
|
|
628
|
-
#
|
|
629
|
-
# denylist of common prefixes, not exhaustive — but stdout, the
|
|
630
|
-
# stream where the model could echo transcript secrets, is already
|
|
631
|
-
# entirely, so this only hardens claude's own diagnostics.
|
|
734
|
+
# claude echoes its own key (sk-ant-) in auth errors. Only ccRecall's own MCP
|
|
735
|
+
# server is started for extraction now, so a third-party server can no longer
|
|
736
|
+
# print its token here, but claude's own diagnostics still can. This is a
|
|
737
|
+
# best-effort denylist of common prefixes, not exhaustive — but stdout, the
|
|
738
|
+
# higher-risk stream where the model could echo transcript secrets, is already
|
|
739
|
+
# discarded entirely, so this only hardens claude's own diagnostics.
|
|
632
740
|
extract_stderr=$(printf '%s' "$extract_stderr" | sed -E 's/(sk-ant-|sk-proj-|ghp_|gho_|ghu_|ghs_|github_pat_|AKIA)[A-Za-z0-9_-]*/[REDACTED]/g' | head -c 2000)
|
|
633
741
|
|
|
634
742
|
local extract_end
|