dsh-continual-evolve 0.10.4 → 0.10.5

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 CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-1075%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-1097%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-98%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-92%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-99%25-brightgreen)]()
@@ -34,7 +34,7 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
34
34
  2. **Capability-aware auxiliary calls** — the memory loop, review, planner, wrapup, and fate resolve exact provider/model metadata through [`src/llm-text.ts`](src/llm-text.ts), use the lowest advertised enabled reasoning effort (falling back to a closing effort only when no enabled level exists), and forward the host session id for provider routing; models without reasoning metadata use their provider default.
35
35
  3. **Guard** — code-enforced validation: edit schema, blast-radius/scope coherence, and the promotion policy (project-scoped markers, thin content, near-duplicate detection, credential screening keep the global store clean — secrets are rejected at every write sink, including mount materialization). Global creates that near-duplicate an existing entry are rejected at write time (≥0.8 similarity); moderate overlaps carry a `conflictHint` for later consolidation.
36
36
  4. **Approve** — global and project writes require explicit human approval; the dialog shows the bounded structured edit diff and conflict warnings, while malformed/lost responses remain retryable rather than counting as rejection.
37
- 5. **Apply & inject** — memory batches preflight every persistent approval, recheck abort before writes, and compensate earlier scope writes if a later batch fails; every successful scope still passes through snapshot + audit. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memories/skills appear as a relevance-ordered capped directory index (`- [memory:type:id] title` hooks, full text one `evolve_list` away).
37
+ 5. **Apply & inject** — memory batches preflight every persistent approval, recheck abort before writes, and compensate earlier scope writes if a later batch fails; every successful scope still passes through snapshot + audit. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memory CONTENT injects at session start under a hard character budget, frozen per session, with the when_to_save guide riding along; skills still appear as a relevance-ordered capped directory index.
38
38
  6. **Validate & roll back** — benchmarks score candidates against frozen cases; rejected candidates roll back deterministically and are captured as draft regression cases (`auto_regression` benchmark).
39
39
 
40
40
  ## Install
@@ -55,14 +55,11 @@ Commands (in-session):
55
55
 
56
56
  | Command | Effect |
57
57
  |---|---|
58
- | `/evolve` | help + current local store |
58
+ | `/evolve` | short help + current local store (`/evolve help all` lists every subcommand) |
59
59
  | `/evolve list · history · rollback <id>` | inspect and revert (add `project` for this project's store, `global` for the cross-project store) |
60
60
  | `/evolve plan [msg]` | run the LLM planner against the store |
61
61
  | `/evolve wrapup` | assess this session's local entries: promote / archive / keep |
62
- | `/evolve archive · unarchive · demote <id>` | hide from injection (data kept, restorable) — `demote` targets global noise |
63
- | `/evolve recall [scope] <query…>` | targeted memory recall: full content with version, source, and staleness |
64
- | `/evolve remember <type> [scope] <text…>` | immediately persist one typed memory (`user|feedback|project|reference`) |
65
- | `/evolve forget [scope] <query…>` | locate one memory and archive it (restorable); ambiguous queries only list |
62
+ | `/evolve archive · unarchive <id>` | hide from injection (data kept, restorable) |
66
63
  | `/evolve consolidate [apply] [merge]` | report (or apply) one batch archive of conflict-hinted + stale zero-use global entries; `merge` folds near-duplicate content into the survivors |
67
64
  | `/evolve failures` | aggregated failure classes (gate + benchmark) |
68
65
  | `/evolve log [tail N] [session <id>]` | plugin log |
@@ -73,13 +70,13 @@ Commands (in-session):
73
70
  | `/evolve pause · resume · status` | pause/resume the auto-review gate (manual tools keep working), gate state |
74
71
  | `/evolve usage` | per-entry injection counts + exact provider-reported tokens for direct memory/review/planner/wrapup/fate calls (benchmark host subagents excluded) |
75
72
 
76
- Model tools: `evolve_list / add / update / delete / rollback / recall` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval; `evolve_recall` filters by query, kinds, scopes, memory types, and limit, and returns full content with version, source, and staleness).
73
+ Model tools: `evolve_list / add / update / delete / rollback / recall` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval; `evolve_recall` filters by query, kinds, scopes, memory types, and limit, and returns full content with version, source, and staleness). Memory reads and writes live in the conversation — the model carries the injected memory index plus `evolve_recall`/`evolve_add` — so there are no `/evolve remember · forget · recall` counterparts: ask in chat.
77
74
 
78
75
  For third-party consumers: every applied evolution (gate or manual) appends a structured `evolve_complete` event to `reviews.jsonl` (`src/evolve-event.ts` defines the shape) alongside the human-readable audit records.
79
76
 
80
77
  `/evolve usage` also reads `evolve/token-usage.jsonl`: exact provider-reported input/cache/output/total tokens for the plugin's direct memory-agent, review, planner, manual-wrapup, and automatic-fate calls. The report covers a retained tail rather than lifetime usage, distinguishes missing provider samples, and explicitly excludes host benchmark subagents, their agent-loop calls, and per-entry injection attribution.
81
78
 
82
- Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories and skills appear as a relevance-ordered directory index (`[memory:type:id] title` hooks, capped at 15 lines with a fold counter) — full text via `evolve_recall` (targeted) or `evolve_list`. Every memory apply also refreshes a readable `MEMORY.md` index plus one fact file per entry in the store directory. Empty store = zero injected tokens.
79
+ Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories inject as CONTENT at session start: the `evolve:memory-index` section (order 400) carries the when_to_save guide plus every memory body the budget allows, ranked `project > feedback > user > reference`, with overflow degraded to `[memory:type:id] title` hooks and the remainder counted (hard cap `memoryIndex.maxChars`, default 6000 chars; `evolve_recall` reads anything not shown). The section is computed once per session and reused byte-for-byte, so the system prompt stays stable and prompt-cache reads keep hitting — a memory written mid-session appears in the next session. Skills still appear as a relevance-ordered directory index (capped at 15 lines with a fold counter); memories no longer do. Every memory apply also refreshes a readable `MEMORY.md` index plus one fact file per entry in the store directory. With `memoryIndex.guide` off and an empty store, injection is zero tokens.
83
80
 
84
81
  ## Configuration
85
82
 
@@ -100,7 +97,11 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
100
97
  | `promotionBlockPatterns` | POSIX paths, session ids, `~/.dsh` | content matching these is project-scoped and never promoted to global |
101
98
  | `promotionMinChars` | `100` | whole promotions below this length stay local |
102
99
  | `injectionDirectoryLines` | `15` | entry-directory lines per build before folding into a counter |
103
- | `sectionOrder` | `118` | system-prompt section order |
100
+ | `sectionOrder` | `118` | system-prompt section order for the guidance + entries sections |
101
+ | `memoryIndex.enabled` | `true` | register the `evolve:memory-index` content section |
102
+ | `memoryIndex.guide` | `true` | include the when_to_save guide (with the store empty and this off, the section renders nothing) |
103
+ | `memoryIndex.order` | `400` | section order (upstream named slots start at `PLAN_POLICY=500`; the plugin's other sections sit at 118/119) |
104
+ | `memoryIndex.maxChars` | `6000` | hard character budget for the injected memories block |
104
105
  | `skillsDir` | `<dshHome>/skills` | where skill entries materialize as SKILL.md bundles |
105
106
  | `rubricKey` | auto-generated key file | AES-256-GCM passphrase for benchmark rubrics (`DSH_EVOLVE_RUBRIC_KEY` overrides) |
106
107
  | `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | plugin-owned JSONL file log with rotation |
@@ -142,7 +143,7 @@ before aborting.
142
143
 
143
144
  ```bash
144
145
  pnpm install && pnpm build # deps + tsc -> lib/
145
- pnpm test # vitest (1075 tests)
146
+ pnpm test # vitest (1097 tests)
146
147
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
147
148
  pnpm coverage:gaps # locate uncovered lines per file (read-only)
148
149
  pnpm lint # oxlint src test
@@ -152,7 +153,7 @@ Project layout:
152
153
 
153
154
  ```
154
155
  ├── src/ # engine, tools, commands, memory agent, recall, projection, gate, fate, benchmark, injection + token usage…
155
- ├── test/ # vitest suites (58 files)
156
+ ├── test/ # vitest suites (59 files)
156
157
  ├── lib/ # build output (tsc)
157
158
  ├── docs/
158
159
  │ ├── design.md # full design doc (hardening matrix)
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-1075%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-1097%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-98%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-92%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-99%25-brightgreen)]()
@@ -34,7 +34,7 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
34
34
  2. **能力感知的辅助调用**——memory loop、review、planner、wrapup、fate 通过 [`src/llm-text.ts`](src/llm-text.ts) 解析精确 provider/model 能力并使用模型公布的最低开启 reasoning effort,同时转发 host session id 供 provider 路由;只有没有开启档时才回退关闭档,没有 reasoning 元数据时使用 provider 默认行为。
35
35
  3. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
36
36
  4. **审批**——全局与项目写入需明确人工批准;弹窗展示有界结构化编辑 diff 与冲突提示,弹窗丢失/响应畸形会重试,不会被误记为拒绝。
37
- 5. **应用与注入**——memory 批次先完成所有持久化审批,写前重查 abort;后续 scope 失败时补偿回滚先前写入,成功 scope 仍保留快照与审计。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子)。
37
+ 5. **应用与注入**——memory 批次先完成所有持久化审批,写前重查 abort;后续 scope 失败时补偿回滚先前写入,成功 scope 仍保留快照与审计。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);**memory 正文**在会话开场按硬预算注入(会话内冻结,when_to_save 指南随行);skill 仍以按相关性排序的目录索引出现。
38
38
  6. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚,并自动沉淀为 draft 回归用例(`auto_regression` 基准)。
39
39
 
40
40
  ## 安装
@@ -55,14 +55,11 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
55
55
 
56
56
  | 命令 | 效果 |
57
57
  |---|---|
58
- | `/evolve` | 帮助 + 当前 local store |
58
+ | `/evolve` | 简短帮助 + 当前 local store(`/evolve help all` 列全部子命令) |
59
59
  | `/evolve list · history · rollback <id>` | 查看与回滚(加 `project` 操作本项目库,`global` 操作跨项目库) |
60
60
  | `/evolve plan [msg]` | 对 store 运行 LLM 规划器 |
61
61
  | `/evolve wrapup` | 收尾本会话 local 条目:晋升 / 归档 / 保留 |
62
- | `/evolve archive · unarchive · demote <id>` | 从注入中隐藏(数据保留可恢复)——`demote` 针对全局噪声 |
63
- | `/evolve recall [scope] <query…>` | 定向记忆召回:全文 + 版本 + 来源 + 过时信号 |
64
- | `/evolve remember <type> [scope] <text…>` | 立即持久化一条类型化记忆(`user|feedback|project|reference`) |
65
- | `/evolve forget [scope] <query…>` | 按 query 定位一条记忆并归档(可恢复);多义只列候选不写 |
62
+ | `/evolve archive · unarchive <id>` | 从注入中隐藏(数据保留可恢复) |
66
63
  | `/evolve consolidate [apply] [merge]` | 报告(或应用)冲突提示 + 零使用陈旧全局条目的批量归档;`merge` 将近重复内容并入幸存原条目 |
67
64
  | `/evolve failures` | 失败类聚合(门禁 + benchmark) |
68
65
  | `/evolve log [tail N] [session <id>]` | 插件日志 |
@@ -73,13 +70,13 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
73
70
  | `/evolve pause · resume · status` | 暂停/恢复自动门禁(手动工具不受影响)、门禁状态 |
74
71
  | `/evolve usage` | 每条目注入次数 + memory/review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
75
72
 
76
- 模型工具:`evolve_list / add / update / delete / rollback / recall`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批;`evolve_recall` 按 query/kind/scope/memoryType/limit 过滤,返回全文 + 版本 + 来源 + 过时信号)。
73
+ 模型工具:`evolve_list / add / update / delete / rollback / recall`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批;`evolve_recall` 按 query/kind/scope/memoryType/limit 过滤,返回全文 + 版本 + 来源 + 过时信号)。记忆的读写都在对话里完成——模型手里有注入的记忆索引 + `evolve_recall`/`evolve_add`——因此不再有 `/evolve remember · forget · recall` 对应命令:直接在对话里说。
77
74
 
78
75
  第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
79
76
 
80
77
  `/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 memory Agent、review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
81
78
 
82
- 注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_recall`(精确)或 `evolve_list` 获取。每次 memory 落盘同时刷新 store 目录下的可读 `MEMORY.md` 索引 + 单事实文件。空 store = 零注入 token。
79
+ 注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory 以**正文**在会话开场注入:`evolve:memory-index` section(order 400)承载 when_to_save 指南 + 预算内的全部记忆正文,排序 `project > feedback > user > reference`,放不下的降级为 `[memory:type:id] 标题`钩子、其余折叠计数(硬上限 `memoryIndex.maxChars`,默认 6000 字符;未展示的用 `evolve_recall` 读)。该 section 每会话只算一次并逐字节复用,system prompt 因此稳定、prompt cache 持续命中——会话中途写入的记忆下个会话生效。skill 仍以按相关性排序的目录索引出现(15 行封顶 + 折叠计数行),memory 不再列入。每次 memory 落盘同时刷新 store 目录下的可读 `MEMORY.md` 索引 + 单事实文件。`memoryIndex.guide` 关闭且 store 为空时 = 零注入 token。
83
80
 
84
81
  ## 配置
85
82
 
@@ -100,7 +97,11 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
100
97
  | `promotionBlockPatterns` | POSIX 路径、session id、`~/.dsh` | 内容命中即判定项目专属,永不晋升全局 |
101
98
  | `promotionMinChars` | `100` | 低于此长度的整体晋升留在本地 |
102
99
  | `injectionDirectoryLines` | `15` | 每次构建的目录行数上限,超出折叠为计数行 |
103
- | `sectionOrder` | `118` | 系统提示词 section 顺序 |
100
+ | `sectionOrder` | `118` | 指南与条目两个 section 的系统提示词顺序 |
101
+ | `memoryIndex.enabled` | `true` | 是否注册 `evolve:memory-index` 内容 section |
102
+ | `memoryIndex.guide` | `true` | 是否注入 when_to_save 指南(关闭且 store 为空时该 section 不渲染) |
103
+ | `memoryIndex.order` | `400` | section 顺序(上游命名槽位自 `PLAN_POLICY=500` 起;本插件另两段在 118/119) |
104
+ | `memoryIndex.maxChars` | `6000` | 注入记忆块的硬字符预算 |
104
105
  | `skillsDir` | `<dshHome>/skills` | 技能条目物化为 SKILL.md 的根目录 |
105
106
  | `rubricKey` | 自动生成本地密钥文件 | benchmark rubric 的 AES-256-GCM 口令(`DSH_EVOLVE_RUBRIC_KEY` 可覆盖) |
106
107
  | `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | 插件自带 JSONL 文件日志带轮转 |
@@ -126,7 +127,7 @@ profile patch 示例:
126
127
 
127
128
  ```bash
128
129
  pnpm install && pnpm build # 依赖 + tsc -> lib/
129
- pnpm test # vitest(1075 例)
130
+ pnpm test # vitest(1097 例)
130
131
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
131
132
  pnpm coverage:gaps # 定位各文件未覆盖行(只读)
132
133
  pnpm lint # oxlint src test
@@ -136,7 +137,7 @@ pnpm lint # oxlint src test
136
137
 
137
138
  ```
138
139
  ├── src/ # 引擎、工具、命令、memory Agent、召回、投影、门禁、fate、benchmark、注入 + token 用量…
139
- ├── test/ # vitest 测试套件(58 个文件)
140
+ ├── test/ # vitest 测试套件(59 个文件)
140
141
  ├── lib/ # 构建产物(tsc)
141
142
  ├── docs/
142
143
  │ ├── design.md # 完整设计文档(硬化矩阵)
package/lib/command.js CHANGED
@@ -1,6 +1,5 @@
1
- import { ARCHIVED_AT_KEY, MEMORY_TYPE_KEY } from "./types.js";
1
+ import { ARCHIVED_AT_KEY } from "./types.js";
2
2
  import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
3
- import { compactText } from "./render.js";
4
3
  import { planWithLlm } from "./planner.js";
5
4
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
5
  import { join } from "node:path";
@@ -19,9 +18,8 @@ import { projectKeyOf } from "./project.js";
19
18
  import { loadUsage, getUsageCount } from "./usage.js";
20
19
  import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
21
20
  import { planConsolidation } from "./consolidate.js";
22
- import { formatRecallResult, recallMemories, RECALL_MEMORY_TYPES } from "./recall.js";
23
21
  import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
24
- const USAGE = `Usage:
22
+ const USAGE_COMMON = `Usage:
25
23
  /evolve show this help and the current local store
26
24
  /evolve list [project|global] list entries (default local staging; "project" = this project's cross-session store, "global" = cross-project store)
27
25
  /evolve history [global] show applied refinements (rollback ids)
@@ -31,19 +29,16 @@ const USAGE = `Usage:
31
29
  to the global store (approval required), archive one-offs
32
30
  /evolve archive <id> [global] hide an entry from injection (data kept, restorable)
33
31
  /evolve unarchive <id> [global] restore an archived entry
34
- /evolve demote <id> hide a (global) entry from injection, keep data
35
- /evolve recall <query...> [scope] targeted memory recall: full content with version,
36
- source, and staleness (same engine as the evolve_recall tool)
37
- /evolve remember <type> [scope] <text...>
38
- immediately persist one memory (type = user|feedback|project|reference;
39
- feedback/project text must carry Why + How; default scope local)
40
- /evolve forget [scope] <query...> locate one memory by query and archive it (restorable via /evolve unarchive)
32
+ /evolve pause | resume pause/resume the automatic Memory Agent (manual tools and commands keep working)
33
+ /evolve status automatic Memory Agent state plus store entry counts
34
+ /evolve help all every subcommand, including the advanced ones below`;
35
+ const USAGE_ADVANCED = `Advanced:
41
36
  /evolve consolidate [apply] [merge]
42
- report (or apply) a batch archive of conflict-hinted
43
- and stale zero-use global entries; "merge" folds
44
- near-duplicate content into the surviving original
45
- /evolve log [tail N] show the recent plugin log (default 50 lines)
37
+ report (or apply) a batch archive of conflict-hinted
38
+ and stale zero-use global entries; "merge" folds
39
+ near-duplicate content into the surviving original
46
40
  /evolve failures aggregated failure counts (gate + benchmark, by class)
41
+ /evolve log [tail N] show the recent plugin log (default 50 lines)
47
42
  /evolve export [global] <path> backup a store to a JSON file
48
43
  /evolve import [global] <path> restore a store from an export file
49
44
  /evolve mount <skillId> hot-mount a skill entry as a live cordis plugin
@@ -52,14 +47,15 @@ const USAGE = `Usage:
52
47
  /evolve goal show the evolution goal (round-driven auto-review)
53
48
  /evolve goal <objective> create/update the evolution goal
54
49
  /evolve goal done complete the evolution goal
55
- /evolve pause | resume pause/resume the automatic Memory Agent (manual tools and commands keep working)
56
- /evolve status automatic Memory Agent state plus store entry counts
50
+ /evolve benchmark ... case lifecycle, runs, acceptance
57
51
  /evolve usage injection counts + exact direct-call token usage (benchmark subagents excluded)`;
52
+ /** Default help (`/evolve`, `/evolve help`): the commands worth knowing. */
53
+ const USAGE = USAGE_COMMON;
58
54
  export function registerEvolveCommand(ctx, engine, opts, runtime) {
59
55
  ctx.commands.register({
60
56
  name: "evolve",
61
57
  description: "自进化:检查并演进 harness 状态(记忆/技能/提示词/子代理) | Inspect and evolve the continual harness state (memories, skills, prompt notes, subagent specs)",
62
- input: { hint: "[list [global] | history [global] | rollback <id> [global] | plan [msg]]" },
58
+ input: { hint: "[list [global] | history [global] | rollback <id> [global] | plan [msg] | help all]" },
63
59
  handler: (invocation) => executeEvolveCommand(ctx, engine, invocation, opts, runtime),
64
60
  });
65
61
  }
@@ -162,7 +158,9 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
162
158
  switch (sub) {
163
159
  case "":
164
160
  case "help":
165
- return success(`${USAGE}\n\n${formatHarnessStateForPrompt(engine.load("local", sessionId))}`);
161
+ // Layered help (2026-10-01): the default listing stays short;
162
+ // `help all` expands the advanced half.
163
+ return success(`${rest[0] === "all" ? `${USAGE_COMMON}\n\n${USAGE_ADVANCED}` : USAGE_COMMON}\n\n${formatHarnessStateForPrompt(engine.load("local", sessionId))}`);
166
164
  case "list": {
167
165
  const { scope } = scopeArg(rest);
168
166
  return success(formatHarnessStateForPrompt(engine.load(scope, storeIdForCommand(scope, invocation))));
@@ -182,16 +180,12 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
182
180
  return success(renderResult(result));
183
181
  }
184
182
  case "archive":
185
- case "unarchive":
186
- case "demote": {
183
+ case "unarchive": {
187
184
  const { scope, rest: after } = scopeArg(rest);
188
185
  const id = stripAngleBrackets(after[0] ?? "");
189
186
  if (!id) {
190
187
  return error(`${sub} requires an entry id.\n${USAGE}`);
191
188
  }
192
- if (sub === "demote") {
193
- return demoteEntry(engine, id, sessionId, projectKeyOf(invocation.agent));
194
- }
195
189
  const state = engine.load(scope, storeIdForCommand(scope, invocation));
196
190
  const found = findEntryById(state, id);
197
191
  if (!found) {
@@ -214,90 +208,6 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
214
208
  }, { scope });
215
209
  return success(renderResult(result));
216
210
  }
217
- case "recall": {
218
- // Human counterpart of the evolve_recall tool: same engine,
219
- // same filters (memory kind, one scope or all three).
220
- const { scope, rest: after } = scopeArg(rest);
221
- const explicitScope = rest.length > 0 && (rest[0] === "global" || rest[0] === "project" || rest[0] === "local");
222
- const query = after.join(" ");
223
- const result = recallMemories(engine, { sessionId, projectKey: projectKeyOf(invocation.agent) ?? undefined }, { kinds: ["memory"], ...(explicitScope ? { scopes: [scope] } : {}), ...(query ? { query } : {}), limit: 10 });
224
- return success(formatRecallResult(result, query || undefined));
225
- }
226
- case "remember": {
227
- // Immediate manual memory path (P1 §6.6): one fact, explicit
228
- // type, straight into the engine — validation, snapshot,
229
- // versioning, and approval behave exactly like any other
230
- // write. feedback/project text must already carry Why + How;
231
- // the engine rejects it loudly otherwise.
232
- const type = rest[0] ?? "";
233
- if (!RECALL_MEMORY_TYPES.includes(type)) {
234
- return error(`remember requires a memory type: user|feedback|project|reference.\nExample: /evolve remember feedback local 对方偏好深色主题。Why:…… How to apply:……\n${USAGE}`);
235
- }
236
- const maybeScope = rest[1] ?? "";
237
- const scoped = maybeScope === "global" || maybeScope === "project" || maybeScope === "local";
238
- const scope = scoped ? maybeScope : "local";
239
- const text = (scoped ? rest.slice(2) : rest.slice(1)).join(" ").trim();
240
- if (!text) {
241
- return error(`remember requires the memory text after the type.\n${USAGE}`);
242
- }
243
- const storeId = storeIdForCommand(scope, invocation);
244
- if ((scope === "global" || scope === "project") && opts.requireGlobalApproval) {
245
- await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve remember ${type} → ${scope} store: ${compactText(text, 120)}`);
246
- }
247
- const result = engine.apply(scope, storeId, {
248
- summary: `remember: ${compactText(text, 80)}`,
249
- rationale: "Human-invoked immediate memory via /evolve remember.",
250
- expectedOutcome: "The fact is persisted as a typed memory entry.",
251
- edits: [{
252
- action: "create",
253
- kind: "memory",
254
- title: compactText(text, 80),
255
- content: text,
256
- metadata: { [MEMORY_TYPE_KEY]: type },
257
- }],
258
- }, { scope });
259
- return success(renderResult(result));
260
- }
261
- case "forget": {
262
- // Manual forget path (P1 §6.6): locate by query, archive the
263
- // single match (restorable via unarchive). An ambiguous query
264
- // lists candidates instead of archiving — a silent multi-
265
- // archive would look like success while hiding too much.
266
- const { scope, rest: after } = scopeArg(rest);
267
- const explicitScope = rest.length > 0 && (rest[0] === "global" || rest[0] === "project" || rest[0] === "local");
268
- const query = after.join(" ").trim();
269
- if (!query) {
270
- return error(`forget requires a query to locate the memory.\n${USAGE}`);
271
- }
272
- const recalled = recallMemories(engine, { sessionId, projectKey: projectKeyOf(invocation.agent) ?? undefined }, { query, kinds: ["memory"], ...(explicitScope ? { scopes: [scope] } : {}), limit: 6, includeArchived: false });
273
- if (recalled.hits.length === 0) {
274
- return error(`no memory matches "${query}"${recalled.notes.length > 0 ? ` (${recalled.notes.join("; ")})` : ""} — try fewer words, or /evolve list to browse.`);
275
- }
276
- if (recalled.hits.length > 1) {
277
- const list = recalled.hits.map((hit) => `- [${hit.scope}:${hit.id}] ${hit.title}`).join("\n");
278
- return success(`"${query}" matches ${recalled.hits.length} memories — refine the query so exactly one matches, then forget archives it:\n${list}`);
279
- }
280
- const hit = recalled.hits[0];
281
- const state = engine.load(hit.scope, hit.scope === "local" ? sessionId : hit.scope === "project" ? (projectKeyOf(invocation.agent) ?? undefined) : undefined);
282
- const entry = state.entries.memory[hit.id];
283
- if (!entry) {
284
- return error(`memory ${hit.scope}:${hit.id} vanished since recall — run /evolve forget again.`);
285
- }
286
- const result = engine.apply(hit.scope, hit.scope === "local" ? sessionId : hit.scope === "project" ? (projectKeyOf(invocation.agent) ?? undefined) : undefined, {
287
- summary: `forget: archive memory ${hit.scope}:${hit.id}`,
288
- rationale: `Human-invoked forget via /evolve forget "${query}".`,
289
- expectedOutcome: "The entry is hidden from injection (data kept; restorable via /evolve unarchive).",
290
- edits: [{
291
- action: "update",
292
- kind: "memory",
293
- id: hit.id,
294
- title: entry.title,
295
- content: entry.content,
296
- metadata: { ...entry.metadata, [ARCHIVED_AT_KEY]: new Date().toISOString() },
297
- }],
298
- }, { scope: hit.scope });
299
- return success(`forgot [${hit.scope}:${hit.id}] ${hit.title} (archived — restore with /evolve unarchive ${hit.id}${hit.scope === "local" ? "" : ` ${hit.scope}`})\n${renderResult(result)}`);
300
- }
301
211
  case "consolidate": {
302
212
  // R3: deterministic global-store hygiene. Report by default;
303
213
  // `apply` re-scans fresh state and lands the whole batch as ONE
@@ -511,44 +421,6 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
511
421
  return error(cause instanceof Error ? cause.message : String(cause));
512
422
  }
513
423
  }
514
- /**
515
- * Demote (2026-08-22): hide an entry from injection WITHOUT deleting it —
516
- * the one-command remedy for store pollution. Searches the global store
517
- * first (the primary target: cross-project noise), then the project store,
518
- * then the session's local store. The data stays; `/evolve unarchive` restores it.
519
- */
520
- function demoteEntry(engine, id, sessionId, projectKey) {
521
- const targets = [
522
- { scope: "global", storeId: undefined },
523
- ...(projectKey ? [{ scope: "project", storeId: projectKey }] : []),
524
- { scope: "local", storeId: sessionId },
525
- ];
526
- for (const { scope, storeId } of targets) {
527
- const state = engine.load(scope, storeId);
528
- const found = findEntryById(state, id);
529
- if (!found)
530
- continue;
531
- const [kind, entry] = found;
532
- const result = engine.apply(scope, storeId, {
533
- summary: `demote: archive ${kind}:${id} from the ${scope} store`,
534
- rationale: "Human-invoked demote via the /evolve command.",
535
- expectedOutcome: "The entry is hidden from injection in every scope it touched; data is kept and restorable.",
536
- edits: [
537
- {
538
- action: "update",
539
- kind,
540
- id,
541
- title: entry.title,
542
- content: entry.content,
543
- metadata: { ...entry.metadata, [ARCHIVED_AT_KEY]: new Date().toISOString() },
544
- },
545
- ],
546
- }, { scope });
547
- const restoreScope = scope === "global" ? " global" : scope === "project" ? " project" : "";
548
- return success(`demoted ${kind}:${id} from the ${scope} store (archived — restore with /evolve unarchive ${id}${restoreScope})\n${renderResult(result)}`);
549
- }
550
- return error(`entry ${id} not found in the global, project, or local store`);
551
- }
552
424
  /**
553
425
  * Status answers whether the automatic listener is wired and what
554
426
  * stores/retention it currently sees.
package/lib/index.d.ts CHANGED
@@ -98,6 +98,24 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
98
98
  promotionMinChars: z<number, number, "defined">;
99
99
  /** Entry-directory lines injected per build before folding into a counter. */
100
100
  injectionDirectoryLines: z<number, number, "defined">;
101
+ /**
102
+ * evolve v2 memory index: the section that injects memory CONTENT (not just
103
+ * an id/title directory) at session start, frozen per session to keep the
104
+ * system prompt byte-stable (prompt cache). `enabled`/`guide`/`order`/
105
+ * `maxChars` default to true/true/400/6000; with `guide` off and an empty
106
+ * store the section renders to "" and costs no tokens.
107
+ */
108
+ memoryIndex: z<Schemastery.ObjectS<NoInfer<{
109
+ enabled: z<boolean, boolean, "plain">;
110
+ guide: z<boolean, boolean, "plain">;
111
+ order: z<number, number, "plain">;
112
+ maxChars: z<number, number, "plain">;
113
+ }>>, Schemastery.ObjectT<NoInfer<{
114
+ enabled: z<boolean, boolean, "plain">;
115
+ guide: z<boolean, boolean, "plain">;
116
+ order: z<number, number, "plain">;
117
+ maxChars: z<number, number, "plain">;
118
+ }>>, "plain">;
101
119
  /**
102
120
  * Storage hygiene (#20): how much append-only past each write keeps.
103
121
  * `snapshots` = full-state copies per store, `refinements` = tail lines
@@ -210,6 +228,24 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
210
228
  promotionMinChars: z<number, number, "defined">;
211
229
  /** Entry-directory lines injected per build before folding into a counter. */
212
230
  injectionDirectoryLines: z<number, number, "defined">;
231
+ /**
232
+ * evolve v2 memory index: the section that injects memory CONTENT (not just
233
+ * an id/title directory) at session start, frozen per session to keep the
234
+ * system prompt byte-stable (prompt cache). `enabled`/`guide`/`order`/
235
+ * `maxChars` default to true/true/400/6000; with `guide` off and an empty
236
+ * store the section renders to "" and costs no tokens.
237
+ */
238
+ memoryIndex: z<Schemastery.ObjectS<NoInfer<{
239
+ enabled: z<boolean, boolean, "plain">;
240
+ guide: z<boolean, boolean, "plain">;
241
+ order: z<number, number, "plain">;
242
+ maxChars: z<number, number, "plain">;
243
+ }>>, Schemastery.ObjectT<NoInfer<{
244
+ enabled: z<boolean, boolean, "plain">;
245
+ guide: z<boolean, boolean, "plain">;
246
+ order: z<number, number, "plain">;
247
+ maxChars: z<number, number, "plain">;
248
+ }>>, "plain">;
213
249
  /**
214
250
  * Storage hygiene (#20): how much append-only past each write keeps.
215
251
  * `snapshots` = full-state copies per store, `refinements` = tail lines
package/lib/index.js CHANGED
@@ -8,6 +8,10 @@
8
8
  * initial default when `evolve/runtime.json` does not exist yet, and
9
9
  * `/evolve pause|resume` own it afterwards. Generic review/planner/fate are
10
10
  * not reachable from this listener.
11
+ *
12
+ * evolve v2 registers a third section (`evolve:memory-index`) that injects
13
+ * memory content plus the when_to_save guide, frozen per session — see
14
+ * memory-index.ts.
11
15
  */
12
16
  import { join } from "node:path";
13
17
  import z from "@deepseek-ai/schemastery";
@@ -18,6 +22,7 @@ import { registerEvolveCommand } from "./command.js";
18
22
  import { registerAutoReview } from "./auto.js";
19
23
  import { syncSkillsFromResult } from "./skill.js";
20
24
  import { entriesSectionText } from "./inject.js";
25
+ import { createFrozenMemorySection, DEFAULT_MEMORY_INDEX_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memoryIndexSectionText, } from "./memory-index.js";
21
26
  import { resolveRubricKey } from "./rubric.js";
22
27
  import { restoreMounted } from "./mount.js";
23
28
  import { registerFileLogger } from "./logfile.js";
@@ -119,6 +124,19 @@ export const Config = z.object({
119
124
  promotionMinChars: z.natural().default(100),
120
125
  /** Entry-directory lines injected per build before folding into a counter. */
121
126
  injectionDirectoryLines: z.natural().default(15),
127
+ /**
128
+ * evolve v2 memory index: the section that injects memory CONTENT (not just
129
+ * an id/title directory) at session start, frozen per session to keep the
130
+ * system prompt byte-stable (prompt cache). `enabled`/`guide`/`order`/
131
+ * `maxChars` default to true/true/400/6000; with `guide` off and an empty
132
+ * store the section renders to "" and costs no tokens.
133
+ */
134
+ memoryIndex: z.object({
135
+ enabled: z.boolean(),
136
+ guide: z.boolean(),
137
+ order: z.natural(),
138
+ maxChars: z.natural(),
139
+ }),
122
140
  /**
123
141
  * Storage hygiene (#20): how much append-only past each write keeps.
124
142
  * `snapshots` = full-state copies per store, `refinements` = tail lines
@@ -162,11 +180,35 @@ export function apply(ctx, config) {
162
180
  // entries real delegation specs. The text is a provider evaluated at every
163
181
  // assembly with the assembling agent; a store without prompt/subagent
164
182
  // entries renders to "" and the prompt renderer drops the section.
183
+ //
184
+ // evolve v2: when the memory index section carries memory content, the
185
+ // directory stops listing memories so the same entries are never paid for
186
+ // twice (one fact, one home).
187
+ const memoryIndexEnabled = config.memoryIndex?.enabled ?? true;
165
188
  ctx.systemPrompt.section({
166
189
  name: "tool:continual-evolve:entries",
167
190
  order: (config.sectionOrder ?? 118) + 1,
168
- text: (context) => entriesSectionText(engine, context.agent, undefined, { directoryLines: config.injectionDirectoryLines ?? 15 }),
191
+ text: (context) => entriesSectionText(engine, context.agent, undefined, {
192
+ directoryLines: config.injectionDirectoryLines ?? 15,
193
+ includeMemoryDirectory: !memoryIndexEnabled,
194
+ }),
169
195
  });
196
+ // evolve v2 passive read path: memory content in the prompt at session
197
+ // start, frozen per session (see memory-index.ts) so the whole system
198
+ // prompt stays byte-stable within a session and prompt-cache reads keep
199
+ // hitting. Registered in the empty 1–499 order slot — the two sections
200
+ // above sit at 118/119, upstream's named slots start at PLAN_POLICY=500.
201
+ if (memoryIndexEnabled) {
202
+ const frozenMemories = createFrozenMemorySection();
203
+ const memoryIndexOrder = config.memoryIndex?.order ?? DEFAULT_MEMORY_SECTION_ORDER;
204
+ const memoryIndexMaxChars = config.memoryIndex?.maxChars ?? DEFAULT_MEMORY_INDEX_MAX_CHARS;
205
+ const memoryIndexGuide = config.memoryIndex?.guide ?? true;
206
+ ctx.systemPrompt.section({
207
+ name: MEMORY_SECTION_NAME,
208
+ order: memoryIndexOrder,
209
+ text: (context) => frozenMemories.textFor(context.agent, (agent) => memoryIndexSectionText(engine, agent, { maxChars: memoryIndexMaxChars, guide: memoryIndexGuide })),
210
+ });
211
+ }
170
212
  const gate = { requireGlobalApproval: config.requireGlobalApproval ?? true };
171
213
  const promotionPolicy = resolvePromotionPolicy({
172
214
  blockPatterns: config.promotionBlockPatterns,
package/lib/inject.d.ts CHANGED
@@ -158,6 +158,16 @@ export declare function rankedDirectoryEntries(maxLines: number, query: string |
158
158
  * beyond the nearest carrying store).
159
159
  */
160
160
  export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, agent: AgentLike): HarnessState | undefined;
161
+ /**
162
+ * The single three-store merge every injection path reads from: the global
163
+ * store merged with this project's store (when the session cwd resolves one)
164
+ * and the nearest carrying local store (precedence global < project < local —
165
+ * see {@link mergeHarnessStates}). One home, so the memory section and the
166
+ * entry directory can never disagree about what the model can see.
167
+ */
168
+ export declare function mergedInjectionState(engine: EvolutionEngine, agent: AgentLike, opts?: {
169
+ projectKey?: string;
170
+ }): HarnessState;
161
171
  /**
162
172
  * Compose the full injected block for one assembling agent: global entries
163
173
  * merged with this project's store (when the session cwd resolves one) and
@@ -171,6 +181,9 @@ export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, ag
171
181
  * `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
172
182
  * `opts.projectKey` pins the project layer explicitly (tests, tools) —
173
183
  * otherwise it is derived from the agent's session cwd, best-effort.
184
+ * `opts.includeMemoryDirectory` (default true) drops memory lines from the
185
+ * directory when the memory section already carries them (evolve v2) — one
186
+ * fact, one home, so the same memories are never paid for twice.
174
187
  * Usage recording covers ALL kinds — memories and skills appear as directory
175
188
  * lines, prompts/subagents as content — and is deduped per session so the
176
189
  * counts read "how many sessions saw this", not "how many prompt builds".
@@ -178,5 +191,6 @@ export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, ag
178
191
  export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string, opts?: {
179
192
  directoryLines?: number;
180
193
  projectKey?: string;
194
+ includeMemoryDirectory?: boolean;
181
195
  }): string;
182
196
  //# sourceMappingURL=inject.d.ts.map
package/lib/inject.js CHANGED
@@ -324,6 +324,20 @@ export function nearestLocalStateWithEntries(engine, agent) {
324
324
  }
325
325
  return undefined;
326
326
  }
327
+ /**
328
+ * The single three-store merge every injection path reads from: the global
329
+ * store merged with this project's store (when the session cwd resolves one)
330
+ * and the nearest carrying local store (precedence global < project < local —
331
+ * see {@link mergeHarnessStates}). One home, so the memory section and the
332
+ * entry directory can never disagree about what the model can see.
333
+ */
334
+ export function mergedInjectionState(engine, agent, opts) {
335
+ const globalState = engine.load("global", undefined);
336
+ const projectKey = opts?.projectKey ?? projectKeyOf(agent);
337
+ const projectState = projectKey ? engine.load("project", projectKey) : undefined;
338
+ const localState = nearestLocalStateWithEntries(engine, agent);
339
+ return mergeHarnessStates(globalState, localState, projectState ? { projectState } : undefined);
340
+ }
327
341
  /**
328
342
  * Compose the full injected block for one assembling agent: global entries
329
343
  * merged with this project's store (when the session cwd resolves one) and
@@ -337,6 +351,9 @@ export function nearestLocalStateWithEntries(engine, agent) {
337
351
  * `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
338
352
  * `opts.projectKey` pins the project layer explicitly (tests, tools) —
339
353
  * otherwise it is derived from the agent's session cwd, best-effort.
354
+ * `opts.includeMemoryDirectory` (default true) drops memory lines from the
355
+ * directory when the memory section already carries them (evolve v2) — one
356
+ * fact, one home, so the same memories are never paid for twice.
340
357
  * Usage recording covers ALL kinds — memories and skills appear as directory
341
358
  * lines, prompts/subagents as content — and is deduped per session so the
342
359
  * counts read "how many sessions saw this", not "how many prompt builds".
@@ -345,11 +362,7 @@ export function entriesSectionText(engine, agent, query, opts) {
345
362
  if (!agent) {
346
363
  return "";
347
364
  }
348
- const globalState = engine.load("global", undefined);
349
- const projectKey = opts?.projectKey ?? projectKeyOf(agent);
350
- const projectState = projectKey ? engine.load("project", projectKey) : undefined;
351
- const localState = nearestLocalStateWithEntries(engine, agent);
352
- const merged = mergeHarnessStates(globalState, localState, projectState ? { projectState } : undefined);
365
+ const merged = mergedInjectionState(engine, agent, opts);
353
366
  const promptEntries = Object.values(merged.entries.prompt);
354
367
  const subagentEntries = Object.values(merged.entries.subagent);
355
368
  const relevanceQuery = (query ?? recentUserText(agent)).trim();
@@ -371,7 +384,9 @@ export function entriesSectionText(engine, agent, query, opts) {
371
384
  const directoryLines = opts?.directoryLines ?? DEFAULT_DIRECTORY_LINES;
372
385
  const promptKind = Object.values(merged.entries.prompt);
373
386
  const subagentKind = Object.values(merged.entries.subagent);
374
- const memoryKind = Object.values(merged.entries.memory);
387
+ // v2: memories live in their own content-carrying section, so the directory
388
+ // stops listing them (passing [] keeps the positional contract intact).
389
+ const memoryKind = (opts?.includeMemoryDirectory ?? true) ? Object.values(merged.entries.memory) : [];
375
390
  const skillKind = Object.values(merged.entries.skill);
376
391
  const directoryText = formatEntriesDirectoryRanked(directoryLines, relevanceQuery, promptKind, subagentKind, memoryKind, skillKind);
377
392
  // Directory-visible keys count too: a memory's injection IS its directory
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The persistent-memory guide injected as part of the `evolve:memory-index`
3
+ * system-prompt section (evolve v2, change two).
4
+ *
5
+ * This text is the write-path half of the v2 plan: it teaches the model when a
6
+ * durable memory is worth creating, which recall type it belongs to, what body
7
+ * structure each type owes, and that the main session may write directly with
8
+ * `evolve_add` instead of waiting for the background pipeline.
9
+ *
10
+ * Provenance: `docs/research/evolve-v2-passive-read-write-plan.md` §改动二
11
+ * (Chinese full text, adapted from ZCode's persistent-memory prompt). Kept in
12
+ * its own module so the wording is a single home — `memory-index.ts` only
13
+ * composes it with the `<memories>` block.
14
+ */
15
+ /** Heading and framing before the `<memories>` block. */
16
+ export declare const MEMORY_GUIDE_INTRO = "# \u6301\u4E45\u8BB0\u5FC6\n\n\u4F60\u6709\u4E00\u4E2A\u8DE8\u4F1A\u8BDD\u7684\u6301\u4E45\u8BB0\u5FC6\u7CFB\u7EDF\u3002\u4EE5\u4E0B\u662F\u5F53\u524D\u8BB0\u5FC6\uFF08\u5DF2\u76F4\u63A5\u6CE8\u5165\uFF0C\u65E0\u9700\u67E5\u8BE2\u5373\u53EF\u4F7F\u7528\uFF1B\u9700\u8981\u68C0\u7D22\u5B8C\u6574\u5185\u5BB9\u6216\u786E\u8BA4\u662F\u5426\u6709\u76F8\u5173\u8BB0\u5FC6\u65F6\uFF0C\u8C03\u7528 evolve_recall\uFF09\u3002\u672C\u4F1A\u8BDD\u5185\u53EF\u4EE5\u76F4\u63A5\u7528 evolve_add \u843D\u76D8\uFF0C\u4E0D\u5FC5\u7B49\u540E\u53F0\u6D41\u7A0B\u2014\u2014\u9ED8\u8BA4\u5199 local\uFF0C\u53EA\u6709\u7A33\u5B9A\u7684\u8DE8\u9879\u76EE\u6559\u8BAD\u624D\u5199 global\uFF08global \u5199\u5165\u4F1A\u5F39\u4EBA\u5DE5\u6279\u51C6\uFF09\u3002";
17
+ /** The when_to_save / how_to_use rules and maintenance discipline. */
18
+ export declare const MEMORY_GUIDE_RULES = "## \u4F55\u65F6\u5199\u5165\u8BB0\u5FC6\n\n\u7528\u6237\u660E\u786E\u8981\u6C42\u8BB0\u4F4F\u65F6\uFF0C\u7ACB\u5373\u5B58\u4E3A\u6700\u5408\u9002\u7684\u7C7B\u578B\uFF1B\u8981\u6C42\u5FD8\u8BB0\u65F6\uFF0C\u627E\u5230\u5E76\u5220\u9664\u76F8\u5173\u6761\u76EE\u3002\n\u9664\u6B64\u4E4B\u5916\uFF0C\u5728\u5BF9\u8BDD\u4E2D\u81EA\u7136\u9047\u5230\u4EE5\u4E0B\u4FE1\u53F7\u65F6\u4E3B\u52A8\u5199\u5165\u3002\u5B58\u4E4B\u524D\u5148\u8FC7\u4E00\u9053\u7B5B\u5B50\uFF1A\n**\u80FD\u4ECE\u4EE3\u7801\u3001git \u5386\u53F2\u3001\u4ED3\u5E93\u6587\u4EF6\u91CC\u91CD\u65B0\u63A8\u5BFC\u51FA\u6765\u7684\u4E8B\u5B9E\u4E0D\u5B58**\u2014\u2014\u53EA\u5B58\u91CD\u63A8\u5BFC\u4E0D\u51FA\u6765\u7684\n\u80CC\u666F\u3001\u51B3\u7B56\u548C\u504F\u597D\u3002\u76F8\u5BF9\u65E5\u671F\u4E00\u5F8B\u8F6C\u6210\u7EDD\u5BF9\u65E5\u671F\uFF08\"\u5468\u56DB\"\u2192\"2026-10-08\"\uFF09\uFF0C\u5426\u5219\u672A\u6765\n\u7684\u4F1A\u8BDD\u65E0\u6CD5\u89E3\u8BFB\u3002\u4E00\u6761\u4E8B\u5B9E\u4E00\u4E2A\u6761\u76EE\uFF1B\u4E0D\u786E\u5B9A\u503C\u4E0D\u503C\u5F97\u5B58\u65F6\uFF0C\u95EE\u81EA\u5DF1\"\u8FD9\u6761\u7684\u4FE1\u606F\u91CF\n\u662F\u5426\u5728'\u4E3A\u4EC0\u4E48'\u91CC\uFF0C\u800C\u4E0D\u5728'\u662F\u4EC0\u4E48'\u91CC\"\u3002\n\n<type name=\"user\">\n <when_to_save>\u5F97\u77E5\u7528\u6237\u7684\u89D2\u8272\u3001\u6280\u672F\u6808\u3001\u7ECF\u9A8C\u6C34\u5E73\u3001\u504F\u597D\u6216\u77E5\u8BC6\u80CC\u666F\u7684\u4EFB\u4F55\u7EC6\u8282\u65F6\u3002\n \u4F8B\u5982\u7528\u6237\u81EA\u8FF0\"\u6211\u662F\u6570\u636E\u79D1\u5B66\u5BB6\uFF0C\u5728\u67E5\u65E5\u5FD7\u7CFB\u7EDF\"\u2014\u2014\u5B58\uFF1A\u7528\u6237\u662F\u6570\u636E\u79D1\u5B66\u5BB6\uFF0C\n \u5F53\u524D\u5173\u6CE8\u53EF\u89C2\u6D4B\u6027/\u65E5\u5FD7\u3002</when_to_save>\n <how_to_use>\u540E\u7EED\u89E3\u91CA\u548C\u5EFA\u8BAE\u6309\u7528\u6237\u7684\u80CC\u666F\u5B9A\u5236\u6DF1\u6D45\u4E0E\u7C7B\u6BD4\uFF1B\u5BF9\u8D44\u6DF1\u5DE5\u7A0B\u5E08\n \u4E0D\u89E3\u91CA\u57FA\u7840\u6982\u5FF5\uFF0C\u5BF9\u67D0\u9886\u57DF\u7684\u65B0\u624B\u7528\u5176\u719F\u6089\u9886\u57DF\u7684\u7C7B\u6BD4\u5207\u5165\u3002</how_to_use>\n</type>\n\n<type name=\"feedback\">\n <when_to_save>\u4E24\u6761\u4FE1\u53F7\uFF0C\u7F3A\u4E00\u4E0D\u53EF\uFF1A\n \uFF081\uFF09\u7EA0\u6B63\u2014\u2014\u7528\u6237\u8BF4\"\u4E0D\u662F\u8FD9\u6837\"\"\u522B\"\"\u505C\u4E0B\"\u65F6\uFF0C\u5B58\u88AB\u7EA0\u6B63\u7684\u505A\u6CD5\u4E0E\u6B63\u786E\u505A\u6CD5\uFF1B\n \uFF082\uFF09\u786E\u8BA4\u2014\u2014\u8FD9\u662F\u66F4\u5B89\u9759\u3001\u66F4\u5BB9\u6613\u6F0F\u7684\u4FE1\u53F7\uFF1A\u7528\u6237\u63A5\u53D7\u4E86\u4F60\u4E0D\u5BFB\u5E38\u7684\u9009\u62E9\u800C\u6CA1\u6709\u5F02\u8BAE\uFF0C\n \u6216\u660E\u786E\u8BF4\"\u5BF9\uFF0C\u5C31\u8FD9\u6837\"\"\u4FDD\u6301\u8FD9\u6837\"\u3002\u786E\u8BA4\u8FC7\u7684\u5224\u65AD\u662F\u5DF2\u9A8C\u8BC1\u7684\u65B9\u6CD5\u8BBA\uFF0C\u53EA\u5B58\u7EA0\u9519\n \u4F1A\u8BA9\u4F60\u7684\u884C\u4E3A\u8D8A\u6765\u8D8A\u4FDD\u5B88\u3001\u504F\u79BB\u7528\u6237\u5DF2\u8BA4\u53EF\u7684\u8DEF\u3002\n \u4F8B\uFF1A\"\u6D4B\u8BD5\u522B mock \u6570\u636E\u5E93\uFF0C\u4E0A\u6B21 mock \u8FC7\u4E86\u7EBF\u4E0A\u8FC1\u79FB\u6302\u4E86\"\u2014\u2014\u5B58\u89C4\u5219\u5E76\u5E26\u4E0A\u4E8B\u6545\u539F\u56E0\u3002\n \u4F8B\uFF1A\"\u7ED3\u5C3E\u4E0D\u7528\u603B\u7ED3\uFF0Cdiff \u6211\u81EA\u5DF1\u4F1A\u770B\"\u2014\u2014\u5B58\uFF1A\u8BE5\u7528\u6237\u4E0D\u8981\u5C3E\u968F\u6027\u603B\u7ED3\u3002</when_to_save>\n <how_to_use>\u8BA9\u7528\u6237\u4E0D\u5FC5\u628A\u540C\u4E00\u6761\u6307\u5BFC\u8BF4\u7B2C\u4E8C\u904D\u3002</how_to_use>\n <body_structure>\u89C4\u5219\u5148\u884C\uFF0C\u968F\u540E\u5FC5\u5E26\u4E24\u884C\uFF1A**Why:**\uFF08\u7528\u6237\u7ED9\u7684\u7406\u7531\uFF0C\u5E38\u662F\u67D0\u6B21\n \u4E8B\u6545\u6216\u5F3A\u504F\u597D\u2014\u2014\u77E5\u9053\u4E3A\u4EC0\u4E48\u624D\u80FD\u5224\u65AD\u8FB9\u7F18\u60C5\u51B5\u8BE5\u4E0D\u8BE5\u7834\u4F8B\uFF09\u548C **How to apply:**\n \uFF08\u4F55\u65F6\u4F55\u5730\u751F\u6548\uFF09\u3002feedback \u7C7B\u578B\u7F3A\u8FD9\u4E24\u884C\u89C6\u4E3A\u4E0D\u5B8C\u6574\u3002</body_structure>\n</type>\n\n<type name=\"project\">\n <when_to_save>\u5F97\u77E5\u8FDB\u884C\u4E2D\u7684\u5DE5\u4F5C\u3001\u76EE\u6807\u3001\u51B3\u7B56\u3001\u622A\u6B62\u671F\u6216\u4E8B\u6545\u7684\u80CC\u666F\u52A8\u673A\u65F6\u2014\u2014\n \u51E1\u662F\"\u8C01\u5728\u505A\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u3001\u5230\u4EC0\u4E48\u65F6\u5019\"\u4E14\u65E0\u6CD5\u4ECE\u4EE3\u7801\u6216 git \u63A8\u5BFC\u7684\u4FE1\u606F\u3002\n \u8FD9\u7C7B\u8BB0\u5FC6\u8870\u51CF\u5FEB\uFF0C\u72B6\u6001\u53D8\u5316\u65F6\u66F4\u65B0\u5B83\uFF0C\u522B\u5806\u53E0\u65B0\u6761\u76EE\u3002</when_to_save>\n <how_to_use>\u7528\u5B83\u8865\u5168\u8BF7\u6C42\u80CC\u540E\u7684\u4E0A\u4E0B\u6587\u4E0E\u52A8\u673A\uFF0C\u8BA9\u5EFA\u8BAE\u8D34\u4F4F\u771F\u5B9E\u7EA6\u675F\u3002\n \u4F8B\uFF1A\"\u5468\u56DB\u8D77\u51BB\u7ED3\u5408\u5E76\"\u2014\u2014\u5B58\uFF1A2026-03-05 \u8D77\u51BB\u7ED3\u5408\u5E76\uFF0C\u975E\u5173\u952E PR \u63D0\u524D\u6807\u8BB0\u3002</how_to_use>\n <body_structure>\u4E8B\u5B9E/\u51B3\u7B56\u5148\u884C + **Why:**\uFF08\u52A8\u673A\uFF1A\u7EA6\u675F\u3001deadline\u3001\u5E72\u7CFB\u4EBA\u8981\u6C42\uFF09\n + **How to apply:**\uFF08\u8FD9\u6761\u5982\u4F55\u5F71\u54CD\u540E\u7EED\u5EFA\u8BAE\uFF09\u3002</body_structure>\n</type>\n\n<type name=\"reference\">\n <when_to_save>\u7528\u6237\u63D0\u5230\u672A\u6765\u4F1A\u7528\u5230\u7684\u5916\u90E8\u8D44\u6E90\u6307\u9488\uFF1AURL\u3001\u4EEA\u8868\u76D8\u3001\u5DE5\u5355\u3001\n \u6587\u6863\u4F4D\u7F6E\u3002</when_to_save>\n <how_to_use>\u9700\u8981\u65F6\u76F4\u63A5\u53D6\u7528\uFF0C\u4E0D\u91CD\u65B0\u68C0\u7D22\u3002</how_to_use>\n</type>\n\n## \u5982\u4F55\u7EF4\u62A4\n\n- \u66F4\u65B0\u4F18\u5148\u4E8E\u65B0\u5EFA\uFF1A\u72B6\u6001\u53D8\u4E86\u6539\u539F\u6761\u76EE\uFF0C\u4E0D\u53E6\u8D77\u4E00\u6761\uFF08\u65B0\u65E7\u5E76\u5B58\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\uFF09\u3002\n- \u6BCF\u6761\u7684 description \u662F\u7ED9\u672A\u6765\u4F1A\u8BDD\u7684\u76F8\u5173\u6027 hook\u2014\u2014\u5199\"\u51B3\u5B9A\u4E86\u4E00\u6761\u8BB0\u5FC6\u4F1A\u4E0D\u4F1A\n \u88AB\u60F3\u8D77\"\u7684\u4E00\u53E5\u8BDD\uFF0C\u4E0D\u5199\u6D41\u6C34\u8D26\u3002\n- [[\u540D\u5B57]] \u4E92\u76F8\u5F15\u7528\u76F8\u5173\u8BB0\u5FC6\uFF1B\u5F15\u7528\u8FD8\u4E0D\u5B58\u5728\u7684\u540D\u5B57\u662F\u5408\u6CD5\u7684\uFF0C\u5B83\u6807\u8BB0\"\u503C\u5F97\u5C06\u6765\u8865\u5199\"\u3002";
19
+ //# sourceMappingURL=memory-guide.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The persistent-memory guide injected as part of the `evolve:memory-index`
3
+ * system-prompt section (evolve v2, change two).
4
+ *
5
+ * This text is the write-path half of the v2 plan: it teaches the model when a
6
+ * durable memory is worth creating, which recall type it belongs to, what body
7
+ * structure each type owes, and that the main session may write directly with
8
+ * `evolve_add` instead of waiting for the background pipeline.
9
+ *
10
+ * Provenance: `docs/research/evolve-v2-passive-read-write-plan.md` §改动二
11
+ * (Chinese full text, adapted from ZCode's persistent-memory prompt). Kept in
12
+ * its own module so the wording is a single home — `memory-index.ts` only
13
+ * composes it with the `<memories>` block.
14
+ */
15
+ /** Heading and framing before the `<memories>` block. */
16
+ export const MEMORY_GUIDE_INTRO = `# 持久记忆
17
+
18
+ 你有一个跨会话的持久记忆系统。以下是当前记忆(已直接注入,无需查询即可使用;需要检索完整内容或确认是否有相关记忆时,调用 evolve_recall)。本会话内可以直接用 evolve_add 落盘,不必等后台流程——默认写 local,只有稳定的跨项目教训才写 global(global 写入会弹人工批准)。`;
19
+ /** The when_to_save / how_to_use rules and maintenance discipline. */
20
+ export const MEMORY_GUIDE_RULES = `## 何时写入记忆
21
+
22
+ 用户明确要求记住时,立即存为最合适的类型;要求忘记时,找到并删除相关条目。
23
+ 除此之外,在对话中自然遇到以下信号时主动写入。存之前先过一道筛子:
24
+ **能从代码、git 历史、仓库文件里重新推导出来的事实不存**——只存重推导不出来的
25
+ 背景、决策和偏好。相对日期一律转成绝对日期("周四"→"2026-10-08"),否则未来
26
+ 的会话无法解读。一条事实一个条目;不确定值不值得存时,问自己"这条的信息量
27
+ 是否在'为什么'里,而不在'是什么'里"。
28
+
29
+ <type name="user">
30
+ <when_to_save>得知用户的角色、技术栈、经验水平、偏好或知识背景的任何细节时。
31
+ 例如用户自述"我是数据科学家,在查日志系统"——存:用户是数据科学家,
32
+ 当前关注可观测性/日志。</when_to_save>
33
+ <how_to_use>后续解释和建议按用户的背景定制深浅与类比;对资深工程师
34
+ 不解释基础概念,对某领域的新手用其熟悉领域的类比切入。</how_to_use>
35
+ </type>
36
+
37
+ <type name="feedback">
38
+ <when_to_save>两条信号,缺一不可:
39
+ (1)纠正——用户说"不是这样""别""停下"时,存被纠正的做法与正确做法;
40
+ (2)确认——这是更安静、更容易漏的信号:用户接受了你不寻常的选择而没有异议,
41
+ 或明确说"对,就这样""保持这样"。确认过的判断是已验证的方法论,只存纠错
42
+ 会让你的行为越来越保守、偏离用户已认可的路。
43
+ 例:"测试别 mock 数据库,上次 mock 过了线上迁移挂了"——存规则并带上事故原因。
44
+ 例:"结尾不用总结,diff 我自己会看"——存:该用户不要尾随性总结。</when_to_save>
45
+ <how_to_use>让用户不必把同一条指导说第二遍。</how_to_use>
46
+ <body_structure>规则先行,随后必带两行:**Why:**(用户给的理由,常是某次
47
+ 事故或强偏好——知道为什么才能判断边缘情况该不该破例)和 **How to apply:**
48
+ (何时何地生效)。feedback 类型缺这两行视为不完整。</body_structure>
49
+ </type>
50
+
51
+ <type name="project">
52
+ <when_to_save>得知进行中的工作、目标、决策、截止期或事故的背景动机时——
53
+ 凡是"谁在做什么、为什么、到什么时候"且无法从代码或 git 推导的信息。
54
+ 这类记忆衰减快,状态变化时更新它,别堆叠新条目。</when_to_save>
55
+ <how_to_use>用它补全请求背后的上下文与动机,让建议贴住真实约束。
56
+ 例:"周四起冻结合并"——存:2026-03-05 起冻结合并,非关键 PR 提前标记。</how_to_use>
57
+ <body_structure>事实/决策先行 + **Why:**(动机:约束、deadline、干系人要求)
58
+ + **How to apply:**(这条如何影响后续建议)。</body_structure>
59
+ </type>
60
+
61
+ <type name="reference">
62
+ <when_to_save>用户提到未来会用到的外部资源指针:URL、仪表盘、工单、
63
+ 文档位置。</when_to_save>
64
+ <how_to_use>需要时直接取用,不重新检索。</how_to_use>
65
+ </type>
66
+
67
+ ## 如何维护
68
+
69
+ - 更新优先于新建:状态变了改原条目,不另起一条(新旧并存比没有记忆更糟)。
70
+ - 每条的 description 是给未来会话的相关性 hook——写"决定了一条记忆会不会
71
+ 被想起"的一句话,不写流水账。
72
+ - [[名字]] 互相引用相关记忆;引用还不存在的名字是合法的,它标记"值得将来补写"。`;
73
+ //# sourceMappingURL=memory-guide.js.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * evolve v2 — the passive read path: the `evolve:memory-index` system-prompt
3
+ * section that puts memory CONTENT in front of the model at session start.
4
+ *
5
+ * Why not just hand out a path: the memory store lives in `<dshHome>/evolve/`,
6
+ * outside the workspace sandbox, so the model's read tool cannot open it.
7
+ * The section therefore carries the entries themselves — full body while the
8
+ * character budget allows, then one-line index rows, then a `evolve_recall`
9
+ * pointer for the rest. Budget is hard: a bounded prompt beats an unbounded one.
10
+ *
11
+ * Session-freeze contract (the prompt-cache guard): the section is computed
12
+ * once per session and reused byte-for-byte afterwards, so the system prompt
13
+ * stays stable within a session and cache reads keep hitting. A memory written
14
+ * mid-session becomes visible in the NEXT session; `evolve_recall` covers the
15
+ * immediate need. See `.agents/notes/proposed/feature/2026-10-01-evolve-v2-passive-read-write.md`.
16
+ */
17
+ import type { HarnessEntry } from "./types.js";
18
+ import { type MemoryRecallType } from "./types.js";
19
+ import type { EvolutionEngine } from "./service.js";
20
+ import { type AgentLike } from "./inject.js";
21
+ /** Hard character budget for the `<memories>` block. */
22
+ export declare const DEFAULT_MEMORY_INDEX_MAX_CHARS = 6000;
23
+ /** Prompt-section order of the memory index (empty 1–499 slot, see ADR). */
24
+ export declare const DEFAULT_MEMORY_SECTION_ORDER = 400;
25
+ /** Sessions whose frozen section text is kept before LRU eviction. */
26
+ export declare const DEFAULT_FROZEN_SESSIONS = 32;
27
+ /** Name of the injected section (unique — duplicate names throw upstream). */
28
+ export declare const MEMORY_SECTION_NAME = "evolve:memory-index";
29
+ /**
30
+ * Recall-type priority for the budget: durable, behavior-shaping types first.
31
+ * `project` (current work context) > `feedback` (how to behave) > `user` (who
32
+ * the user is) > `reference` (external pointers). Entries without a valid
33
+ * memoryType sink below every typed entry.
34
+ */
35
+ export declare const MEMORY_TYPE_PRIORITY: Record<MemoryRecallType, number>;
36
+ /** The entry's recall type, or undefined when the metadata key is absent/invalid. */
37
+ export declare function memoryTypeOf(entry: HarnessEntry): MemoryRecallType | undefined;
38
+ /**
39
+ * Order memories for injection: recall-type priority first
40
+ * ({@link MEMORY_TYPE_PRIORITY}), then recency (newest first), then the stable
41
+ * dictionary order — deterministic for equal inputs, so a frozen section is
42
+ * reproducible across builds. The input is never mutated.
43
+ */
44
+ export declare function rankMemories(entries: readonly HarnessEntry[], now?: number): HarnessEntry[];
45
+ /** One memory rendered in full, as a list item with the body indented. */
46
+ export declare function memoryFullLine(entry: HarnessEntry): string;
47
+ /** What a budget-bounded memory block shows. */
48
+ export interface MemorySelection {
49
+ /** Entries rendered with their full body, in display order. */
50
+ full: HarnessEntry[];
51
+ /** Entries degraded to a one-line index row (`id` + title hook). */
52
+ indexed: HarnessEntry[];
53
+ /** Entries that fit neither and are only counted. */
54
+ dropped: number;
55
+ /** Entries actually shown (full + indexed), in display order. */
56
+ shown: HarnessEntry[];
57
+ }
58
+ /**
59
+ * Greedy budget fill over {@link rankMemories}: each entry takes the full body
60
+ * while it fits, otherwise degrades to an index row, otherwise is dropped —
61
+ * and because the ranking is fixed, the first entry that fits neither means
62
+ * every later one is dropped too (their rows are no smaller).
63
+ */
64
+ export declare function selectMemoryInjection(entries: readonly HarnessEntry[], maxChars?: number): MemorySelection;
65
+ /** The `evolve_recall` pointer appended when rows had to be degraded or dropped. */
66
+ export declare const MEMORY_OVERFLOW_HINT = "- \u66F4\u591A\u8BB0\u5FC6\u53EF\u7528 evolve_recall \u6309\u9700\u8BFB\u53D6";
67
+ /**
68
+ * Render a selection as the `<memories>` block, or "" when nothing is shown.
69
+ * Full bodies come first in display order, then the degraded index rows, then
70
+ * the `evolve_recall` pointer with the degradation/drop counts.
71
+ */
72
+ export declare function renderMemorySelection(selection: MemorySelection): string;
73
+ /** {@link renderMemorySelection} over a fresh selection (convenience). */
74
+ export declare function formatMemoriesBlock(entries: readonly HarnessEntry[], maxChars?: number): string;
75
+ /** Options for {@link memoryIndexSectionText}. */
76
+ export interface MemoryIndexOptions {
77
+ /** Hard budget for the memories block (default 6000). */
78
+ maxChars?: number;
79
+ /** Include the when_to_save guide (default true). */
80
+ guide?: boolean;
81
+ /** Pin the project layer explicitly (tests/tools); otherwise derived from cwd. */
82
+ projectKey?: string;
83
+ }
84
+ /**
85
+ * Compose the full `evolve:memory-index` section for one assembling agent:
86
+ * the persistent-memory guide (when to save / how to maintain) wrapped around
87
+ * the budget-bounded memories block. With the guide off and no memories the
88
+ * result is "" — the prompt renderer then drops the section, so an empty store
89
+ * keeps costing zero tokens.
90
+ *
91
+ * Injection usage is recorded for everything shown (best-effort; a failure
92
+ * never blocks injection), deduped per session by {@link recordInjection}.
93
+ */
94
+ export declare function memoryIndexSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, opts?: MemoryIndexOptions): string;
95
+ /** A per-session frozen view of the memory section text. */
96
+ export interface FrozenMemorySection {
97
+ /**
98
+ * The section text for this agent: built on first use per `agent.id` and
99
+ * reused byte-for-byte afterwards. `undefined` agent → "" (unbuilt).
100
+ */
101
+ textFor(agent: AgentLike | undefined, build: (agent: AgentLike) => string): string;
102
+ /** Sessions currently frozen (test/observability hook). */
103
+ size(): number;
104
+ /** Drop every frozen session (tests; a restart rebuilds naturally). */
105
+ clear(): void;
106
+ }
107
+ /**
108
+ * Freeze the section text per session id. The frozen value is what keeps the
109
+ * system prompt stable across a session's assemblies (prompt-cache guard);
110
+ * bounded by `maxSessions` with insertion-order (LRU) eviction, so a long-lived
111
+ * process cannot grow without limit.
112
+ */
113
+ export declare function createFrozenMemorySection(maxSessions?: number): FrozenMemorySection;
114
+ //# sourceMappingURL=memory-index.d.ts.map
@@ -0,0 +1,197 @@
1
+ import { MEMORY_TYPE_KEY, isArchived, isMemoryType } from "./types.js";
2
+ import { recordInjection } from "./usage.js";
3
+ import { MEMORY_GUIDE_INTRO, MEMORY_GUIDE_RULES } from "./memory-guide.js";
4
+ import { directoryLine, mergedInjectionState, recencyScore } from "./inject.js";
5
+ /** Hard character budget for the `<memories>` block. */
6
+ export const DEFAULT_MEMORY_INDEX_MAX_CHARS = 6000;
7
+ /** Prompt-section order of the memory index (empty 1–499 slot, see ADR). */
8
+ export const DEFAULT_MEMORY_SECTION_ORDER = 400;
9
+ /** Sessions whose frozen section text is kept before LRU eviction. */
10
+ export const DEFAULT_FROZEN_SESSIONS = 32;
11
+ /** Name of the injected section (unique — duplicate names throw upstream). */
12
+ export const MEMORY_SECTION_NAME = "evolve:memory-index";
13
+ /**
14
+ * Recall-type priority for the budget: durable, behavior-shaping types first.
15
+ * `project` (current work context) > `feedback` (how to behave) > `user` (who
16
+ * the user is) > `reference` (external pointers). Entries without a valid
17
+ * memoryType sink below every typed entry.
18
+ */
19
+ export const MEMORY_TYPE_PRIORITY = {
20
+ project: 0,
21
+ feedback: 1,
22
+ user: 2,
23
+ reference: 3,
24
+ };
25
+ /** Priority of an entry whose metadata carries no valid recall type. */
26
+ const UNTYPED_PRIORITY = 4;
27
+ /** The entry's recall type, or undefined when the metadata key is absent/invalid. */
28
+ export function memoryTypeOf(entry) {
29
+ const value = entry.metadata[MEMORY_TYPE_KEY];
30
+ return isMemoryType(value) ? value : undefined;
31
+ }
32
+ /** Deterministic final tiebreak (matches `inject.ts` ranking). */
33
+ function stableCompare(a, b) {
34
+ return [a.path, a.title, a.id].join("\0").localeCompare([b.path, b.title, b.id].join("\0"));
35
+ }
36
+ /**
37
+ * Order memories for injection: recall-type priority first
38
+ * ({@link MEMORY_TYPE_PRIORITY}), then recency (newest first), then the stable
39
+ * dictionary order — deterministic for equal inputs, so a frozen section is
40
+ * reproducible across builds. The input is never mutated.
41
+ */
42
+ export function rankMemories(entries, now = Date.now()) {
43
+ return [...entries].sort((a, b) => {
44
+ const aType = memoryTypeOf(a);
45
+ const bType = memoryTypeOf(b);
46
+ const priorityDelta = (aType ? MEMORY_TYPE_PRIORITY[aType] : UNTYPED_PRIORITY) - (bType ? MEMORY_TYPE_PRIORITY[bType] : UNTYPED_PRIORITY);
47
+ if (priorityDelta !== 0) {
48
+ return priorityDelta;
49
+ }
50
+ const recencyDelta = recencyScore(b, now) - recencyScore(a, now);
51
+ if (recencyDelta !== 0) {
52
+ return recencyDelta;
53
+ }
54
+ return stableCompare(a, b);
55
+ });
56
+ }
57
+ /** One memory rendered in full, as a list item with the body indented. */
58
+ export function memoryFullLine(entry) {
59
+ const type = memoryTypeOf(entry);
60
+ const body = entry.content.trim().replace(/\n/g, "\n ");
61
+ const head = `- [memory${type ? `:${type}` : ""}:${entry.id}] ${entry.title}`;
62
+ return body.length > 0 ? `${head}\n ${body}` : head;
63
+ }
64
+ /**
65
+ * Greedy budget fill over {@link rankMemories}: each entry takes the full body
66
+ * while it fits, otherwise degrades to an index row, otherwise is dropped —
67
+ * and because the ranking is fixed, the first entry that fits neither means
68
+ * every later one is dropped too (their rows are no smaller).
69
+ */
70
+ export function selectMemoryInjection(entries, maxChars = DEFAULT_MEMORY_INDEX_MAX_CHARS) {
71
+ const visible = entries.filter((entry) => !isArchived(entry));
72
+ const ranked = rankMemories(visible);
73
+ const budget = Math.max(0, maxChars);
74
+ const full = [];
75
+ const indexed = [];
76
+ const shown = [];
77
+ let used = 0;
78
+ let dropped = 0;
79
+ for (const entry of ranked) {
80
+ const fullLine = memoryFullLine(entry);
81
+ if (used + fullLine.length <= budget) {
82
+ full.push(entry);
83
+ shown.push(entry);
84
+ used += fullLine.length;
85
+ continue;
86
+ }
87
+ const indexLine = directoryLine(entry);
88
+ if (used + indexLine.length <= budget) {
89
+ indexed.push(entry);
90
+ shown.push(entry);
91
+ used += indexLine.length;
92
+ continue;
93
+ }
94
+ dropped = ranked.length - shown.length;
95
+ break;
96
+ }
97
+ return { full, indexed, dropped, shown };
98
+ }
99
+ /** The `evolve_recall` pointer appended when rows had to be degraded or dropped. */
100
+ export const MEMORY_OVERFLOW_HINT = "- 更多记忆可用 evolve_recall 按需读取";
101
+ /**
102
+ * Render a selection as the `<memories>` block, or "" when nothing is shown.
103
+ * Full bodies come first in display order, then the degraded index rows, then
104
+ * the `evolve_recall` pointer with the degradation/drop counts.
105
+ */
106
+ export function renderMemorySelection(selection) {
107
+ const { full, indexed, dropped, shown } = selection;
108
+ if (shown.length === 0) {
109
+ return "";
110
+ }
111
+ const lines = [];
112
+ for (const entry of full) {
113
+ lines.push(memoryFullLine(entry));
114
+ }
115
+ for (const entry of indexed) {
116
+ lines.push(directoryLine(entry));
117
+ }
118
+ const omitted = [];
119
+ if (indexed.length > 0) {
120
+ omitted.push(`${indexed.length} 条仅列索引`);
121
+ }
122
+ if (dropped > 0) {
123
+ omitted.push(`${dropped} 条未列出`);
124
+ }
125
+ lines.push(omitted.length > 0 ? `${MEMORY_OVERFLOW_HINT}(${omitted.join(",")})` : MEMORY_OVERFLOW_HINT);
126
+ return `<memories>\n${lines.join("\n")}\n</memories>`;
127
+ }
128
+ /** {@link renderMemorySelection} over a fresh selection (convenience). */
129
+ export function formatMemoriesBlock(entries, maxChars = DEFAULT_MEMORY_INDEX_MAX_CHARS) {
130
+ return renderMemorySelection(selectMemoryInjection(entries, maxChars));
131
+ }
132
+ /**
133
+ * Compose the full `evolve:memory-index` section for one assembling agent:
134
+ * the persistent-memory guide (when to save / how to maintain) wrapped around
135
+ * the budget-bounded memories block. With the guide off and no memories the
136
+ * result is "" — the prompt renderer then drops the section, so an empty store
137
+ * keeps costing zero tokens.
138
+ *
139
+ * Injection usage is recorded for everything shown (best-effort; a failure
140
+ * never blocks injection), deduped per session by {@link recordInjection}.
141
+ */
142
+ export function memoryIndexSectionText(engine, agent, opts) {
143
+ if (!agent) {
144
+ return "";
145
+ }
146
+ const guide = opts?.guide ?? true;
147
+ const maxChars = opts?.maxChars ?? DEFAULT_MEMORY_INDEX_MAX_CHARS;
148
+ const merged = mergedInjectionState(engine, agent, opts);
149
+ const selection = selectMemoryInjection(Object.values(merged.entries.memory), maxChars);
150
+ const block = renderMemorySelection(selection);
151
+ if (selection.shown.length > 0) {
152
+ try {
153
+ recordInjection(engine.baseDir, selection.shown.map((entry) => `${entry.kind}:${entry.id}`), agent.id);
154
+ }
155
+ catch {
156
+ // Usage recording is diagnostic; never interrupt the injection path.
157
+ }
158
+ }
159
+ if (!guide) {
160
+ return block;
161
+ }
162
+ return [MEMORY_GUIDE_INTRO, block.length > 0 ? block : "<memories>\n(暂无记忆)\n</memories>", MEMORY_GUIDE_RULES].join("\n\n");
163
+ }
164
+ /**
165
+ * Freeze the section text per session id. The frozen value is what keeps the
166
+ * system prompt stable across a session's assemblies (prompt-cache guard);
167
+ * bounded by `maxSessions` with insertion-order (LRU) eviction, so a long-lived
168
+ * process cannot grow without limit.
169
+ */
170
+ export function createFrozenMemorySection(maxSessions = DEFAULT_FROZEN_SESSIONS) {
171
+ const limit = Math.max(1, maxSessions);
172
+ const frozen = new Map();
173
+ return {
174
+ textFor(agent, build) {
175
+ if (!agent) {
176
+ return "";
177
+ }
178
+ const cached = frozen.get(agent.id);
179
+ if (cached !== undefined) {
180
+ return cached;
181
+ }
182
+ const text = build(agent);
183
+ frozen.set(agent.id, text);
184
+ while (frozen.size > limit) {
185
+ const oldest = frozen.keys().next();
186
+ if (oldest.done === true) {
187
+ break;
188
+ }
189
+ frozen.delete(oldest.value);
190
+ }
191
+ return text;
192
+ },
193
+ size: () => frozen.size,
194
+ clear: () => frozen.clear(),
195
+ };
196
+ }
197
+ //# sourceMappingURL=memory-index.js.map
package/lib/tool.js CHANGED
@@ -83,12 +83,12 @@ function textResult(text) {
83
83
  export function registerEvolveTools(ctx, engine, opts) {
84
84
  ctx.tools.register(defineTool({
85
85
  name: "evolve_list",
86
- description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local), the current project (project), or across projects (global).",
86
+ description: "List the harness state (prompt notes, memories, skills, subagent specs) for one store.",
87
87
  parameters: {
88
88
  scope: {
89
89
  type: "string",
90
90
  enum: SCOPES,
91
- description: "Which store to list: 'local' (default), 'project', or 'global'.",
91
+ description: "'local' (default), 'project', or 'global'.",
92
92
  },
93
93
  },
94
94
  output: {
@@ -118,25 +118,25 @@ export function registerEvolveTools(ctx, engine, opts) {
118
118
  }));
119
119
  ctx.tools.register(defineTool({
120
120
  name: "evolve_recall",
121
- description: "Targeted recall over the harness stores: filter memories (or other kinds) by query, kind, scope, and memory type, and read back full content with version, source, and staleness signals. Read-only — it never mutates a store. Prefer this over evolve_list when looking for something specific.",
121
+ description: "Read-only targeted recall across the harness stores: filter by query, kind, scope, and memory type; returns full content with version, source, and staleness signals. Prefer this over evolve_list when looking for something specific.",
122
122
  parameters: {
123
- query: { type: "string", description: "Free-text relevance query. Empty means most-recently-updated first." },
123
+ query: { type: "string", description: "Free-text relevance query. Empty → most-recently-updated first." },
124
124
  kinds: {
125
125
  type: "array",
126
126
  items: { type: "string" },
127
- description: "Entry kinds to search (default memory only).",
127
+ description: "Entry kinds (default memory only).",
128
128
  },
129
129
  scopes: {
130
130
  type: "array",
131
131
  items: { type: "string" },
132
- description: "Stores to search (default local, project, and global).",
132
+ description: "Stores to search (default all three).",
133
133
  },
134
134
  memoryTypes: {
135
135
  type: "array",
136
136
  items: { type: "string" },
137
- description: "Memory-type filter: user, feedback, project, reference (memory entries only).",
137
+ description: "Memory types: user, feedback, project, reference.",
138
138
  },
139
- limit: { type: "number", description: "Maximum hits (default 10, at most 50)." },
139
+ limit: { type: "number", description: "Max hits (default 10, max 50)." },
140
140
  includeArchived: { type: "boolean", description: "Include archived entries (default false)." },
141
141
  },
142
142
  output: {
@@ -157,18 +157,18 @@ export function registerEvolveTools(ctx, engine, opts) {
157
157
  }));
158
158
  ctx.tools.register(defineTool({
159
159
  name: "evolve_add",
160
- description: "Create one harness entry (prompt/memory/skill/subagent). Executable skills require reference {type:python, import, callable} and an arguments contract; guidance skills (skill_kind=guidance) are SKILL.md documents — recurring multi-step workflows — and must NOT carry a reference. Snapshot, version, and history are handled automatically.",
160
+ description: "Create one harness entry (prompt/memory/skill/subagent). Snapshots, versioning, and audit are automatic.",
161
161
  parameters: {
162
162
  kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true, description: "Entry kind." },
163
163
  title: { type: "string", required: true, description: "Stable title." },
164
164
  content: { type: "string", required: true, description: "Entry body." },
165
165
  path: { type: "string", description: "Optional grouping path." },
166
- skill_kind: { type: "string", enum: ["executable", "guidance"], description: "For skills: executable (python reference, default) or guidance (SKILL.md document, no reference)." },
167
- reference: { type: "object", additionalProperties: true, description: "For executable skills: {type:'python', import, callable}." },
168
- arguments: { type: "object", additionalProperties: true, description: "For executable skills: accepted input contract." },
169
- memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory: one fact per entry (feedback/project must carry Why + How to apply)." },
170
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
171
- global: { type: "boolean", description: "Set true to write the cross-session store (requires human approval; only for durable, reusable lessons)." },
166
+ skill_kind: { type: "string", enum: ["executable", "guidance"], description: "Skills: executable (default, needs reference) or guidance (SKILL.md, no reference)." },
167
+ reference: { type: "object", additionalProperties: true, description: "Executable skills: {type:'python', import, callable}." },
168
+ arguments: { type: "object", additionalProperties: true, description: "Executable skills: accepted input contract." },
169
+ memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory (feedback/project need Why + How to apply)." },
170
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
171
+ global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
172
172
  },
173
173
  output: {
174
174
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
@@ -216,9 +216,9 @@ export function registerEvolveTools(ctx, engine, opts) {
216
216
  id: { type: "string", required: true, description: "Existing entry id." },
217
217
  title: { type: "string" },
218
218
  content: { type: "string" },
219
- memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "For memory: (re)classify the entry's recall type." },
220
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
221
- global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
219
+ memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Memory: (re)classify the recall type." },
220
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
221
+ global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
222
222
  },
223
223
  output: {
224
224
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
@@ -242,17 +242,17 @@ export function registerEvolveTools(ctx, engine, opts) {
242
242
  }));
243
243
  ctx.tools.register(defineTool({
244
244
  name: "evolve_delete",
245
- description: "Delete harness entries by id. Pass `id` for one entry or `ids` for several — a batch lands as one refinement behind one approval. Literal ids win: entries whose stored id itself carries a scope-like prefix (e.g. a legacy `local:…` id) are addressed verbatim.",
245
+ description: "Delete harness entries: `id` for one, `ids` for a batch (one refinement, one approval). Ids are addressed verbatim, scope-prefixed or not.",
246
246
  parameters: {
247
247
  kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
248
- id: { type: "string", description: "Existing entry id (single delete; combine with ids or use ids for a batch)." },
248
+ id: { type: "string", description: "One entry id (or use ids for a batch)." },
249
249
  ids: {
250
250
  type: "array",
251
251
  items: { type: "string" },
252
- description: "Existing entry ids for one batch delete (one refinement, one approval).",
252
+ description: "Entry ids for one batch delete.",
253
253
  },
254
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
255
- global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
254
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
255
+ global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
256
256
  },
257
257
  output: {
258
258
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
@@ -271,11 +271,11 @@ export function registerEvolveTools(ctx, engine, opts) {
271
271
  }));
272
272
  ctx.tools.register(defineTool({
273
273
  name: "evolve_rollback",
274
- description: "Deterministically revert a previous refinement by its id (from evolve_list history or the /evolve command).",
274
+ description: "Deterministically revert a previous refinement by its id (from evolve_list history or /evolve).",
275
275
  parameters: {
276
- refinementId: { type: "string", required: true, description: "The refinement id to roll back." },
277
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
278
- global: { type: "boolean", description: "Set true to roll back a cross-session refinement." },
276
+ refinementId: { type: "string", required: true, description: "Refinement id to roll back." },
277
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
278
+ global: { type: "boolean", description: "Shorthand for scope='global'." },
279
279
  },
280
280
  output: {
281
281
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -56,6 +56,7 @@
56
56
  "lint": "oxlint src test",
57
57
  "clean": "rm -rf lib",
58
58
  "check:docs": "tsx scripts/run-gates.ts docs",
59
+ "check:handoff": "tsx scripts/verify-handoff-structure.ts --self-test && tsx scripts/verify-handoff-structure.ts",
59
60
  "check:governance": "tsx scripts/verify-governance.ts"
60
61
  },
61
62
  "peerDependencies": {