dsh-continual-evolve 0.14.2 → 0.15.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 CHANGED
@@ -7,25 +7,28 @@ English | [中文](README.zh.md)
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-92%20passing-brightgreen)]()
11
- [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
- [![Coverage · branches](https://img.shields.io/badge/coverage_branches-93%25-green)]()
10
+ [![Tests](https://img.shields.io/badge/tests-173%20passing-brightgreen)]()
11
+ [![Coverage · statements](https://img.shields.io/badge/coverage_statements-97%25-brightgreen)]()
12
+ [![Coverage · branches](https://img.shields.io/badge/coverage_branches-91%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
14
14
 
15
- A workspace-memory plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`): one plain-markdown memory store per workspace (`<workspace>/.evolve/memory/`), the index injected at session start, used by the model with its **native file tools** — and nothing else.
15
+ A workspace-memory plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`): every memory lives in one central SQLite database (`~/.dsh/evolve/memory.db`, rows partitioned by workspace), the index is injected at session start, and the plugin's code is the literal **sole writer** — the model proposes, code gates and lands.
16
16
 
17
17
  ## Why
18
18
 
19
- Reusable experience gathered in one session (user preferences, pitfalls, project context) is forgotten by the next. ZCode solves this with a folder: one fact per markdown file plus a `MEMORY.md` index, injected at session start, read and written natively by the model, with no dedicated machinery. This plugin brings that exact shape to DSH.
19
+ Reusable experience gathered in one session (user preferences, pitfalls, project context) is forgotten by the next. A folder of markdown files makes memory visible but leaves every write ungoverned: index drift, frontmatter rot, oversized files, and secrets are only visible *after* a bad write lands. This plugin keeps ZCode's memory shape (per-workspace partitioning, injected index, typed memories) but moves the store into one database whose every mutation passes mechanical gates.
20
20
 
21
21
  ## How it works
22
22
 
23
- 1. **Session-start injection** — the plugin registers a single system-prompt section: the `MEMORY.md` index content (whole-line truncated over budget, with a read-the-directory hint), the absolute store path, and the when_to_save guide. The section is computed once per session and reused byte-for-byte, keeping the system prompt stable and the prompt cache warm; an empty store costs zero tokens.
24
- 2. **Native read/write** — DSH fully permits file access inside the workspace (reads are never fenced; the write fence only covers paths outside the workspace, see [`docs/FAQ.md`](docs/FAQ.md) #6). The model reads, writes, and edits memory files and maintains the index directly; the store is bootstrapped on first use.
25
- 3. **Governance is the file** — no versions, no snapshots, no approvals, no background extraction: a bad memory is a visible file in the workspace, and deleting it is the retirement path. Memories are personal context — on bootstrap in a git workspace the store appends `.evolve/` to the workspace `.gitignore` (non-git workspaces are left untouched).
26
- 4. **Read-only card** — the plugin mounts a read-only memory card on its own page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card (collapsed by default, header toggle with a rotating chevron, hover/open frame states) and aligned with ZCode's Settings → Memory viewer: one workspace at a time behind a dropdown scope selector fed by the host workspace registry, a file search box, per-file relative updated times, click-to-preview file bodies (5 MiB cap), plus index-drift warnings (unindexed files, index rows whose file is gone). The plugin's display name and description localize through the host's package `locale/*.json` metadata channel. The card follows the market's look — host theme tokens, the `Button` primitive for refresh, and zh/en copy that tracks the DSH language (plain themed elements on old hosts). The card is a request-time projection of the files on disk — the single source of truth remains `.evolve/memory/`; the card has no edit path, edit the files directly.
23
+ 1. **Session-start injection** — the plugin registers a single system-prompt section: the workspace's memory index as a store query (hooks only, whole-line truncated over budget, ordered feedback > user > reference), the central-library path, and a three-line guide. The section is computed once per session and reused byte-for-byte, keeping the system prompt stable and the prompt cache warm; an empty store costs zero tokens.
24
+ 2. **Two tools, one writer** — `memory_write` (create/update/delete) is the explicit path for "remember …" / "forget …": every proposal passes the code gates (id shape, type enum, the feedback Why/How contract, secret screening carried over from v1, 64KB body cap) inside one transaction. `memory_read` fetches bodies by id and searches by keyword — whitespace-separated terms are AND-combined, with literal substring matching for short terms and whenever the trigram index misses; the injected index carries hooks only. On hosts without the tools service the plugin degrades to "tell the user" phrasing and stays functional.
25
+ 3. **Proposal-based extraction** — the only background automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered.
26
+ 4. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted). The read-only card surfaces quarantine anomalies.
27
+ 5. **Read-only card** — mounted on the plugin's page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card and aligned with ZCode's Settings → Memory viewer: a dropdown scope selector over the store's workspace partitions, a search box, per-entry relative updated times, click-to-preview bodies (5 MiB cap), and patrol warnings. Localized through the host's `locale/*.json` metadata channel; host theme tokens, the `Button` primitive, and zh/en copy that tracks the DSH language. The card is a request-time projection of the database and has no edit path.
27
28
 
28
- Memory file format (same as ZCode): frontmatter with `name` / `description` (the hook that decides whether a future session recalls it) / `metadata.type` (`user | feedback | project | reference`); `feedback` bodies must carry **Why:** and **How to apply:** lines.
29
+ Memory types follow the ZCode taxonomy: `user` (profile and environment facts), `feedback` (corrected or confirmed practices — bodies must carry **Why:** and **How to apply:** lines), `reference` (resource pointers). The extraction boundary is person- and environment-scoped knowledge only: decisions and trade-offs are repo-scoped by nature and belong to the repo's ADR route, never to memory.
30
+
31
+ Legacy markdown stores (`<workspace>/.evolve/memory/*.md`) are imported losslessly on first contact and the directory is renamed aside as `memory-imported-<date>/` — never deleted.
29
32
 
30
33
  ## Install
31
34
 
@@ -41,16 +44,17 @@ After installing or updating, restart the DSH profile you actually use (`dsh web
41
44
 
42
45
  ## Usage
43
46
 
44
- No commands, no tools. On the first session after restart, `.evolve/memory/` is created and the index is injected automatically. Just tell the model "remember …" / "forget …" — the memory files plus their index lines are the entire persistent state. Memories written mid-session become visible in the next session (the section is frozen to protect the prompt cache); for an immediate look the model simply reads the directory. To see the whole store, open the plugin's page in the official plugin manager: the read-only card shows each workspace's entries and index drift.
47
+ No commands. On the first session after restart the index is injected automatically. Just tell the model "remember …" / "forget …" — it lands through `memory_write` and the gates. Memories written mid-session become visible in the next session (the section is frozen to protect the prompt cache). To see the whole store, open the plugin's page in the official plugin manager: the read-only card shows each workspace's entries, search, and patrol anomalies. The store requires Node ≥ 22.5 (`node:sqlite`, part of the desktop host's EnsureNode runtime); without it the plugin degrades to a no-op with a console warning.
45
48
 
46
49
  ## Configuration
47
50
 
48
51
  | Key | Default | Meaning |
49
52
  |---|---|---|
50
53
  | `memoryIndex.enabled` | `true` | Register the memory section |
51
- | `memoryIndex.guide` | `true` | Inject the when_to_save guide (zero tokens when off and the index is empty) |
54
+ | `memoryIndex.guide` | `true` | Inject the guide (zero tokens when off and the index is empty) |
52
55
  | `memoryIndex.order` | `400` | Section order (upstream named slots start at `PLAN_POLICY=500`) |
53
56
  | `memoryIndex.maxChars` | `6000` | Hard character budget for the injected index |
57
+ | `memoryIndex.extraction` | `true` | The proposal-based extraction run (turn-level, per-session slots, no idle timer) |
54
58
  | `memoryCard.enabled` | `true` | Read-only memory card in the official plugin manager (degrades to no card when the host lacks `webServer`/`plugins.bundle.config`) |
55
59
 
56
60
  Profile patch example:
@@ -60,32 +64,40 @@ Profile patch example:
60
64
  config:
61
65
  memoryIndex:
62
66
  maxChars: 8000
67
+ extraction: true
63
68
  ```
64
69
 
65
70
  ## Development
66
71
 
67
72
  ```bash
68
- pnpm install && pnpm build # deps + tsc -> lib/
69
- pnpm test # vitest (92 tests)
73
+ pnpm install && pnpm build # deps + clean build -> lib/
74
+ pnpm test # vitest (173 tests)
70
75
  pnpm test:coverage # v8 coverage, CI-enforced thresholds
71
76
  pnpm lint # oxlint src test client
77
+ pnpm check:pack # published-artifact consistency (also runs on prepack)
72
78
  ```
73
79
 
74
80
  Layout:
75
81
 
76
82
  ```
77
83
  ├── src/
78
- │ ├── index.ts # registration: the one section + config + card route wiring
79
- │ ├── memory-section.ts # injection: read index / bootstrap / truncate / session freeze
80
- │ ├── memory-guide.ts # guide: when_to_save and upkeep (adapted from ZCode)
81
- │ ├── memory-snapshot.ts # card: read-only store projection (entries / drift / error degrade)
82
- │ ├── workspace-catalog.ts# card: workspace list from the host registry (the API read fence)
83
- │ ├── known-workspaces.ts# card: in-process LRU fallback source for the catalogue
84
- │ └── card-routes.ts # card: GET-only read API on the host webServer
84
+ │ ├── index.ts # registration: section + tools + extraction + card wiring
85
+ │ ├── store.ts # central SQLite store: DDL/WAL/FTS, reads, ledger, patrol, cursors
86
+ │ ├── store-apply.ts # the sole write gate: validate all, apply atomically, ledger
87
+ │ ├── memory-rules.ts # mechanical gates: enums, sizes, id shape, secret screening
88
+ │ ├── memory-section.ts # injection: store query / truncate / session freeze
89
+ │ ├── memory-guide.ts # the main-session guide (three lines)
90
+ │ ├── memory-tools.ts # memory_write + memory_read (host-shaped definitions)
91
+ │ ├── extraction.ts # runner + per-session scheduler (turn-level, no idle timer)
92
+ │ ├── extraction-surface.ts # trajectory slicing: cursor, eligibility, serialization
93
+ │ ├── extraction-prompt.ts # the extraction prompt + answer parsing
94
+ │ ├── import-md.ts # lossless legacy markdown migration
95
+ │ ├── memory-snapshot.ts # card: read-only store projection (entries / patrol / degrade)
96
+ │ └── card-routes.ts # card: GET-only read API on the host webServer
85
97
  ├── client/
86
98
  │ └── client.js # hand-written client bundle: read-only card in the plugin manager
87
99
  ├── locale/ # host package metadata: localized display name + description (zh/en)
88
- ├── test/ # vitest suite (8 files)
100
+ ├── test/ # vitest suite (15 files)
89
101
  ├── lib/ # build output (tsc)
90
102
  ├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
91
103
  └── .agents/ # AI collaboration layer (AGENTS.md, skills, ADR notes)
@@ -93,7 +105,7 @@ Layout:
93
105
 
94
106
  ## Docs
95
107
 
96
- - Design: [`docs/design.md`](docs/design.md) · Pitfalls: [`docs/FAQ.md`](docs/FAQ.md) · Teardown decision: [`.agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md`](.agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md)
108
+ - Design: [`docs/design.md`](docs/design.md) · Pitfalls: [`docs/FAQ.md`](docs/FAQ.md) · Store decision: [`.agents/notes/implemented/architecture/2026-10-06-sqlite-single-store.md`](.agents/notes/implemented/architecture/2026-10-06-sqlite-single-store.md)
97
109
 
98
110
  ## License
99
111
 
package/README.zh.md CHANGED
@@ -7,25 +7,28 @@
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-92%20passing-brightgreen)]()
11
- [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
- [![Coverage · branches](https://img.shields.io/badge/coverage_branches-93%25-green)]()
10
+ [![Tests](https://img.shields.io/badge/tests-173%20passing-brightgreen)]()
11
+ [![Coverage · statements](https://img.shields.io/badge/coverage_statements-97%25-brightgreen)]()
12
+ [![Coverage · branches](https://img.shields.io/badge/coverage_branches-91%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
14
14
 
15
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的工作区记忆插件:每个工作区一个纯 markdown 记忆库(`<workspace>/.evolve/memory/`),会话开局注入索引,模型用**原生文件读写工具**直接使用——仅此而已。
15
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的工作区记忆插件:全部记忆住在一个中央 SQLite 库里(`~/.dsh/evolve/memory.db`,按工作区路径行分区),会话开局注入索引,插件的代码是字面意义的**唯一写者**——模型提议,代码门禁后落盘。
16
16
 
17
17
  ## 为什么
18
18
 
19
- Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项目背景)下个会话就忘。ZCode 用一个文件夹解决了这件事:一事一文的 md 记忆 + `MEMORY.md` 索引、开局注入、模型原生读写、没有任何专用机器。本插件把同一形态带给 DSH。
19
+ Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项目背景)下个会话就忘。文件夹式的 md 记忆让记忆可见,但每一条写入都不设防:索引漂移、frontmatter 腐坏、超尺寸膨胀、密钥泄漏——坏写入落地之后才可见。本插件保留 ZCode 记忆形态(按工作区分区、开局注入索引、类型化记忆),把存储搬进一个所有变更都过机械门禁的数据库。
20
20
 
21
21
  ## 工作原理
22
22
 
23
- 1. **开局注入** —— 插件只注册一个 system-prompt section:注入 `MEMORY.md` 索引正文(超预算按整行截断并提示读目录)、记忆目录绝对路径、when_to_save 指南。该 section 每会话只算一次并逐字节复用,system prompt 稳定、prompt cache 持续命中;空 store 零 token。
24
- 2. **原生读写** —— DSH 对工作区内文件读写全放行(读路径从不设防,写围栏只围工作区外,见 [`docs/FAQ.md`](docs/FAQ.md) #6)。模型直接 Read/Write/Edit 记忆文件并维护索引;store 不存在时自动 bootstrap。
25
- 3. **治理即文件** —— 无版本、无快照、无审批、无后台提取:一条坏记忆就是工作区里一个可见的文件,删掉它就是退役。记忆是个人上下文——bootstrap 时若工作区是 git 仓库会自动把 `.evolve/` 追加进 `.gitignore`(非 git 工作区不碰任何文件)。
26
- 4. **只读卡片** —— 插件在官方插件管理的本 bundle 页面挂一张只读记忆卡片(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡(默认收起、头部整行可点 + 旋转 chevron、hover/展开框态),内容对齐 ZCode 设置的记忆选项卡——工作区一次选一个(下拉作用域选择器,目录取宿主工作区注册表)、文件搜索框、每个文件的相对更新时间、点击行内展开文件内容预览(5 MiB 上限),外加索引漂移警告(未索引文件与索引失联文件)。插件的显示名与描述走宿主包元数据 `locale/*.json` 通道双语切换。卡片与市场同款——宿主主题 token、`Button` 原语刷新按钮、中英文案跟随 DSH 语言(老宿主降级为同主题裸元素)。卡片是磁盘文件的请求时投影——单一事实源永远是 `.evolve/memory/` 里的文件,卡片没有编辑路径,编辑请直接改文件。
23
+ 1. **开局注入** —— 插件只注册一个 system-prompt section:注入本工作区的记忆索引(查库结果,只含钩子,超预算按整行截断,feedback > user > reference 排序)、中央库路径、三行指南。该 section 每会话只算一次并逐字节复用,system prompt 稳定、prompt cache 持续命中;空 store 零 token。
24
+ 2. **两个工具,一个写者** —— `memory_write`(create/update/delete)承接显式的"记住……"/"忘掉……":每条提案过代码门禁(id 规范、type 枚举、feedback 的 Why/How 契约、v1 移植的 secret 筛查、64KB 正文上限)后在一个事务内落盘。`memory_read` 按 id 取正文、按关键词检索——空格分隔的关键词取 AND,短词与 trigram 未命中时回退字面子串匹配;注入索引只有钩子。宿主没有工具服务时降级为"告诉用户"措辞,注入不受影响。
25
+ 3. **提案制提取** —— 插件唯一的后台自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账。
26
+ 4. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。每次应用后随跑一次巡检:孤儿 FTS 行清除、含密钥条目隔离(隐藏,不物理删除)。只读卡片显形巡检异常。
27
+ 5. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode 设置的记忆选项卡——下拉作用域选择器列出库里的工作区分区、搜索框、每条记忆的相对更新时间、点击行内展开正文预览(5 MiB 上限)、巡检异常警告。显示名与描述走宿主 `locale/*.json` 通道双语;宿主主题 token、`Button` 原语、中英文案跟随 DSH 语言。卡片是数据库的请求时投影,没有编辑路径。
27
28
 
28
- 记忆文件格式(与 ZCode 一致):frontmatter 带 `name` / `description`(决定未来会话会不会想起它)/ `metadata.type`(`user | feedback | project | reference`);`feedback` 正文必带 **Why:** 与 **How to apply:**。
29
+ 记忆类型沿 ZCode 分类法:`user`(画像与环境事实)、`feedback`(被纠正/确认的做法——正文必带 **Why:** 与 **How to apply:**)、`reference`(资源指针)。提取边界只收"跟人与环境走的知识":决策与取舍天然属于仓库,走仓库的 ADR 路线,不进记忆库。
30
+
31
+ 旧版 markdown 记忆库(`<workspace>/.evolve/memory/*.md`)在首次接触时无损导入,原目录改名为 `memory-imported-<date>/` 保留——不删不丢。
29
32
 
30
33
  ## 安装
31
34
 
@@ -41,16 +44,17 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
41
44
 
42
45
  ## 使用
43
46
 
44
- 无需命令、无需工具。重启后第一场会话,`.evolve/memory/` 会自动生成,索引自动注入开场;对模型直接说"记住……"/"忘掉……"即可,写入的记忆文件 + 索引行就是全部持久状态。会话中途写入的记忆下个会话生效(会话内冻结守 prompt cache);需要立即查看时,模型直接读目录。想看记忆库全貌,打开官方插件管理里本插件的页面:只读卡片展示各工作区的记忆条目与索引漂移。
47
+ 无需命令。重启后第一场会话索引自动注入开场;对模型直接说"记住……"/"忘掉……",经 `memory_write` 过门禁落盘。会话中途写入的记忆下个会话生效(会话内冻结守 prompt cache)。想看记忆库全貌,打开官方插件管理里本插件的页面:只读卡片展示各工作区的记忆条目、搜索与巡检异常。中央库需要 Node ≥ 22.5(`node:sqlite`,桌面宿主的 EnsureNode 运行时自带);运行时过旧时插件整体降级为无操作并打控制台告警。
45
48
 
46
49
  ## 配置
47
50
 
48
51
  | 键 | 默认 | 含义 |
49
52
  |---|---|---|
50
53
  | `memoryIndex.enabled` | `true` | 是否注册记忆 section |
51
- | `memoryIndex.guide` | `true` | 是否注入 when_to_save 指南(关闭且索引为空时该 section 零 token) |
54
+ | `memoryIndex.guide` | `true` | 是否注入指南(关闭且索引为空时该 section 零 token) |
52
55
  | `memoryIndex.order` | `400` | section 顺序(上游命名槽位自 `PLAN_POLICY=500` 起) |
53
56
  | `memoryIndex.maxChars` | `6000` | 注入索引的硬字符预算 |
57
+ | `memoryIndex.extraction` | `true` | 提案制提取运行(轮级、按会话分槽、无空闲计时器) |
54
58
  | `memoryCard.enabled` | `true` | 官方插件管理内的只读记忆卡片(宿主缺 `webServer`/`plugins.bundle.config` 时自动降级为无卡片) |
55
59
 
56
60
  profile patch 示例:
@@ -60,32 +64,40 @@ profile patch 示例:
60
64
  config:
61
65
  memoryIndex:
62
66
  maxChars: 8000
67
+ extraction: true
63
68
  ```
64
69
 
65
70
  ## 开发
66
71
 
67
72
  ```bash
68
- pnpm install && pnpm build # 依赖 + tsc -> lib/
69
- pnpm test # vitest(92 例)
73
+ pnpm install && pnpm build # 依赖 + 干净构建 -> lib/
74
+ pnpm test # vitest(173 例)
70
75
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
71
76
  pnpm lint # oxlint src test client
77
+ pnpm check:pack # 发布产物一致性(prepack 也会跑)
72
78
  ```
73
79
 
74
80
  目录结构:
75
81
 
76
82
  ```
77
83
  ├── src/
78
- │ ├── index.ts # 注册:唯一 section + 配置 + 卡片路由接线
79
- │ ├── memory-section.ts # 注入:读索引 / bootstrap / 截断 / 会话冻结
80
- │ ├── memory-guide.ts # 指南:when_to_save 与维护纪律(改编自 ZCode)
81
- │ ├── memory-snapshot.ts # 卡片:记忆库只读投影(条目 / 漂移 / 错误降级)
82
- │ ├── workspace-catalog.ts# 卡片:工作区目录(宿主注册表;API 读围栏)
83
- │ ├── known-workspaces.ts# 卡片:目录的进程内 LRU 回退源
84
- │ └── card-routes.ts # 卡片:webServer 上的 GET-only 只读 API
84
+ │ ├── index.ts # 注册:section + 工具 + 提取 + 卡片接线
85
+ │ ├── store.ts # 中央 SQLite 库:DDL/WAL/FTS、读面、账本、巡检、游标
86
+ │ ├── store-apply.ts # 唯一写门:全量校验、事务原子应用、账本
87
+ │ ├── memory-rules.ts # 机械门禁:枚举、尺寸、id 规范、secret 筛查
88
+ │ ├── memory-section.ts # 注入:查库 / 截断 / 会话冻结
89
+ │ ├── memory-guide.ts # 主会话指南(三行)
90
+ │ ├── memory-tools.ts # memory_write + memory_read(宿主形状定义)
91
+ │ ├── extraction.ts # 运行器 + 按会话分槽的调度器(轮级、无空闲计时器)
92
+ │ ├── extraction-surface.ts # 轨迹切片:游标、资格判定、序列化
93
+ │ ├── extraction-prompt.ts # 提取 prompt + 应答解析
94
+ │ ├── import-md.ts # 旧版 markdown 无损迁移
95
+ │ ├── memory-snapshot.ts # 卡片:记忆库只读投影(条目 / 巡检 / 错误降级)
96
+ │ └── card-routes.ts # 卡片:webServer 上的 GET-only 只读 API
85
97
  ├── client/
86
98
  │ └── client.js # 手写 client bundle:官方插件管理内只读卡片
87
99
  ├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
88
- ├── test/ # vitest 测试(8 个文件)
100
+ ├── test/ # vitest 测试(15 个文件)
89
101
  ├── lib/ # 构建产物(tsc)
90
102
  ├── docs/ # design.md(设计)· FAQ.md(踩坑)
91
103
  └── .agents/ # AI 协作层(AGENTS.md、技能、ADR 笔记)
@@ -93,7 +105,7 @@ pnpm lint # oxlint src test client
93
105
 
94
106
  ## 文档
95
107
 
96
- - 设计:[`docs/design.md`](docs/design.md) · 踩坑:[`docs/FAQ.md`](docs/FAQ.md) · 拆解决策:[`.agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md`](.agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md)
108
+ - 设计:[`docs/design.md`](docs/design.md) · 踩坑:[`docs/FAQ.md`](docs/FAQ.md) · 存储决策:[`.agents/notes/implemented/architecture/2026-10-06-sqlite-single-store.md`](.agents/notes/implemented/architecture/2026-10-06-sqlite-single-store.md)
97
109
 
98
110
  ## License
99
111
 
package/client/client.js CHANGED
@@ -85,25 +85,24 @@ window.__ModuleLoader__.load({
85
85
 
86
86
  var zh = {
87
87
  title: "工作区记忆",
88
- desc: "各工作区 .evolve/memory/ 的只读投影。",
88
+ desc: "中央记忆库(~/.dsh/evolve/memory.db)的只读投影。",
89
89
  refresh: "刷新",
90
- hint: "事实源是各工作区 .evolve/memory/ 下的 markdown 文件——模型原生读写;本卡片只展示,编辑请直接改文件。",
90
+ hint: "事实源是中央记忆库——模型经 memory_write 提案、代码校验后落库(代码是唯一写者);本卡片只展示。",
91
91
  loading: "读取中…",
92
92
  loadFailed: "卡片数据加载失败:",
93
- empty: "还没有任何工作区有记忆库——在某个工作区开一次会话后回来刷新。",
94
- absent: "该工作区尚无记忆库(开一次会话自动创建)。",
93
+ empty: "还没有任何工作区写过记忆——在某个工作区正常使用后回来刷新。",
94
+ absent: "该工作区还没有记忆。",
95
95
  readFailed: "记忆库读取失败:",
96
- stats: "{files} 条记忆 · 索引 {index} 行",
96
+ stats: "{files} 条记忆",
97
97
  filesHeading: "文件",
98
98
  scopeLabel: "选择工作区",
99
- searchPlaceholder: "搜索记忆文件…",
99
+ searchPlaceholder: "搜索记忆…",
100
100
  searchEmpty: "没有匹配的记忆文件。",
101
- missing: "索引失联(索引引用但文件已删):",
102
- unindexed: "未入索引(文件存在但索引没有行):",
103
- readError: "读取失败:",
104
- previewLoading: "正在加载文件…",
101
+ quarantined: "巡检隔离(疑似密钥泄漏等异常,已隐藏):",
102
+ readError: "读取失败:",
103
+ previewLoading: "正在加载记忆…",
105
104
  previewTooLarge: "该记忆文件超过 5 MiB 预览上限。",
106
- previewDeleted: "该记忆文件已被删除,请刷新文件列表。",
105
+ previewDeleted: "该记忆已被删除,请刷新列表。",
107
106
  previewFailed: "内容读取失败:",
108
107
  justNow: "刚刚",
109
108
  minutesAgo: "{count} 分钟前",
@@ -113,25 +112,24 @@ window.__ModuleLoader__.load({
113
112
  };
114
113
  var en = {
115
114
  title: "Workspace Memory",
116
- desc: "Read-only projection of each workspace's .evolve/memory/.",
115
+ desc: "Read-only projection of the central memory store (~/.dsh/evolve/memory.db).",
117
116
  refresh: "Refresh",
118
- hint: "The source of truth is the markdown files under each workspace's .evolve/memory/ — read and written by the model; this card only displays them, edit the files directly.",
117
+ hint: "The source of truth is the central memory store — the model proposes through memory_write, code validates and lands it (code is the sole writer); this card only displays it.",
119
118
  loading: "Loading…",
120
119
  loadFailed: "Failed to load card data: ",
121
- empty: "No workspace has a memory store yet — open a session in a workspace, then come back and refresh.",
122
- absent: "No memory store in this workspace yet (one is created when you open a session).",
120
+ empty: "No workspace has memories yet — use a workspace normally, then come back and refresh.",
121
+ absent: "No memories in this workspace yet.",
123
122
  readFailed: "Failed to read the memory store: ",
124
- stats: "{files} memories · {index} index rows",
123
+ stats: "{files} memories",
125
124
  filesHeading: "Files",
126
125
  scopeLabel: "Choose workspace",
127
126
  searchPlaceholder: "Search memory files…",
128
127
  searchEmpty: "No matching memory files.",
129
- missing: "Dangling index (indexed but deleted): ",
130
- unindexed: "Unindexed (on disk, missing from the index): ",
131
- readError: "Read failed: ",
132
- previewLoading: "Loading file…",
128
+ quarantined: "Quarantined by patrol (suspected secrets, hidden): ",
129
+ readError: "Read failed: ",
130
+ previewLoading: "Loading memory…",
133
131
  previewTooLarge: "This memory file exceeds the 5 MiB preview limit.",
134
- previewDeleted: "This memory file has been deleted — refresh the file list.",
132
+ previewDeleted: "This memory has been deleted — refresh the list.",
135
133
  previewFailed: "Failed to read the content: ",
136
134
  justNow: "Just now",
137
135
  minutesAgo: "{count} min ago",
@@ -231,11 +229,11 @@ window.__ModuleLoader__.load({
231
229
  *
232
230
  * @param {typeof fetch} fetchImpl - injectable for tests.
233
231
  * @param {string} root - known workspace root.
234
- * @param {string} file - plain .md file name inside that memory store.
232
+ * @param {string} id - the memory id inside that workspace's partition.
235
233
  * @returns {Promise<{status: "ready"|"deleted"|"tooLarge"|"failed", content: string, message: string}>}
236
234
  */
237
- async function loadFileContent(fetchImpl, root, file) {
238
- var url = API_ROOT + "/memory/file?root=" + encodeURIComponent(root) + "&file=" + encodeURIComponent(file);
235
+ async function loadFileContent(fetchImpl, root, id) {
236
+ var url = API_ROOT + "/memory/file?root=" + encodeURIComponent(root) + "&id=" + encodeURIComponent(id);
239
237
  try {
240
238
  var response = await fetchImpl(url);
241
239
  if (response.ok) {
@@ -379,11 +377,11 @@ window.__ModuleLoader__.load({
379
377
  createElement(
380
378
  "span",
381
379
  { key: "icon", className: "dce-file-icon" },
382
- createElement(UI.FileIcon, { path: file.file, size: 16 }),
380
+ createElement(UI.FileIcon, { path: file.id + ".md", size: 16 }),
383
381
  ),
384
382
  );
385
383
  }
386
- rowChildren.push(createElement("span", { key: "name", className: "dce-file-name" }, file.file));
384
+ rowChildren.push(createElement("span", { key: "name", className: "dce-file-name" }, file.title));
387
385
  var time = formatRelative(file.updatedAt, now, t);
388
386
  if (time !== "") {
389
387
  rowChildren.push(createElement("span", { key: "time", className: "dce-file-time" }, time));
@@ -403,7 +401,7 @@ window.__ModuleLoader__.load({
403
401
  ];
404
402
  if (expanded) {
405
403
  var body = null;
406
- if (preview === null || preview.file !== file.file) {
404
+ if (preview === null || preview.file !== file.id) {
407
405
  body = createElement("div", { className: "dce-hint" }, t("previewLoading"));
408
406
  } else if (preview.status === "loading") {
409
407
  body = createElement("div", { className: "dce-hint" }, t("previewLoading"));
@@ -651,7 +649,7 @@ window.__ModuleLoader__.load({
651
649
  if (normalizedQuery.length === 0) {
652
650
  return true;
653
651
  }
654
- return [file.file, file.name, file.description, file.type]
652
+ return [file.id, file.title, file.description, file.type]
655
653
  .filter(function (value) {
656
654
  return typeof value === "string";
657
655
  })
@@ -663,21 +661,12 @@ window.__ModuleLoader__.load({
663
661
  createElement(
664
662
  "div",
665
663
  { key: "stats", className: "dce-stats" },
666
- t("stats", { files: snapshot.fileCount, index: snapshot.indexEntryCount }),
664
+ t("stats", { files: snapshot.fileCount }),
667
665
  ),
668
666
  ];
669
- if (snapshot.missingFiles.length > 0) {
667
+ if (snapshot.quarantined.length > 0) {
670
668
  parts.push(
671
- createElement("div", { key: "missing", className: "dce-warn" }, t("missing") + snapshot.missingFiles.join("、")),
672
- );
673
- }
674
- if (snapshot.unindexedFiles.length > 0) {
675
- parts.push(
676
- createElement(
677
- "div",
678
- { key: "unindexed", className: "dce-warn" },
679
- t("unindexed") + snapshot.unindexedFiles.join("、"),
680
- ),
669
+ createElement("div", { key: "quarantined", className: "dce-warn" }, t("quarantined") + snapshot.quarantined.map(function (entry) { return entry.id; }).join("、")),
681
670
  );
682
671
  }
683
672
  parts.push(
@@ -716,15 +705,15 @@ window.__ModuleLoader__.load({
716
705
  { key: "files", className: "dce-list" },
717
706
  files.map(function (file) {
718
707
  return createElement(FileRow, {
719
- key: file.file,
708
+ key: file.id,
720
709
  file: file,
721
710
  now: now,
722
711
  t: t,
723
712
  UI: UI,
724
- expanded: expandedFile === file.file,
713
+ expanded: expandedFile === file.id,
725
714
  preview: preview,
726
715
  onToggle: function () {
727
- onToggleFile(file.file);
716
+ onToggleFile(file.id);
728
717
  },
729
718
  });
730
719
  }),
@@ -1,4 +1,4 @@
1
- import type { WorkspaceCatalog } from "./workspace-catalog.js";
1
+ import type { MemoryStore } from "./store.js";
2
2
  /** URL prefix of the card API (kind-exact routes registered under it). */
3
3
  export declare const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
4
4
  export declare const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
@@ -24,10 +24,10 @@ export interface CardWebServer {
24
24
  register(route: CardRoute): unknown;
25
25
  }
26
26
  /**
27
- * Register the two card routes and return a disposer that removes them.
27
+ * Register the card routes and return a disposer that removes them.
28
28
  * Registration return values are passed through untouched: whatever disposer
29
29
  * shape the host hands back (or none, as on some host versions), the
30
30
  * disposer calls only the functions among them.
31
31
  */
32
- export declare function mountCardRoutes(webServer: CardWebServer, catalog: WorkspaceCatalog): () => void;
32
+ export declare function mountCardRoutes(webServer: CardWebServer, store: MemoryStore): () => void;
33
33
  //# sourceMappingURL=card-routes.d.ts.map
@@ -3,17 +3,15 @@
3
3
  *
4
4
  * Mounted through the host's `webServer` service (same registration shape
5
5
  * dshmarket uses: `kind: "exact"` routes on the profile's web server), the
6
- * three endpoints are GET-only projections: the workspace catalogue, the
7
- * memory snapshot for a known root, and one memory file's body for the card's
8
- * content preview. The catalogue IS the security fence — the memory endpoints
9
- * resolve their `root` parameter only against workspaces the host's registry
10
- * (or, on hosts without one, a root this process has served) knows about, and
11
- * the file endpoint additionally accepts only plain .md entries of that
12
- * workspace's memory directory, so the API cannot read arbitrary paths.
13
- * Handlers are synchronous and never throw past the response.
6
+ * three endpoints are GET-only projections of the central SQLite store: the
7
+ * workspace list (active partitions whose directory still exists), the
8
+ * memory snapshot for a known workspace, and one record's body by id. The
9
+ * store itself is the security fence — a `root` parameter only resolves
10
+ * when the database has rows for it, so the API cannot probe the
11
+ * filesystem; the content endpoint takes an id, never a path. Handlers are
12
+ * synchronous and never throw past the response.
14
13
  */
15
- import { resolve } from "node:path";
16
- import { memoryFileContent, memorySnapshot } from "./memory-snapshot.js";
14
+ import { memoryEntryContent, listCardWorkspaces, memorySnapshot } from "./memory-snapshot.js";
17
15
  /** URL prefix of the card API (kind-exact routes registered under it). */
18
16
  export const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
19
17
  export const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
@@ -25,13 +23,17 @@ function sendJson(response, status, body) {
25
23
  function isGet(request) {
26
24
  return (request.method ?? "GET") === "GET";
27
25
  }
26
+ /** The card's read fence: a root only resolves when the store has rows for it. */
27
+ function isKnownRoot(store, root) {
28
+ return listCardWorkspaces(store).some((workspace) => workspace.root === root);
29
+ }
28
30
  /**
29
- * Register the two card routes and return a disposer that removes them.
31
+ * Register the card routes and return a disposer that removes them.
30
32
  * Registration return values are passed through untouched: whatever disposer
31
33
  * shape the host hands back (or none, as on some host versions), the
32
34
  * disposer calls only the functions among them.
33
35
  */
34
- export function mountCardRoutes(webServer, catalog) {
36
+ export function mountCardRoutes(webServer, store) {
35
37
  const disposers = [];
36
38
  disposers.push(webServer.register({
37
39
  kind: "exact",
@@ -42,9 +44,7 @@ export function mountCardRoutes(webServer, catalog) {
42
44
  response.end();
43
45
  return;
44
46
  }
45
- // The listed set is the display projection (workspaces with a
46
- // memory directory); the read fence is `known` below.
47
- sendJson(response, 200, { workspaces: catalog.list() });
47
+ sendJson(response, 200, { workspaces: listCardWorkspaces(store) });
48
48
  },
49
49
  }));
50
50
  disposers.push(webServer.register({
@@ -61,14 +61,13 @@ export function mountCardRoutes(webServer, catalog) {
61
61
  sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
62
62
  return;
63
63
  }
64
- const known = knownEntryOf(catalog, root);
65
- if (known === undefined) {
66
- // The catalogue answers 404 without distinguishing "not a
67
- // workspace here" from "wrong path": no filesystem oracle.
64
+ if (!isKnownRoot(store, root)) {
65
+ // The store answers 404 without distinguishing "no rows" from
66
+ // "wrong path": no filesystem oracle.
68
67
  sendJson(response, 404, { error: "unknown workspace root" });
69
68
  return;
70
69
  }
71
- sendJson(response, 200, memorySnapshot(known.root));
70
+ sendJson(response, 200, memorySnapshot(store, root));
72
71
  },
73
72
  }));
74
73
  disposers.push(webServer.register({
@@ -85,39 +84,33 @@ export function mountCardRoutes(webServer, catalog) {
85
84
  sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
86
85
  return;
87
86
  }
88
- const file = fileParamOf(request);
89
- if (file === null) {
90
- sendJson(response, 400, { error: "file query parameter is required" });
87
+ const id = idParamOf(request);
88
+ if (id === null) {
89
+ sendJson(response, 400, { error: "id query parameter is required" });
91
90
  return;
92
91
  }
93
- const known = knownEntryOf(catalog, root);
94
- if (known === undefined) {
95
- // Same fence as the snapshot route: 404 without distinguishing
96
- // "not a workspace here" from "wrong path".
92
+ if (!isKnownRoot(store, root)) {
93
+ // Same fence as the snapshot route.
97
94
  sendJson(response, 404, { error: "unknown workspace root" });
98
95
  return;
99
96
  }
100
- const content = memoryFileContent(known.root, file);
97
+ const content = memoryEntryContent(store, root, id);
101
98
  if (!content.ok) {
102
- // The fence reasons stay indistinguishable from absence in status
103
- // terms only where safe: "outside" answers 400 (a client bug),
104
- // absence 404, oversized 413 — never a body leak either way.
105
- if (content.reason === "outside") {
106
- sendJson(response, 400, { error: "file must be a plain .md entry of the memory directory" });
107
- }
108
- else if (content.reason === "too-large") {
109
- sendJson(response, 413, { error: "memory file exceeds the 5 MiB preview limit" });
99
+ // Absence answers 404; the oversized case is theoretically
100
+ // unreachable (bodies are schema-capped at 64KB) but keeps its
101
+ // own status for the day the preview limit shrinks.
102
+ if (content.reason === "too-large") {
103
+ sendJson(response, 413, { error: "memory body exceeds the 5 MiB preview limit" });
110
104
  }
111
105
  else {
112
- sendJson(response, 404, { error: "memory file not found" });
106
+ sendJson(response, 404, { error: "memory not found" });
113
107
  }
114
108
  return;
115
109
  }
116
110
  sendJson(response, 200, {
117
- file: content.file,
111
+ id: content.id,
118
112
  content: content.content,
119
- mtimeMs: content.mtimeMs,
120
- changed: content.changed,
113
+ updatedAtMs: content.updatedAtMs,
121
114
  });
122
115
  },
123
116
  }));
@@ -129,11 +122,6 @@ export function mountCardRoutes(webServer, catalog) {
129
122
  }
130
123
  };
131
124
  }
132
- /** The catalogued workspace a `root` query parameter resolves to, if any. */
133
- function knownEntryOf(catalog, root) {
134
- const target = resolve(root);
135
- return catalog.known().find((workspace) => workspace.root === target);
136
- }
137
125
  /** The decoded `root` query parameter, or null when absent/relative. */
138
126
  function rootParamOf(request) {
139
127
  try {
@@ -150,15 +138,15 @@ function rootParamOf(request) {
150
138
  return null;
151
139
  }
152
140
  }
153
- /** The decoded `file` query parameter, or null when absent. */
154
- function fileParamOf(request) {
141
+ /** The decoded `id` query parameter, or null when absent. */
142
+ function idParamOf(request) {
155
143
  try {
156
144
  const url = new URL(request.url ?? "/", "http://dsh-card.invalid");
157
- const file = url.searchParams.get("file");
158
- if (file === null || file.trim().length === 0) {
145
+ const id = url.searchParams.get("id");
146
+ if (id === null || id.trim().length === 0) {
159
147
  return null;
160
148
  }
161
- return file;
149
+ return id;
162
150
  }
163
151
  catch (error) {
164
152
  void error;