dsh-continual-evolve 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -21
- package/README.zh.md +33 -21
- package/client/client.js +32 -43
- package/lib/card-routes.d.ts +3 -3
- package/lib/card-routes.js +38 -50
- package/lib/extraction-prompt.d.ts +52 -0
- package/lib/extraction-prompt.js +139 -0
- package/lib/extraction-surface.d.ts +66 -0
- package/lib/extraction-surface.js +186 -0
- package/lib/extraction.d.ts +60 -0
- package/lib/extraction.js +269 -0
- package/lib/import-md.d.ts +15 -0
- package/lib/import-md.js +140 -0
- package/lib/index.d.ts +63 -27
- package/lib/index.js +121 -65
- package/lib/memory-guide.d.ts +22 -14
- package/lib/memory-guide.js +33 -76
- package/lib/memory-rules.d.ts +56 -0
- package/lib/memory-rules.js +152 -0
- package/lib/memory-section.d.ts +26 -26
- package/lib/memory-section.js +48 -82
- package/lib/memory-snapshot.d.ts +43 -59
- package/lib/memory-snapshot.js +101 -141
- package/lib/memory-tools.d.ts +34 -0
- package/lib/memory-tools.js +142 -0
- package/lib/store-apply.d.ts +46 -0
- package/lib/store-apply.js +177 -0
- package/lib/store.d.ts +95 -0
- package/lib/store.js +207 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,25 +7,28 @@ English | [中文](README.zh.md)
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
|
-
[]()
|
|
13
13
|
[]()
|
|
14
14
|
|
|
15
|
-
A workspace-memory plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`):
|
|
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.
|
|
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
|
|
24
|
-
2. **
|
|
25
|
-
3. **
|
|
26
|
-
4. **
|
|
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 and searches by keyword — 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` rearms an idle debounce (default 10 min, `memoryIndex.debounceMin`, single-flight with burst coalescing; `compaction/start` and session close flush). 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
|
|
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,18 @@ After installing or updating, restart the DSH profile you actually use (`dsh web
|
|
|
41
44
|
|
|
42
45
|
## Usage
|
|
43
46
|
|
|
44
|
-
No commands
|
|
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
|
|
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 |
|
|
58
|
+
| `memoryIndex.debounceMin` | `10` | Idle debounce minutes after a turn (0 = run every turn) |
|
|
54
59
|
| `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
60
|
|
|
56
61
|
Profile patch example:
|
|
@@ -60,13 +65,14 @@ Profile patch example:
|
|
|
60
65
|
config:
|
|
61
66
|
memoryIndex:
|
|
62
67
|
maxChars: 8000
|
|
68
|
+
debounceMin: 5
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
## Development
|
|
66
72
|
|
|
67
73
|
```bash
|
|
68
74
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
69
|
-
pnpm test # vitest (
|
|
75
|
+
pnpm test # vitest (168 tests)
|
|
70
76
|
pnpm test:coverage # v8 coverage, CI-enforced thresholds
|
|
71
77
|
pnpm lint # oxlint src test client
|
|
72
78
|
```
|
|
@@ -75,17 +81,23 @@ Layout:
|
|
|
75
81
|
|
|
76
82
|
```
|
|
77
83
|
├── src/
|
|
78
|
-
│ ├── index.ts
|
|
79
|
-
│ ├──
|
|
80
|
-
│ ├──
|
|
81
|
-
│ ├── memory-
|
|
82
|
-
│ ├──
|
|
83
|
-
│ ├──
|
|
84
|
-
│
|
|
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 + scheduler (debounce, single-flight, flush)
|
|
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 (
|
|
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) ·
|
|
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
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
|
-
[]()
|
|
13
13
|
[]()
|
|
14
14
|
|
|
15
|
-
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh
|
|
15
|
+
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的工作区记忆插件:全部记忆住在一个中央 SQLite 库里(`~/.dsh/evolve/memory.db`,按工作区路径行分区),会话开局注入索引,插件的代码是字面意义的**唯一写者**——模型提议,代码门禁后落盘。
|
|
16
16
|
|
|
17
17
|
## 为什么
|
|
18
18
|
|
|
19
|
-
Agent
|
|
19
|
+
Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项目背景)下个会话就忘。文件夹式的 md 记忆让记忆可见,但每一条写入都不设防:索引漂移、frontmatter 腐坏、超尺寸膨胀、密钥泄漏——坏写入落地之后才可见。本插件保留 ZCode 记忆形态(按工作区分区、开局注入索引、类型化记忆),把存储搬进一个所有变更都过机械门禁的数据库。
|
|
20
20
|
|
|
21
21
|
## 工作原理
|
|
22
22
|
|
|
23
|
-
1. **开局注入** —— 插件只注册一个 system-prompt section
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
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 取正文、按关键词检索——注入索引只有钩子。宿主没有工具服务时降级为"告诉用户"措辞,注入不受影响。
|
|
25
|
+
3. **提案制提取** —— 插件唯一的后台自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 重置空闲去抖计时器(默认 10 分钟,`memoryIndex.debounceMin`,单飞行合并突发;`compaction/start` 与会话收尾强制 flush)。一次运行读取该会话游标之后的轨迹增量,用 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
|
-
|
|
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,18 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
41
44
|
|
|
42
45
|
## 使用
|
|
43
46
|
|
|
44
|
-
|
|
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` |
|
|
54
|
+
| `memoryIndex.guide` | `true` | 是否注入指南(关闭且索引为空时该 section 零 token) |
|
|
52
55
|
| `memoryIndex.order` | `400` | section 顺序(上游命名槽位自 `PLAN_POLICY=500` 起) |
|
|
53
56
|
| `memoryIndex.maxChars` | `6000` | 注入索引的硬字符预算 |
|
|
57
|
+
| `memoryIndex.extraction` | `true` | 提案制提取运行 |
|
|
58
|
+
| `memoryIndex.debounceMin` | `10` | 轮末空闲去抖分钟数(0 = 每轮即跑) |
|
|
54
59
|
| `memoryCard.enabled` | `true` | 官方插件管理内的只读记忆卡片(宿主缺 `webServer`/`plugins.bundle.config` 时自动降级为无卡片) |
|
|
55
60
|
|
|
56
61
|
profile patch 示例:
|
|
@@ -60,13 +65,14 @@ profile patch 示例:
|
|
|
60
65
|
config:
|
|
61
66
|
memoryIndex:
|
|
62
67
|
maxChars: 8000
|
|
68
|
+
debounceMin: 5
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
## 开发
|
|
66
72
|
|
|
67
73
|
```bash
|
|
68
74
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
69
|
-
pnpm test # vitest(
|
|
75
|
+
pnpm test # vitest(168 例)
|
|
70
76
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
71
77
|
pnpm lint # oxlint src test client
|
|
72
78
|
```
|
|
@@ -75,17 +81,23 @@ pnpm lint # oxlint src test client
|
|
|
75
81
|
|
|
76
82
|
```
|
|
77
83
|
├── src/
|
|
78
|
-
│ ├── index.ts
|
|
79
|
-
│ ├──
|
|
80
|
-
│ ├──
|
|
81
|
-
│ ├── memory-
|
|
82
|
-
│ ├──
|
|
83
|
-
│ ├──
|
|
84
|
-
│
|
|
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 # 运行器 + 调度器(去抖、单飞行、flush)
|
|
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 测试(
|
|
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) ·
|
|
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: "
|
|
88
|
+
desc: "中央记忆库(~/.dsh/evolve/memory.db)的只读投影。",
|
|
89
89
|
refresh: "刷新",
|
|
90
|
-
hint: "
|
|
90
|
+
hint: "事实源是中央记忆库——模型经 memory_write 提案、代码校验后落库(代码是唯一写者);本卡片只展示。",
|
|
91
91
|
loading: "读取中…",
|
|
92
92
|
loadFailed: "卡片数据加载失败:",
|
|
93
|
-
empty: "
|
|
94
|
-
absent: "
|
|
93
|
+
empty: "还没有任何工作区写过记忆——在某个工作区正常使用后回来刷新。",
|
|
94
|
+
absent: "该工作区还没有记忆。",
|
|
95
95
|
readFailed: "记忆库读取失败:",
|
|
96
|
-
stats: "{files} 条记忆
|
|
96
|
+
stats: "{files} 条记忆",
|
|
97
97
|
filesHeading: "文件",
|
|
98
98
|
scopeLabel: "选择工作区",
|
|
99
|
-
searchPlaceholder: "
|
|
99
|
+
searchPlaceholder: "搜索记忆…",
|
|
100
100
|
searchEmpty: "没有匹配的记忆文件。",
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
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
|
|
122
|
-
absent: "No
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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}
|
|
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,
|
|
238
|
-
var url = API_ROOT + "/memory/file?root=" + encodeURIComponent(root) + "&
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
664
|
+
t("stats", { files: snapshot.fileCount }),
|
|
667
665
|
),
|
|
668
666
|
];
|
|
669
|
-
if (snapshot.
|
|
667
|
+
if (snapshot.quarantined.length > 0) {
|
|
670
668
|
parts.push(
|
|
671
|
-
createElement("div", { key: "
|
|
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.
|
|
708
|
+
key: file.id,
|
|
720
709
|
file: file,
|
|
721
710
|
now: now,
|
|
722
711
|
t: t,
|
|
723
712
|
UI: UI,
|
|
724
|
-
expanded: expandedFile === file.
|
|
713
|
+
expanded: expandedFile === file.id,
|
|
725
714
|
preview: preview,
|
|
726
715
|
onToggle: function () {
|
|
727
|
-
onToggleFile(file.
|
|
716
|
+
onToggleFile(file.id);
|
|
728
717
|
},
|
|
729
718
|
});
|
|
730
719
|
}),
|
package/lib/card-routes.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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,
|
|
32
|
+
export declare function mountCardRoutes(webServer: CardWebServer, store: MemoryStore): () => void;
|
|
33
33
|
//# sourceMappingURL=card-routes.d.ts.map
|
package/lib/card-routes.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
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(
|
|
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
|
|
89
|
-
if (
|
|
90
|
-
sendJson(response, 400, { error: "
|
|
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
|
-
|
|
94
|
-
|
|
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 =
|
|
97
|
+
const content = memoryEntryContent(store, root, id);
|
|
101
98
|
if (!content.ok) {
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
if (content.reason === "
|
|
106
|
-
sendJson(response,
|
|
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
|
|
106
|
+
sendJson(response, 404, { error: "memory not found" });
|
|
113
107
|
}
|
|
114
108
|
return;
|
|
115
109
|
}
|
|
116
110
|
sendJson(response, 200, {
|
|
117
|
-
|
|
111
|
+
id: content.id,
|
|
118
112
|
content: content.content,
|
|
119
|
-
|
|
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 `
|
|
154
|
-
function
|
|
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
|
|
158
|
-
if (
|
|
145
|
+
const id = url.searchParams.get("id");
|
|
146
|
+
if (id === null || id.trim().length === 0) {
|
|
159
147
|
return null;
|
|
160
148
|
}
|
|
161
|
-
return
|
|
149
|
+
return id;
|
|
162
150
|
}
|
|
163
151
|
catch (error) {
|
|
164
152
|
void error;
|