@fonlan/dsh-gitmemo 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -35
- package/README.zh.md +41 -19
- package/lib/client/form-specs.d.ts +94 -0
- package/lib/client/form-specs.js +144 -0
- package/lib/client.js +190 -2
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +108 -0
- package/lib/index.js +69 -8
- package/lib/mem.d.ts +37 -1
- package/lib/mem.js +74 -10
- package/lib/systemone.d.ts +38 -1
- package/lib/systemone.js +85 -9
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ dependency, and no manual memory commands are ever needed.
|
|
|
16
16
|
- **Git-only** — no runtime dependency beyond the `git` CLI
|
|
17
17
|
- **Token-efficient** — reuses prior conclusions via `mem_search`; subagents carry neither the workflow rules nor the tool schemas
|
|
18
18
|
- **Immutable entries** — every write creates a new file; corrections use `mem_replace` (one commit deletes the old file and adds the new one), withdrawal uses `mem_delete`
|
|
19
|
-
- **Knowledge freshness** — `mem_replace` is not just for user corrections: work that overrules a stored conclusion actively replaces it; `mem_search`
|
|
19
|
+
- **Knowledge freshness** — `mem_replace` is not just for user corrections: work that overrules a stored conclusion actively replaces it; `mem_search` adds a mild recency bonus (≤ +0.5, linearly decaying to 0 over ~180 days) on top of the match score, so conflicting stale conclusions lose to their replacements
|
|
20
20
|
- **Topic pages (MOC)** — a `kind: "topic"` entry aggregates a topic's "current truth": the summary states the currently-valid conclusions and the page evolves via `mem_replace` (kind is inherited), not per-task rewrites
|
|
21
21
|
- **Wiki links between entries** — content references other entries as `[[<commit-hash>]]`; `mem_read` with `expand: true` resolves those links one hop and automatically follows the `GitMemo-Replaces` chain to a replaced target's active version (dangling links come back with an error)
|
|
22
22
|
- **Structured search** — commit messages carry `GitMemo-*` trailers (keywords, digest, search-text projections); search is `git log --grep --fixed-strings` over commit messages only — entry bodies are never scanned
|
|
@@ -28,7 +28,7 @@ dependency, and no manual memory commands are ever needed.
|
|
|
28
28
|
|
|
29
29
|
| Piece | Description |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| `mem_search` | Search memories: `keywords` (array of 1–
|
|
31
|
+
| `mem_search` | Search memories: `keywords` (array of 1–15, 中英文同义词), `skip` + `snapshot` (stable pagination). Returns up to 20 scored hits with `summary` / `keywords` / `kind` / `matched_keywords`. Score = the summed rarity of the matched keywords within the page (default) or the matched count (`searchScoring: count`), plus a recency bonus |
|
|
32
32
|
| `mem_read` | Read one memory entry by create/replace commit hash (full markdown; historical hashes stay readable); returns `kind`; optional `expand: true` resolves `[[hash]]` links one hop |
|
|
33
33
|
| `mem_write` | Store a task outcome: `title` + `summary` + `keywords` (2–12) + `content` (engine generates front matter), optional `kind` (`"task"` default / `"topic"` aggregated page), `related_branches` / `related_paths`. One ADD commit per immutable file |
|
|
34
34
|
| `mem_delete` | Withdraw an obsolete conclusion (requires `commit_hash` + `reason`) |
|
|
@@ -63,6 +63,7 @@ The bundle patch ships with sensible defaults; override them in the profile's
|
|
|
63
63
|
- id: dsh-gitmemo
|
|
64
64
|
config:
|
|
65
65
|
searchLimit: 20 # max hits per mem_search call (page size)
|
|
66
|
+
searchScoring: weighted # weighted (default) | count — see "Search scoring"
|
|
66
67
|
lockTimeoutMs: 30000 # cross-process lock wait timeout
|
|
67
68
|
projectRoot: null # optional explicit project root (defaults to the session cwd)
|
|
68
69
|
systemOne: # optional System-one recall gate, see below
|
|
@@ -70,15 +71,65 @@ The bundle patch ships with sensible defaults; override them in the profile's
|
|
|
70
71
|
endpoint: https://api.typesafe.ai/v1/systemone
|
|
71
72
|
model: jev-latest
|
|
72
73
|
mode: noul # noul | score
|
|
73
|
-
|
|
74
|
+
policy: rerank # rerank (default) | filter — see "System-one Recall Gate"
|
|
75
|
+
threshold: 0.5 # filter mode: drop a candidate when P(yes) < this
|
|
76
|
+
maxDropFraction: 0.25 # filter mode: never drop more than this share of a page
|
|
74
77
|
```
|
|
75
78
|
|
|
79
|
+
## Search Scoring
|
|
80
|
+
|
|
81
|
+
`mem_search` matches entries with a fixed-string OR grep over the commit-message
|
|
82
|
+
projections — the title, the summary, and the entry's keywords (bodies are never
|
|
83
|
+
scanned). Two modes decide the *order* of the matched entries; both match exactly
|
|
84
|
+
the same entries:
|
|
85
|
+
|
|
86
|
+
| `searchScoring` | Score | Trade-off |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `weighted` (default) | `Σ` rarity of each matched keyword within the recall page, plus the recency bonus | A distinctive match outranks several common ones. One match can beat two, which the `count` mode never allows. |
|
|
89
|
+
| `count` | number of matched keywords, plus the recency bonus | The previous default arithmetic, kept for exact backwards compatibility. |
|
|
90
|
+
|
|
91
|
+
Rarity is the BM25-style weight `log(1 + (n − df + 0.5) / (df + 0.5))` over the
|
|
92
|
+
candidate page (`n` candidates, `df` matching the keyword), so it costs no extra
|
|
93
|
+
git call. The recency bonus stays ≤ +0.5, so it only reorders near-equal scores.
|
|
94
|
+
|
|
95
|
+
**Why this is the default.** On LongMemEval `_S` (500 instances, session
|
|
96
|
+
granularity) the engine reached `recall_all@50 = 0.998` while `ndcg_any@5` was
|
|
97
|
+
only 0.686: evidence was essentially always reachable but ranked badly, because
|
|
98
|
+
a plain match count cannot distinguish a rare keyword from a ubiquitous one.
|
|
99
|
+
Weighting the same matches lifted `recall_all@5` 0.738 → 0.755 and `ndcg@5`
|
|
100
|
+
0.687 → 0.700 on identical ingested data, and `recall_all@50` was unchanged
|
|
101
|
+
(0.998) — matching is untouched by construction.
|
|
102
|
+
`evals/longmemeval/README.md` has the full run and the ablation across scoring,
|
|
103
|
+
granularity and ingestion policy.
|
|
104
|
+
|
|
76
105
|
## System-one Recall Gate (optional)
|
|
77
106
|
|
|
78
107
|
A `mem_search` page can be handed to a **System-one model** (TypeSafe Jev by
|
|
79
108
|
default) for one fast judgement, so memories that are **completely unrelated**
|
|
80
|
-
to the current task
|
|
81
|
-
|
|
109
|
+
to the current task do not get injected.
|
|
110
|
+
|
|
111
|
+
**How the judgement is used — `policy`:**
|
|
112
|
+
|
|
113
|
+
| `policy` | Effect | Context | Measured accuracy |
|
|
114
|
+
| --- | --- | --- | --- |
|
|
115
|
+
| `rerank` (default) | keeps every judged memory and orders the page by the judge's value | unchanged | **0.8020** |
|
|
116
|
+
| `filter` | drops memories below `threshold`, bounded by `maxDropFraction` | **−80 %** | **0.3800** |
|
|
117
|
+
|
|
118
|
+
Those two figures are LongMemEval `_S` LLM-as-Judge over all 500 questions, with
|
|
119
|
+
the same judge, the same ingested memories and the same per-candidate values —
|
|
120
|
+
only the use made of them differs. The reason is in the judge's own signal: its
|
|
121
|
+
AUC separating evidence from non-evidence is ~0.71–0.75, so **no threshold is
|
|
122
|
+
safe**, while the same value is a perfectly good ordering feature. A hard drop on
|
|
123
|
+
a 0.75-AUC signal took 343 gold memories out of the prompt across 254 of 500
|
|
124
|
+
instances. Full study: `evals/longmemeval/README.md`.
|
|
125
|
+
|
|
126
|
+
`filter` remains available because dropping memories is the point of a recall
|
|
127
|
+
gate when context budget is the binding constraint — and it is now **bounded**:
|
|
128
|
+
`maxDropFraction` (default 0.25) restores the best-scoring rejects whenever a
|
|
129
|
+
threshold would drop more than that share of a page. An offline sweep held
|
|
130
|
+
`recall_all@5` at 0.8875 with a 20 % cap against 0.6875 uncapped. A memory
|
|
131
|
+
dropped is gone; a memory kept only costs context, so the errors are not
|
|
132
|
+
symmetric and the bound is deliberately tight.
|
|
82
133
|
|
|
83
134
|
- **Inert by default.** The gate runs only when a usable credential is
|
|
84
135
|
configured (`apiKey`, the credential named by `apiKeyEnv`, or the environment
|
|
@@ -87,9 +138,15 @@ reusable entries survive.
|
|
|
87
138
|
- **Fails open.** A timeout, a non-2xx response, an unparseable body — any
|
|
88
139
|
failure keeps every candidate and reports why in `gated.degraded` +
|
|
89
140
|
`gated.reason`. **A broken endpoint can never hide a memory.**
|
|
90
|
-
- **Never empties a page.**
|
|
91
|
-
|
|
92
|
-
|
|
141
|
+
- **Never empties a page.** Under `filter`, at least `minKeep` (default 1)
|
|
142
|
+
best-ranked candidates are retained when the whole page is rejected, and
|
|
143
|
+
`maxDropFraction` (default 0.25) caps how much of a judged page may be removed
|
|
144
|
+
at all — so "the gate removed it" and "there was nothing there" stay
|
|
145
|
+
distinguishable, and a mis-calibrated threshold cannot gut a page. Under
|
|
146
|
+
`rerank` nothing is removed by construction.
|
|
147
|
+
- **Ordering is reported either way.** The decision carries `ordered` (candidate
|
|
148
|
+
hashes best-first by the judge's value) and each verdict's value, so a caller
|
|
149
|
+
can log or re-rank from the same judgement without a second call. The tool's `gated` line
|
|
93
150
|
says so out loud — such a page carries a `NOTE(no candidate scored above the
|
|
94
151
|
threshold…)` marker, so a retained floor is never mistaken for a candidate that
|
|
95
152
|
genuinely passed. `judged=` counts only the candidates actually sent to the
|
|
@@ -114,6 +171,8 @@ reusable entries survive.
|
|
|
114
171
|
| `mode` | `noul` | `noul` (yes/no probability) or `score` (graded levels) |
|
|
115
172
|
| `threshold` | `0.5` | `noul` mode: keep when P(yes) ≥ this |
|
|
116
173
|
| `scoreMin` | `2` | `score` mode cutoff. Note Jev's `score` is `Σ(level_index × probability)`, ranging 0…levels−1 (5 levels → 0…4), **not 0–1** |
|
|
174
|
+
| `policy` | `rerank` | `rerank` keeps every judged memory and orders the page by the judge's value; `filter` drops below `threshold` |
|
|
175
|
+
| `maxDropFraction` | `0.25` | `filter` only: never drop more than this share of the judged page, lowest-scoring first |
|
|
117
176
|
| `minKeep` | `1` | Candidates retained when the whole page is rejected |
|
|
118
177
|
| `maxCandidates` | `20` | Candidates judged per request; the remainder is **never judged and never dropped** |
|
|
119
178
|
| `maxTaskChars` | `2000` | Task-text truncation length |
|
|
@@ -125,31 +184,21 @@ settings plane to see the entry and apply edits live.
|
|
|
125
184
|
### Settings page
|
|
126
185
|
|
|
127
186
|
The plugin ships a settings card (**Settings → GitMemo**) where the gate can be
|
|
128
|
-
switched on and off
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
single question may use 32k. `maxCandidates` and `maxTaskChars` exist to hold
|
|
144
|
-
that line.
|
|
145
|
-
- **Latency on the critical path.** Recall runs before every repo task, and the
|
|
146
|
-
gate adds one network round trip (~0.4s observed), so `timeoutMs` defaults to
|
|
147
|
-
8s and every failure path fails open.
|
|
148
|
-
- **Data egress.** Memory summaries leave the machine. TypeSafe currently offers
|
|
149
|
-
**no** zero data retention; point `endpoint` at a self-hosted implementation
|
|
150
|
-
(e.g. MIT-licensed `jeff`) or a local endpoint to avoid egress entirely.
|
|
151
|
-
- **Billing.** Priced per input token ($0.042 per million input tokens, output
|
|
152
|
-
free) — not per call.
|
|
187
|
+
switched on and off, its `policy` chosen between **Re-rank** and **Filter**, and
|
|
188
|
+
`maxDropFraction`, the endpoint, the model and the API key filled in directly,
|
|
189
|
+
applying immediately with no restart. The API key is written through the
|
|
190
|
+
credentials domain — it is **never persisted as a plaintext config value** — and
|
|
191
|
+
the form is write-only. Every control stages a draft and a **Save** is what
|
|
192
|
+
writes it; turning the gate off leaves recall byte-for-byte as it was before the
|
|
193
|
+
gate existed.
|
|
194
|
+
|
|
195
|
+
Fields the section does not carry render the value `src/index.ts` declares rather
|
|
196
|
+
than a blank control — an absent `enabled` reads as on, an absent `policy` reads
|
|
197
|
+
as `rerank`, an absent `maxDropFraction` as `0.25` — because a blank control would
|
|
198
|
+
misreport the configuration actually in force. A draft the field does not accept
|
|
199
|
+
(for example a `maxDropFraction` outside 0–1) **blocks the save** instead of being
|
|
200
|
+
silently dropped, and the settings card's defaults are asserted against the host
|
|
201
|
+
schema by `test/form-specs.test.mjs` so the two cannot drift apart.
|
|
153
202
|
|
|
154
203
|
## Memory Location & Format
|
|
155
204
|
|
|
@@ -182,7 +231,7 @@ git -C .mem show <commit-hash>
|
|
|
182
231
|
|
|
183
232
|
## Agent Workflow (always-on rules, root agents)
|
|
184
233
|
|
|
185
|
-
1. **Before work — search.** For repo-related tasks, extract 1–
|
|
234
|
+
1. **Before work — search.** For repo-related tasks, extract 1–15 中英文关键词 → `mem_search`.
|
|
186
235
|
Pure chat and general Q&A need no search.
|
|
187
236
|
2. **Preselect results.** Based on `title` / `summary` / `keywords` / `score` /
|
|
188
237
|
`matched_keywords`, `mem_read` at most 5 most relevant memories (`kind: "topic"` entries
|
|
@@ -215,6 +264,30 @@ npm run build # tsc → lib/
|
|
|
215
264
|
npm test # build + engine/plugin/migration unit tests (node:test)
|
|
216
265
|
```
|
|
217
266
|
|
|
267
|
+
## LongMemEval Evaluation
|
|
268
|
+
|
|
269
|
+
The engine is benchmarked against [LongMemEval](https://github.com/xiaowu0162/LongMemEval) two
|
|
270
|
+
ways, both runnable from this checkout. See [`evals/longmemeval/README.md`](evals/longmemeval/README.md)
|
|
271
|
+
for the retrieval metrics and [`evals/omnimemeval/README.md`](evals/omnimemeval/README.md) for the
|
|
272
|
+
end-to-end QA pipeline.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# official retrieval metrics (recall/NDCG, no LLM needed)
|
|
276
|
+
npm run eval:lme:oracle # wiring check
|
|
277
|
+
npm run eval:lme:s # all 500 instances of the _S split
|
|
278
|
+
|
|
279
|
+
# end-to-end QA accuracy through OmniMemEval's 6-step pipeline
|
|
280
|
+
npm run eval:omni:setup # clone + adapter + venv + env file
|
|
281
|
+
cd .omnimemeval && ./scripts/run_lme_eval.sh --lib gitmemo \
|
|
282
|
+
--env ~/.dsh-gitmemo-lme/.env.gitmemo --streaming 1
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Headline result on `_S` (500/500 instances, session granularity, zero failures): `recall_all@5`
|
|
286
|
+
**0.736** against **0.870** for a plain BM25 over full session text, but `recall_all@50` **0.998** —
|
|
287
|
+
the loss is ranking precision, not coverage. Holding everything else fixed and letting an LLM
|
|
288
|
+
choose what to write raises `recall_all@5` to **0.842**, which is the point of the benchmark:
|
|
289
|
+
for a keyword-grep memory, what you commit to memory dominates the retrieval algorithm.
|
|
290
|
+
|
|
218
291
|
## Layout
|
|
219
292
|
|
|
220
293
|
```
|
|
@@ -227,7 +300,10 @@ dsh-gitmemo/
|
|
|
227
300
|
│ ├── migrate.ts # legacy migration (dry-run/apply, backup refs, CAS swap)
|
|
228
301
|
│ └── cli.ts # `dsh-gitmemo migrate` CLI entry
|
|
229
302
|
├── lib/ # built output (committed; used by file:/git installs)
|
|
230
|
-
|
|
303
|
+
├── test/mem.test.mjs # engine + plugin + migration unit tests
|
|
304
|
+
└── evals/
|
|
305
|
+
├── longmemeval/ # retrieval harness: official recall/NDCG, no LLM required
|
|
306
|
+
└── omnimemeval/ # gitmemo as an OmniMemEval memory backend (QA accuracy)
|
|
231
307
|
```
|
|
232
308
|
|
|
233
309
|
## License
|
package/README.zh.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- **仅依赖 Git** —— 除 `git` CLI 外无任何运行时依赖
|
|
16
16
|
- **省 token** —— 通过 `mem_search` 复用既有结论;子代理既不携带工作流规则也不携带工具 schema
|
|
17
17
|
- **条目不可变** —— 每次写入都创建新文件;更正用 `mem_replace`(一个提交同时删除旧文件、新增新文件),作废用 `mem_delete`
|
|
18
|
-
- **知识保鲜** —— `mem_replace` 不只用于用户更正:本次工作覆盖旧结论时同样主动替换;`mem_search`
|
|
18
|
+
- **知识保鲜** —— `mem_replace` 不只用于用户更正:本次工作覆盖旧结论时同样主动替换;`mem_search` 在匹配分之上叠加温和的 recency 加成(≤ +0.5,180 天线性衰减到 0),新旧结论冲突时自动偏向新条目
|
|
19
19
|
- **主题聚合页(MOC)** —— `kind: "topic"` 条目聚合一个主题的「当前真相」:summary 概括当下有效结论,靠 `mem_replace` 演进(kind 自动继承),不按任务重复新增
|
|
20
20
|
- **条目间 Wiki 链接** —— content 里用 `[[<commit-hash>]]` 引用其他条目;`mem_read` 传 `expand: true` 一跳展开链接目标,并自动跟随 `GitMemo-Replaces` 链到被替换条目的最新版本(悬空链接带 error 返回)
|
|
21
21
|
- **结构化搜索** —— commit message 携带 `GitMemo-*` trailers(keywords、digest、search-text 投影);搜索只对 commit message 执行 `git log --grep --fixed-strings`,绝不扫描条目正文
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
| 内容 | 说明 |
|
|
29
29
|
| --- | --- |
|
|
30
|
-
| `mem_search` | 搜索记忆:`keywords`(1–
|
|
30
|
+
| `mem_search` | 搜索记忆:`keywords`(1–15 个关键词数组,建议中英文同义词)、`skip` + `snapshot`(稳定分页)。每次最多返回 20 条带 `summary` / `keywords` / `kind` / `matched_keywords` 的评分结果。score = 本页内命中关键词的稀有度之和(默认)或命中条数(`searchScoring: count`),再加 recency 加成 |
|
|
31
31
|
| `mem_read` | 按创建/替换提交哈希读取一条记忆的完整 markdown(历史哈希仍可读);返回 `kind`;可选 `expand: true` 一跳展开条目内的 `[[hash]]` 链接 |
|
|
32
32
|
| `mem_write` | 存储任务结论:`title` + `summary` + `keywords`(2–12)+ `content`(front matter 由引擎生成),可选 `kind`(`"task"` 缺省 / `"topic"` 聚合页)、`related_branches` / `related_paths`。每个不可变文件对应一个 ADD 提交 |
|
|
33
33
|
| `mem_delete` | 作废无替代结论(需要 `commit_hash` + `reason`) |
|
|
@@ -61,6 +61,7 @@ bundle patch 自带合理默认值,可在 profile 的 `cordis.patch.yml` 中
|
|
|
61
61
|
- id: dsh-gitmemo
|
|
62
62
|
config:
|
|
63
63
|
searchLimit: 20 # 每次 mem_search 返回的最大条数(每页大小)
|
|
64
|
+
searchScoring: weighted # weighted(默认)| count —— 见「检索评分」
|
|
64
65
|
lockTimeoutMs: 30000 # 跨进程锁等待超时
|
|
65
66
|
projectRoot: null # 可选:显式项目根目录(默认取会话工作目录)
|
|
66
67
|
systemOne: # 可选:System-one 召回门控,见下节
|
|
@@ -68,11 +69,37 @@ bundle patch 自带合理默认值,可在 profile 的 `cordis.patch.yml` 中
|
|
|
68
69
|
endpoint: https://api.typesafe.ai/v1/systemone
|
|
69
70
|
model: jev-latest
|
|
70
71
|
mode: noul # noul | score
|
|
71
|
-
|
|
72
|
+
policy: rerank # rerank(默认)| filter —— 见「System-one 召回门控」
|
|
73
|
+
threshold: 0.5 # filter 模式:概率 < 该值则丢弃
|
|
74
|
+
maxDropFraction: 0.25 # filter 模式:单页最多丢弃此比例
|
|
72
75
|
```
|
|
73
76
|
|
|
77
|
+
## 检索评分
|
|
78
|
+
|
|
79
|
+
`mem_search` 用固定字符串 OR grep 在提交消息投影上匹配——即标题、摘要与条目的关键词(正文从不扫描)。两种模式决定**顺序**,匹配到的条目集合完全相同:
|
|
80
|
+
|
|
81
|
+
| `searchScoring` | 分值 | 取舍 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `weighted`(默认) | 本页内各命中关键词稀有度之和,再加 recency 加成 | 有区分度的命中能压过若干常见命中;一次命中可以胜过两次,这是 `count` 模式不允许的 |
|
|
84
|
+
| `count` | 命中关键词条数,再加 recency 加成 | 此前的默认算法,为完全向后兼容保留 |
|
|
85
|
+
|
|
86
|
+
稀有度用的是候选页上的 BM25 式权重 `log(1 + (n − df + 0.5) / (df + 0.5))`(`n` 为候选数,`df` 为命中该关键词的候选数),因此不增加额外 git 调用。recency 加成仍 ≤ +0.5,只用于重排近乎同分的条目。
|
|
87
|
+
|
|
88
|
+
**为什么把它设为默认。** 在 LongMemEval `_S`(500 个实例、会话粒度)上,引擎的 `recall_all@50` 达到 0.998,而 `ndcg_any@5` 只有 0.686:证据基本都能召回,但排不上来——因为纯命中计数分不清罕见词与随处可见的词。在完全相同的入库数据上只改加权,`recall_all@5` 从 0.738 升到 0.755、`ndcg@5` 从 0.687 升到 0.700,而 `recall_all@50` 仍是 0.998(匹配逻辑在构造上未改动)。完整实验与「打分 / 粒度 / 写入策略」三组消融见 `evals/longmemeval/README.md`。
|
|
89
|
+
|
|
74
90
|
## System-one 召回门控(可选)
|
|
75
91
|
|
|
92
|
+
**判定结果怎么用 —— `policy`:**
|
|
93
|
+
|
|
94
|
+
| `policy` | 作用 | 上下文 | 实测准确率 |
|
|
95
|
+
| --- | --- | --- | --- |
|
|
96
|
+
| `rerank`(默认) | 保留全部被判定的记忆,只按判定分值重排本页 | 不变 | **0.8020** |
|
|
97
|
+
| `filter` | 丢弃低于 `threshold` 的记忆,并由 `maxDropFraction` 兜住上限 | **−80 %** | **0.3800** |
|
|
98
|
+
|
|
99
|
+
以上是 LongMemEval `_S` 全部 500 题、同一个 judge、同一批已入库记忆、同一组候选分值下的 LLM-as-Judge 结果——**唯一的差别就是怎么用这些分值**。原因在信号本身:judge 区分证据与非证据的 AUC 只有约 0.71–0.75,因此**没有任何阈值是安全的**,但同一个分值作为排序特征是可靠的。在 0.75 AUC 的信号上做硬删,把 343 条 gold 记忆挡在了提示之外,涉及 500 个实例中的 254 个。
|
|
100
|
+
|
|
101
|
+
`filter` 仍然保留:当上下文预算才是硬约束时,「丢弃」正是召回门控的意义所在。而它现在**有上限**了:`maxDropFraction`(默认 0.25)会在阈值想丢掉超过该比例时,把得分最高的被拒候选恢复回来。离线扫描显示 20% 上限能把 `recall_all@5` 稳在 0.8875,而不设上限只有 0.6875。丢掉的记忆就没了,留下的只是多花点上下文——两类错误并不对称,所以上限刻意收得紧。
|
|
102
|
+
|
|
76
103
|
`mem_search` 的候选页可以交给一个 **System-one 模型**(默认 [TypeSafe Jev](https://docs.typesafe.ai))
|
|
77
104
|
做一次快速判断,把与当前任务**完全无关**的记忆从注入内容里剔除,只留下真正可复用的条目。
|
|
78
105
|
|
|
@@ -103,6 +130,8 @@ bundle patch 自带合理默认值,可在 profile 的 `cordis.patch.yml` 中
|
|
|
103
130
|
| `threshold` | `0.5` | `noul` 模式:概率 ≥ 该值保留 |
|
|
104
131
|
| `scoreMin` | `2` | `score` 模式阈值。注意 Jev 的 `score` 是 `Σ(层级序号 × 概率)`,范围是 0…层级数−1(默认 5 层 → 0…4),**不是 0–1** |
|
|
105
132
|
| `minKeep` | `1` | 整页被拒时至少保留几条 |
|
|
133
|
+
| `policy` | `rerank` | `rerank` 保留全部判定过的记忆并按分值重排;`filter` 丢弃低于 `threshold` 的候选 |
|
|
134
|
+
| `maxDropFraction` | `0.25` | 仅 `filter`:单页最多丢弃此比例,从最低分开始丢 |
|
|
106
135
|
| `maxCandidates` | `20` | 单次请求最多判定多少条;超出部分**不判定、不剔除** |
|
|
107
136
|
| `maxTaskChars` | `2000` | 任务文本截断长度 |
|
|
108
137
|
| `timeoutMs` | `8000` | 请求超时 |
|
|
@@ -111,22 +140,15 @@ bundle patch 自带合理默认值,可在 profile 的 `cordis.patch.yml` 中
|
|
|
111
140
|
|
|
112
141
|
### 设置页
|
|
113
142
|
|
|
114
|
-
插件自带一个设置卡片(**设置 → GitMemo
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
### 已知取舍
|
|
143
|
+
插件自带一个设置卡片(**设置 → GitMemo**),可直接开关召回闸门、在**重排**与**过滤**之间选择
|
|
144
|
+
`policy`,并填写 `maxDropFraction`、端点、模型与 API key,保存后立即生效、无需重启。API key 通过凭据
|
|
145
|
+
域写入——**绝不**以明文配置值落盘——且表单为只写。所有控件都只是暂存草稿,点**保存**才真正写入;
|
|
146
|
+
关闭闸门后召回行为与门控存在前逐字节一致。
|
|
120
147
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
- **延迟在关键路径上。** 召回发生在每次仓库任务之前,门控会给 `mem_search` 增加一次网络往返
|
|
126
|
-
(实测约 0.4s 量级),因此 `timeoutMs` 默认 8s 且失败即放行。
|
|
127
|
-
- **数据外发。** 记忆摘要会离开本机。TypeSafe 目前**不提供** zero data retention;若要避免外发,
|
|
128
|
-
可把 `endpoint` 指向自建实现(如 MIT 许可的 `jeff`)或本地端点。
|
|
129
|
-
- **计费口径。** 按输入 token 计费($0.042 / 百万输入 token,输出免费),不是按次计费。
|
|
148
|
+
节里没有的字段会渲染成 `src/index.ts` 声明的默认值而不是空白控件——`enabled` 缺省即开启、`policy` 缺省
|
|
149
|
+
即 `rerank`、`maxDropFraction` 缺省即 `0.25`——因为空白控件会误报当前实际生效的配置。字段不接受的值
|
|
150
|
+
(例如超出 0–1 的 `maxDropFraction`)会**阻止保存**而不是被静默丢弃;卡片的默认值由
|
|
151
|
+
`test/form-specs.test.mjs` 对着宿主 schema 断言,两者不会漂移。
|
|
130
152
|
|
|
131
153
|
## 记忆存放位置与格式
|
|
132
154
|
|
|
@@ -156,7 +178,7 @@ git -C .mem show <commit-hash>
|
|
|
156
178
|
|
|
157
179
|
## Agent 工作流(常驻规则,仅根 Agent)
|
|
158
180
|
|
|
159
|
-
1. **开工前 —— 搜索。** 仓库相关任务开始前提取 1–
|
|
181
|
+
1. **开工前 —— 搜索。** 仓库相关任务开始前提取 1–15 个中英文关键词 → `mem_search`。纯闲聊和通用问答无需搜索。
|
|
160
182
|
2. **结果预筛。** 根据 `title` / `summary` / `keywords` / `score` / `matched_keywords` 最多
|
|
161
183
|
`mem_read` 5 条最相关记忆(`kind: "topic"` 的条目是该主题的聚合入口,优先读);用 `skip` + 返回的
|
|
162
184
|
`snapshot` 翻页。
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The staged settings form's pure field vocabulary: how one section field
|
|
3
|
+
* converts between its stored value and the draft text the card renders.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately a plain TypeScript module with no React, DOM or browser import:
|
|
6
|
+
* the card needs it and so do the unit tests, which cannot import the browser
|
|
7
|
+
* bundle. The host tsconfig compiles plain `.ts` files under `src`, so this is
|
|
8
|
+
* both inlined into the client bundle and emitted for the tests to import.
|
|
9
|
+
*/
|
|
10
|
+
import type { SettingsFieldWrite } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
11
|
+
/** The editable projection of this plugin's settings namespace. */
|
|
12
|
+
export interface GitMemoSettings {
|
|
13
|
+
/** The System-one recall gate, as `src/index.ts` declares it. */
|
|
14
|
+
systemOne?: {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
apiKeyEnv?: string;
|
|
20
|
+
mode?: 'noul' | 'score';
|
|
21
|
+
/** How the judge's values are used: order the page, or drop from it. */
|
|
22
|
+
policy?: 'rerank' | 'filter';
|
|
23
|
+
/** `filter` only: the share of a judged page that may be dropped. */
|
|
24
|
+
maxDropFraction?: number;
|
|
25
|
+
threshold?: number;
|
|
26
|
+
scoreMin?: number;
|
|
27
|
+
minKeep?: number;
|
|
28
|
+
maxCandidates?: number;
|
|
29
|
+
maxTaskChars?: number;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** How one section field converts between its stored value and its draft text. */
|
|
34
|
+
export interface PathFieldSpec {
|
|
35
|
+
/** Key addressing this control inside the card's form. */
|
|
36
|
+
field: string;
|
|
37
|
+
/** Path inside the namespace section this control edits. */
|
|
38
|
+
path: readonly string[];
|
|
39
|
+
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
40
|
+
format: (value: unknown) => string;
|
|
41
|
+
/** The write this draft text stages, or undefined when the text is not a value this field accepts. */
|
|
42
|
+
parse: (text: string) => SettingsFieldWrite | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A free-text field. An empty draft clears the field, so emptying the control
|
|
46
|
+
* and saving is the same gesture as resetting it.
|
|
47
|
+
* @param field - key addressing this control inside the card's form.
|
|
48
|
+
* @param path - path of the field inside the namespace section.
|
|
49
|
+
* @returns the field's conversion spec.
|
|
50
|
+
*/
|
|
51
|
+
export declare function pathTextField(field: string, path: readonly string[]): PathFieldSpec;
|
|
52
|
+
/**
|
|
53
|
+
* A whole-number field. An empty draft clears the field; any other draft that
|
|
54
|
+
* is not a finite number blocks the save.
|
|
55
|
+
* @param field - key addressing this control inside the card's form.
|
|
56
|
+
* @param path - path of the field inside the namespace section.
|
|
57
|
+
* @returns the field's conversion spec.
|
|
58
|
+
*/
|
|
59
|
+
export declare function pathNumberField(field: string, path: readonly string[]): PathFieldSpec;
|
|
60
|
+
/**
|
|
61
|
+
* A boolean field, staged as the `'true'`/`'false'` draft the card's switch
|
|
62
|
+
* renders. An empty draft clears the field (re-inheriting the declared
|
|
63
|
+
* default); any other draft that is not one of those two words blocks the save.
|
|
64
|
+
* @param field - key addressing this control inside the card's form.
|
|
65
|
+
* @param path - path of the field inside the namespace section.
|
|
66
|
+
* @returns the field's conversion spec.
|
|
67
|
+
*/
|
|
68
|
+
export declare function pathBooleanField(field: string, path: readonly string[]): PathFieldSpec;
|
|
69
|
+
/**
|
|
70
|
+
* A field restricted to a closed set of strings, rendered with the declared
|
|
71
|
+
* default when the section carries none. An empty draft clears the field; any
|
|
72
|
+
* other draft outside the set blocks the save rather than being dropped, so a
|
|
73
|
+
* typo can never silently reconfigure the gate.
|
|
74
|
+
* @param field - key addressing this control inside the card's form.
|
|
75
|
+
* @param path - path of the field inside the namespace section.
|
|
76
|
+
* @param allowed - the values the field accepts, in contract order.
|
|
77
|
+
* @param declaredDefault - the value `src/index.ts` declares, shown while absent.
|
|
78
|
+
* @returns the field's conversion spec.
|
|
79
|
+
*/
|
|
80
|
+
export declare function pathChoiceField(field: string, path: readonly string[], allowed: readonly string[], declaredDefault: string): PathFieldSpec;
|
|
81
|
+
/**
|
|
82
|
+
* A numeric field bounded to a range, rendered with the declared default when
|
|
83
|
+
* the section carries none. The bound is enforced here, not by the control, so
|
|
84
|
+
* an out-of-range draft blocks the save instead of reaching the plane.
|
|
85
|
+
* @param field - key addressing this control inside the card's form.
|
|
86
|
+
* @param path - path of the field inside the namespace section.
|
|
87
|
+
* @param bounds - inclusive range, and the declared default shown while absent.
|
|
88
|
+
* @returns the field's conversion spec.
|
|
89
|
+
*/
|
|
90
|
+
export declare function pathBoundedNumberField(field: string, path: readonly string[], bounds: {
|
|
91
|
+
min: number;
|
|
92
|
+
max: number;
|
|
93
|
+
declaredDefault: string;
|
|
94
|
+
}): PathFieldSpec;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The staged settings form's pure field vocabulary: how one section field
|
|
3
|
+
* converts between its stored value and the draft text the card renders.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately a plain TypeScript module with no React, DOM or browser import:
|
|
6
|
+
* the card needs it and so do the unit tests, which cannot import the browser
|
|
7
|
+
* bundle. The host tsconfig compiles plain `.ts` files under `src`, so this is
|
|
8
|
+
* both inlined into the client bundle and emitted for the tests to import.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A free-text field. An empty draft clears the field, so emptying the control
|
|
12
|
+
* and saving is the same gesture as resetting it.
|
|
13
|
+
* @param field - key addressing this control inside the card's form.
|
|
14
|
+
* @param path - path of the field inside the namespace section.
|
|
15
|
+
* @returns the field's conversion spec.
|
|
16
|
+
*/
|
|
17
|
+
export function pathTextField(field, path) {
|
|
18
|
+
return {
|
|
19
|
+
field,
|
|
20
|
+
path,
|
|
21
|
+
format: (value) => (typeof value === 'string' ? value : ''),
|
|
22
|
+
parse: (text) => {
|
|
23
|
+
const trimmed = text.trim();
|
|
24
|
+
return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed };
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A whole-number field. An empty draft clears the field; any other draft that
|
|
30
|
+
* is not a finite number blocks the save.
|
|
31
|
+
* @param field - key addressing this control inside the card's form.
|
|
32
|
+
* @param path - path of the field inside the namespace section.
|
|
33
|
+
* @returns the field's conversion spec.
|
|
34
|
+
*/
|
|
35
|
+
export function pathNumberField(field, path) {
|
|
36
|
+
return {
|
|
37
|
+
field,
|
|
38
|
+
path,
|
|
39
|
+
format: (value) => (typeof value === 'number' ? String(value) : ''),
|
|
40
|
+
parse: (text) => {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
if (trimmed === '')
|
|
43
|
+
return { kind: 'clear' };
|
|
44
|
+
const parsed = Number(trimmed);
|
|
45
|
+
return Number.isFinite(parsed) ? { kind: 'set', value: parsed } : undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Draft text a boolean field renders while the section carries no value.
|
|
51
|
+
*
|
|
52
|
+
* A missing boolean is not "unknown": `src/index.ts` declares
|
|
53
|
+
* `systemOne.enabled` with `default(true)` and reads it back through
|
|
54
|
+
* `liveValue(section.enabled, true)`, so an absent value means ENABLED. Rendering
|
|
55
|
+
* an absent boolean as the empty string would show a disabled switch for a gate
|
|
56
|
+
* that is actually running, so the draft is seeded with the declared default.
|
|
57
|
+
*/
|
|
58
|
+
const ABSENT_BOOLEAN_TEXT = 'true';
|
|
59
|
+
/**
|
|
60
|
+
* A boolean field, staged as the `'true'`/`'false'` draft the card's switch
|
|
61
|
+
* renders. An empty draft clears the field (re-inheriting the declared
|
|
62
|
+
* default); any other draft that is not one of those two words blocks the save.
|
|
63
|
+
* @param field - key addressing this control inside the card's form.
|
|
64
|
+
* @param path - path of the field inside the namespace section.
|
|
65
|
+
* @returns the field's conversion spec.
|
|
66
|
+
*/
|
|
67
|
+
export function pathBooleanField(field, path) {
|
|
68
|
+
return {
|
|
69
|
+
field,
|
|
70
|
+
path,
|
|
71
|
+
format: (value) => (typeof value === 'boolean' ? String(value) : ABSENT_BOOLEAN_TEXT),
|
|
72
|
+
parse: (text) => {
|
|
73
|
+
const trimmed = text.trim();
|
|
74
|
+
if (trimmed === '')
|
|
75
|
+
return { kind: 'clear' };
|
|
76
|
+
if (trimmed === 'true')
|
|
77
|
+
return { kind: 'set', value: true };
|
|
78
|
+
if (trimmed === 'false')
|
|
79
|
+
return { kind: 'set', value: false };
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Draft text for a field the section may not carry, given the default
|
|
86
|
+
* `src/index.ts` declares for it. A missing value is not "unknown": the schema
|
|
87
|
+
* declares one, so showing an empty control would misreport the running
|
|
88
|
+
* configuration — the same reasoning as {@link ABSENT_BOOLEAN_TEXT}.
|
|
89
|
+
*/
|
|
90
|
+
function declaredDefaultText(declaredDefault) {
|
|
91
|
+
return declaredDefault;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A field restricted to a closed set of strings, rendered with the declared
|
|
95
|
+
* default when the section carries none. An empty draft clears the field; any
|
|
96
|
+
* other draft outside the set blocks the save rather than being dropped, so a
|
|
97
|
+
* typo can never silently reconfigure the gate.
|
|
98
|
+
* @param field - key addressing this control inside the card's form.
|
|
99
|
+
* @param path - path of the field inside the namespace section.
|
|
100
|
+
* @param allowed - the values the field accepts, in contract order.
|
|
101
|
+
* @param declaredDefault - the value `src/index.ts` declares, shown while absent.
|
|
102
|
+
* @returns the field's conversion spec.
|
|
103
|
+
*/
|
|
104
|
+
export function pathChoiceField(field, path, allowed, declaredDefault) {
|
|
105
|
+
if (!allowed.includes(declaredDefault)) {
|
|
106
|
+
throw new Error(`dsh-gitmemo settings field ${field}: default ${declaredDefault} is not an accepted value`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
field,
|
|
110
|
+
path,
|
|
111
|
+
format: (value) => (typeof value === 'string' && allowed.includes(value) ? value : declaredDefaultText(declaredDefault)),
|
|
112
|
+
parse: (text) => {
|
|
113
|
+
const trimmed = text.trim();
|
|
114
|
+
if (trimmed === '')
|
|
115
|
+
return { kind: 'clear' };
|
|
116
|
+
return allowed.includes(trimmed) ? { kind: 'set', value: trimmed } : undefined;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A numeric field bounded to a range, rendered with the declared default when
|
|
122
|
+
* the section carries none. The bound is enforced here, not by the control, so
|
|
123
|
+
* an out-of-range draft blocks the save instead of reaching the plane.
|
|
124
|
+
* @param field - key addressing this control inside the card's form.
|
|
125
|
+
* @param path - path of the field inside the namespace section.
|
|
126
|
+
* @param bounds - inclusive range, and the declared default shown while absent.
|
|
127
|
+
* @returns the field's conversion spec.
|
|
128
|
+
*/
|
|
129
|
+
export function pathBoundedNumberField(field, path, bounds) {
|
|
130
|
+
return {
|
|
131
|
+
field,
|
|
132
|
+
path,
|
|
133
|
+
format: (value) => (typeof value === 'number' ? String(value) : declaredDefaultText(bounds.declaredDefault)),
|
|
134
|
+
parse: (text) => {
|
|
135
|
+
const trimmed = text.trim();
|
|
136
|
+
if (trimmed === '')
|
|
137
|
+
return { kind: 'clear' };
|
|
138
|
+
const parsed = Number(trimmed);
|
|
139
|
+
if (!Number.isFinite(parsed))
|
|
140
|
+
return undefined;
|
|
141
|
+
return parsed >= bounds.min && parsed <= bounds.max ? { kind: 'set', value: parsed } : undefined;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|