dsh-ledger-memory 0.1.0 → 0.1.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 +93 -1503
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,1546 +1,136 @@
|
|
|
1
|
-
# dsh-ledger-memory
|
|
1
|
+
# dsh-ledger-memory(台账记忆)
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
> 它不是"又一个记忆插件"——**台账**是它的核心形态:可回溯、可追查、只追加、按周归档。
|
|
6
|
-
> 除了台账本身,它还把**上下文压缩**、**逐字胶囊落盘**、**跨会话交接单**做成了闭环,
|
|
7
|
-
> 于是新对话能**无缝接上**旧对话的工作。
|
|
3
|
+
> 一个 **DSH(DeepSeek Harness)宿主侧插件**:让 AI 用可复现的方式,把项目里的工作**记住**。
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
它管的不是聊天记录,而是一个**项目自己的记忆载体 —— 台账**:
|
|
6
|
+
可回溯、可追查、只追加、按周归档。围绕它还有活动日志、上下文压缩与跨会话交接。
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
**不预设任何模板** —— 台账的文件名、分几个文件、有哪些字段,全部由 AI 在初始化时
|
|
9
|
+
按项目类型自己决定;插件只做**检测 / 注入 / 回写 / 归档 / Git 配合**这些流程性工作。
|
|
14
10
|
|
|
15
11
|
---
|
|
16
12
|
|
|
17
|
-
##
|
|
13
|
+
## 它解决什么问题
|
|
18
14
|
|
|
19
|
-
|
|
|
15
|
+
| 问题 | 它怎么做 |
|
|
20
16
|
|---|---|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
| ⑧ 手动命令 | `/ledger`(**裸敲弹选择卡片**,9 项)· `status` · `show` · `init` · `update` · `archive` · `log` · `compact` · `light` · `handoff` |
|
|
29
|
-
| ⑨ 纯本地 | 无外部服务、无网络、无浏览器半边;会话态只在内存,不落盘 |
|
|
30
|
-
| ⑩ 回合末四选一(**默认关**) | 卡片本身还在,但**默认不弹**(用户 2026-10-09 决定:"不用在每条对话结束后进行选择,因为台账自动化了")。要恢复每轮提问就传 `{ turnEndOffer: true }`。**两条手动路一直在**:`/ledger` 命令、以及 AI 自己判断有必要的 `ledger_checkpoint` |
|
|
31
|
-
| ⑪ 上下文水位分档 | 到 **30% / 50% / 70%** 各提醒一次 AI 做一遍**台账 + 日志更新**,**每一档都要求"做完继续干活、不要为此停顿"**。差别在 **70%**:那一档**插件会自己执行一次高保真压缩**(拦不住官方 80%,所以在 70% 提前把状态落袋),AI 照常继续工作 |
|
|
32
|
-
| ⑫ 压缩后台账重注 | 压缩(手动 `/compact`、自动压力、或用户选的那次)**完成后**,活跃台账会**强制重新注入**并说明"上下文刚被压缩" |
|
|
33
|
-
| ⑬ 活动日志(可选) | 项目开始时建立,**永不注入上下文**。五档粒度(L0/L1/L2/L3/**L4**),按周分桶并与归档**共用期间键**;需要时用 `ledger_log` 回溯读切片 |
|
|
34
|
-
| ⑭ `root` 参数 | 会话 cwd 是**容器**时,可用 `root` 点名子项目(不传就只能指向容器根)。`root` **不绕过**容器闸门,且必须是已存在的目录 |
|
|
35
|
-
| ⑮ 问题记录(不是项目时) | "只是解决一个问题 / 做一次调研"这类**有终点**的事:**不建台账**,用 `ledger_note` 写一份问题记录,落到工作区**既有的**记录通道(`60-对话记录与交接/对话总结/`),命名 `YYYY-MM-DD-主题.md`,**永不覆盖** |
|
|
36
|
-
| ⑯ 第一轮先问建不建 | **会话第一轮结束**时插件自己弹一张卡:建项目 / 不建只写问题记录 / 暂时不用记。选完才轮到常规四选一 —— **两问分两轮**,且第一问**只问一次** |
|
|
37
|
-
| ⑰ 多会话同项目 | 多个对话做同一个项目时:条目带**会话编号**,写入带**版本号**。旧版本重写**不拒绝**,但会丢的条目被**自动保留**。谁改过台账可查 `ledger_log({view:"audit"})` |
|
|
38
|
-
| ⑱ 已有项目的台账重构 | 检测**已存在**项目的台账是否合规(对着自己的硬约束判),不合规时弹卡片问要不要改。修复**只做加法**:补清单字段、补缺失的空文件/空目录,**绝不改写或删除已有内容** |
|
|
39
|
-
| ⑲ 改动回执 | AI 自己写完台账后,插件会**让 AI 在你这次的回复里捎带一句**"台账被改过、哪个会话改的"(编号与条目行首一致)。也可用 `/ledger status` 主动查。**不弹卡、不打断工作** |
|
|
40
|
-
| ⑳ 用户消息存档(L4) | **自动记住你说过的每一句话**(不用 AI 记得写),每条带**来源会话编号**与**发送时间**;**永不注入上下文**,只在需要时用 `ledger_log({view:"users"})` 查。多会话项目里能按会话分组看"哪个对话说过什么" |
|
|
41
|
-
| ㉑ 70% 高保真压缩 | 到 **70%** 由插件**自己压一次**(官方 80% 那次拦不住,所以在 70% 提前把状态落袋)。压缩后重新注入一份**逐字胶囊**:**你说过的原话**、**报错原文**、**器物索引**(改过哪些文件)、**可重读的路径**。⇒ 压完 AI 仍然知道刚才在做什么,**照常继续工作** |
|
|
17
|
+
| 换个会话就"忘了这个项目干到哪" | 会话开始把**活跃台账全文**注入上下文;内容没变则不重复注入(防打烂 prompt 缓存) |
|
|
18
|
+
| AI 走完一轮,成果没落地 | 回合末把「完成了什么 / 卡在哪 / 下一步」写回活跃台账 |
|
|
19
|
+
| 台账越来越长,读不完 | 超阈值时把**最旧的已完成条目**移进 `归档目录/YYYY-MM/weekN`,**只追加不删除** |
|
|
20
|
+
| 细节被上下文压缩抹掉 | 压缩**之前**把用户原话、报错原文、改过哪些文件落盘成一份**逐字胶囊**;压缩后与台账一起注回 |
|
|
21
|
+
| 新会话接不上旧会话 | 写一份**交接单**,并把指针推进新会话的**第一轮注入**(不是等你来问) |
|
|
22
|
+
| 想查"谁什么时候改过这份台账" | 台账末尾自动维护一张变更登记;完整历史在只追加的活动日志里 |
|
|
23
|
+
| 只是一次性排查,不是项目 | `ledger_note` 写一份**问题记录**(现象 → 根因 → 结论 → 做了什么),不建台账 |
|
|
42
24
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
## 二、快速上手
|
|
46
|
-
|
|
47
|
-
插件已装入 profile `desktop`。**在项目目录里开一个新会话**,然后:
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
/ledger status # 看这个项目现在有没有台账(只读)
|
|
51
|
-
/ledger init # 让 AI 为它设计一套台账(含 git init)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### ★ 直接敲 `/ledger`(不带参数)会弹一张可点的卡片
|
|
55
|
-
|
|
56
|
-
九个功能都在卡片上,点一下就行 —— **不用记子命令名字**。
|
|
57
|
-
|
|
58
|
-
> **为什么不能在命令下拉里选功能?** 平台不允许。命令的 `input` 只接受**一个字符串提示**:
|
|
59
|
-
> `dsh-commands/lib/index.js` 里写着
|
|
60
|
-
> `if (!("hint" in rawInput) || typeof rawInput.hint !== "string") throw new TypeError(…)`,
|
|
61
|
-
> 归一化后只有 `{ hint, attachments? }`,**没有任何 enum / options 字段**,
|
|
62
|
-
> 所以命令行这一层渲染不出二级菜单。官方 `/goal` 也只能把
|
|
63
|
-
> `[<objective>|clear|edit <objective>|pause|resume]` 当作**纯文字提示**写出来。
|
|
64
|
-
>
|
|
65
|
-
> 但命令的 handler 是**被 await 的、并且平台对它没有超时**
|
|
66
|
-
> (`dsh-commands/lib/index.js` 里 timeout / setTimeout / deadline 命中 **0** 次),
|
|
67
|
-
> 所以裸 `/ledger` 走了另一条路:**弹一张 `userQuestions` 卡片** ——
|
|
68
|
-
> 和回合末四选一、和 `ask_user_question` 是同一个机制。
|
|
69
|
-
>
|
|
70
|
-
> 卡片自带 120 秒显式超时:被丢弃(dismiss)时那条 waterfall **不结算**,
|
|
71
|
-
> 不给 signal 的话 `/ledger` 会永远挂着。超时或丢弃一律回退成打印帮助文本。
|
|
72
|
-
|
|
73
|
-
**★ 那 8 个 `ledger_*` 工具不是给你用的,是给 AI 用的** —— 你没法(也不该)直接调用它们。
|
|
74
|
-
你这条路上能碰的东西只有两个:**`/ledger` 命令**(自己看状态)和**卡片**(做选择)。
|
|
75
|
-
`/ledger update | init | archive` 也只是"请 AI 去做":它们只置一个待办标记,
|
|
76
|
-
由下一步注入给 AI,AI 才真正调工具。
|
|
77
|
-
|
|
78
|
-
`/ledger init` 之后,AI 会自己决定结构(例如 `LEDGER.md` + `DECISIONS.md` +
|
|
79
|
-
`_ledger-archive/`),并调用 `ledger_init` 登记。之后每次会话都会自动带上台账。
|
|
80
|
-
|
|
81
|
-
日常用法:
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
/ledger show # 打印当前活跃台账全文
|
|
85
|
-
/ledger update # 让 AI 把本次进展写回台账
|
|
86
|
-
/ledger archive # 让 AI 把最旧已完成条目移进归档
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
也可以什么都不敲 —— 会话里 AI 自己会在合适的时候调用工具维护台账。
|
|
90
|
-
|
|
91
|
-
### 回合末的四选一长什么样
|
|
92
|
-
|
|
93
|
-
**每次回合结束**,输入框旁边会自己出现一张卡片(不需要 AI 做什么,插件在
|
|
94
|
-
`agent/turn-stopping` 里弹):
|
|
95
|
-
|
|
96
|
-
```
|
|
97
|
-
项目台账
|
|
98
|
-
本轮已结束,接下来怎么处理?
|
|
99
|
-
○ 不记录台账
|
|
100
|
-
○ 只记录台账
|
|
101
|
-
○ 记录台账 + 执行压缩
|
|
102
|
-
○ 记录台账 + 写交接单
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
- 选「**执行压缩**」⇒ AI 先写台账,**本轮结束后**(agent 转 idle)插件自动调用官方
|
|
106
|
-
`compaction.compactNow` 压缩上下文,压完再**把台账重新注入**
|
|
107
|
-
- 选「**写交接单**」⇒ AI 除台账外再产出一份交接文档,并把路径告诉你
|
|
108
|
-
- 选「**不记录台账**」⇒ 什么都不会发生(也不会白起一轮)
|
|
109
|
-
- 你也可以自己敲 `/compact` 手动压缩 —— 压完同样会重新注入台账(见 §五)
|
|
110
|
-
|
|
111
|
-
**这张卡片不会卡住你的回合。** 插件在 `agent/turn-stopping` 里**只把卡片挂出去、立刻返回**,
|
|
112
|
-
所以选不选、什么时候选都不影响会话继续;你选中以后,插件用 `agent.steer` 把 AI 唤醒去执行
|
|
113
|
-
(idle 时 steer 会开新一轮,见 `dsh-agent-loop/lib/index.js:854` 的 JSDoc
|
|
114
|
-
「A wake sent while idle always opens its turn boundary」)。
|
|
115
|
-
|
|
116
|
-
> 为什么不能在里面等答复:`agent/turn-stopping` 是回合循环 `await` 的
|
|
117
|
-
> (`dsh-agent-loop/lib/index.js:999`)。在里面等用户点选,等于把回合一直挂着 ——
|
|
118
|
-
> 你走开十分钟,会话就十分钟显示"运行中",新消息也只能排队。
|
|
119
|
-
> 这也正合需求 ② 的「**不要打断工作**」。
|
|
120
|
-
|
|
121
|
-
卡片不会连续烦你,规则是:
|
|
122
|
-
|
|
123
|
-
- **你选了、AI 去执行了,那一轮结束不会再问**(否则"选完 → 干活 → 又被问"会无限套娃)
|
|
124
|
-
- **卡片还挂着没答复时不重复问**
|
|
125
|
-
- **被无视超过 30 分钟**(`AUTO_OFFER_STALE_MS`)⇒ 之后再问一次,不会永久静默
|
|
126
|
-
- **子 agent 的回合末不弹**(只有 root 会话会打扰你)
|
|
127
|
-
|
|
128
|
-
> AI 自己判断"现在该收尾了"时,也可以主动调 `ledger_checkpoint`(需求 ②)。
|
|
129
|
-
> 两个入口的选项与后果**完全一致**(共用 `checkpointQuestions` / `checkpointNext`),
|
|
130
|
-
> 只是同一次停顿上**只会问一次**。
|
|
131
|
-
|
|
132
|
-
### ★ 第一轮结束时会先问另一件事:要不要把这里建成项目
|
|
133
|
-
|
|
134
|
-
**会话的第一轮**结束时会先弹这张(不是上面那张四选一):
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
项目台账
|
|
138
|
-
这是本次会话的第一轮。这个目录要不要建成一个项目(Git + 台账)?
|
|
139
|
-
○ 建项目(初始化台账)
|
|
140
|
-
○ 不建,只写问题记录
|
|
141
|
-
○ 暂时不用记
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
这就是需求 ① 从"**靠 AI 记得问**"升级成"**插件自己问**"。原来的做法只是注入一句话
|
|
145
|
-
让 AI 去征询用户意见,而 AI 还要顾别的事,实际上经常没问出口。
|
|
146
|
-
|
|
147
|
-
**两问分两轮**:第一轮问「建不建」,选完(AI 去做)之后,**再往后**的回合末才是
|
|
148
|
-
「是否写台账」那张四选一。第一问**每个会话只问一次**。
|
|
149
|
-
|
|
150
|
-
三个选项各自的后果:
|
|
151
|
-
|
|
152
|
-
| 你选 | AI 会做什么 |
|
|
153
|
-
|---|---|
|
|
154
|
-
| 建项目 | 判断项目类型 → **自己设计**台账结构 → `ledger_init`(含 `git init`)。**容器工作区里它不会照办**,而是先问你是哪个子项目 |
|
|
155
|
-
| 不建,只写问题记录 | 用 `ledger_note` 记一份有终点的问题记录,**不建台账、不 `git init`** |
|
|
156
|
-
| 暂时不用记 | 什么都不写 |
|
|
157
|
-
|
|
158
|
-
**已经有台账的目录不会再被问这一句**(没意义),直接走常规四选一。
|
|
159
|
-
|
|
160
|
-
> ★ 为什么"容器里选建项目"不能被照做:那里 `git init` 会把所有子项目卷进一个错误的
|
|
161
|
-
> 父仓库。实测 112 个会话里 **104 个**的 cwd 就是容器根,所以这是**最常见**的情形 ——
|
|
162
|
-
> 卡片不会假装答应,而是让 AI 先问清楚是哪个子项目。
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## 三、十二个工具(AI 用,你也可以在对话里让它调用)
|
|
167
|
-
|
|
168
|
-
| 工具 | 作用 |
|
|
169
|
-
|---|---|
|
|
170
|
-
| `ledger_status` | **只读**探测:有没有台账、用的哪个文件(清单还是猜的)、有没有 Git、几行、超没超阈值、日志开没开、**不是项目时该把问题记录写哪**。免费,不会写任何东西 |
|
|
171
|
-
| `ledger_init` | 登记 AI 设计的结构 + `git init` + 合并 `.gitignore` + 写初始内容(**不覆盖已存在的文件**)。可选传 `logDir` 启用活动日志 |
|
|
172
|
-
| `ledger_conform` | **已有项目的台账重构**:检测合规性,`apply:false`(默认)只预览,`apply:true` 才执行**只做加法**的修复(补清单字段、补缺失的空文件/空目录) |
|
|
173
|
-
| `ledger_read` | `target: active` / `decisions` / `manifest` / `archive`(`archive` 可列出目录或读单个归档文件) |
|
|
174
|
-
| `ledger_write` | 写活跃台账或决策记录。活跃台账超阈值会**拒绝写**并提示先归档;`force: true` 可越过。决策记录不受行数闸门 |
|
|
175
|
-
| `ledger_archive` | 一次提交 `entries`(要归档的原文)+ `newActive`(精简后的活跃台账);插件按 `YYYY-MM/weekN` 建目录、只追加 |
|
|
176
|
-
| `ledger_log` | **回溯读活动日志**(只读)。可按 `levels` / `since` / `until` / `grep` 过滤,视图 `timeline` · `files` · `failures` · **`users`** · `audit` · `raw`。`view:"users"` 专读 **L4 用户消息**(按会话编号汇总)。**日志永不注入上下文**,只有你调它时才读 |
|
|
177
|
-
| `ledger_log_write` | 追加**一条 L1 回合摘要**(一行)。L0/L2/L3/**L4** 由插件自动采集,**只有这一档需要 AI 写** —— 因为它带"为什么" |
|
|
178
|
-
| `ledger_note` | **不是项目时**才用:把一次性问题/调研写成 `YYYY-MM-DD-主题.md` 放进工作区既有记录通道。**永不覆盖**已存在的记录;在 Git 项目里会被拒绝并指向 `ledger_write` |
|
|
179
|
-
| `ledger_handoff` | ★ **写一份交接单,给下一场会话接手用**(第③层)。五段:`did` / `state` / `next` / `files` / `blocked`,自带「去哪重读」的真源路径。落到项目的**约定位置**(`_handoff/`),**永不覆盖**。★ 若你要收尾、换方向、或这次改动很大 —— **用它**:下一场会话开在这个项目时,注入里会**自动告诉它先读这一份** |
|
|
180
|
-
| `ledger_compact` | **手动执行"我们的"高保真压缩**(需求 ②):只**排队**,回合结束、agent 空闲时才真压;压完把台账 + **逐字胶囊**重新注入。**不打断当前回合** —— 排完队你继续干活 |
|
|
181
|
-
| `ledger_checkpoint` | 在输入框旁弹**四选一卡片**(不记录 / 只记台账 / 记台账+压缩 / 记台账+写交接单),返回用户的选择与"接下来该做什么"。**回合末默认已不弹**(需求 ⑫),这个工具是给 AI 在真有必要时主动问用的 |
|
|
182
|
-
|
|
183
|
-
### `ledger_checkpoint` 的两个要点
|
|
184
|
-
|
|
185
|
-
1. **默认阻塞**(`ask`),这正是可靠路径:答复直接回到这次调用。
|
|
186
|
-
传 `nonBlocking: true` 会走 `askTimed`,但**本 profile 下拿不到"用户随时仍可作答"的保证** ——
|
|
187
|
-
因为官方 `tool-ask-user` 走默认 `mode: "legacy"`,`dsh-user-questions` 的事件折叠
|
|
188
|
-
(`lib/index.js:188-215`,判据是 `fold.timed`,由**本次装配的工具 schema** 是否声明
|
|
189
|
-
`timeout` 参数决定)恒为 `false`,插件自己发起的非阻塞请求因此**不进投影、不会变 `continued`**,
|
|
190
|
-
客户端也就不会给它渲染可继续作答的卡片。所以 `nonBlocking` 只作为显式逃生口保留。
|
|
191
|
-
2. **没有 `userQuestions` 服务时软失败**(返回 `ok: false` + 建议),**不抛错**:
|
|
192
|
-
没装卡片 UI 的客户端不该因为台账插件而打断对话。
|
|
193
|
-
|
|
194
|
-
### 压缩是谁执行的
|
|
195
|
-
|
|
196
|
-
**★ 现在有两条路**(2026-10-09 起,见下节 D41):**常态是增量(分段)压缩**,
|
|
197
|
-
绝对 70% 那条降级成**安全网**。
|
|
198
|
-
|
|
199
|
-
- **增量那条**:在 `agent/pre-step`(回合内)调 `compaction.compactRegion(start, end, agent)`
|
|
200
|
-
—— 范围由**我们**选(保留最近一大块,只压最旧一段)。
|
|
201
|
-
- **安全网那条**:水位到绝对阈值时排队,等 `agent/status` 转 `idle` 再调
|
|
202
|
-
`compaction.compactNow(agent, signal)`。
|
|
203
|
-
- **为什么它必须等 idle**:`compactNow` 内部走 `agent.runMaintenance`,非 idle 直接抛
|
|
204
|
-
`ManualCompactionError("busy", …)`(`compaction-basic/lib/index.js:1008-1010` +
|
|
205
|
-
`dsh-agent-loop/lib/index.js:822` 的 `phase.kind !== "idle"` 检查)。
|
|
206
|
-
|
|
207
|
-
### ★★★ 增量(分段)压缩:把 1M 窗口用成 2M(D41)
|
|
208
|
-
|
|
209
|
-
★ 这是用户从一开始就要的那件事。用户原话(逐字):
|
|
210
|
-
|
|
211
|
-
> *"不是完全压缩…而是允许模型进行**轻度压缩**,并且**强制在压缩之前进行台账和日志更新**。"*
|
|
212
|
-
> *"AI每输出20%的时候,调用一次工具压缩到12%,类似这种比例。"*
|
|
213
|
-
> *"把这20%的输出压到只剩10~12%。以此类推,可以极大的拓展一兆的上下文窗口。"*
|
|
214
|
-
> *"要求模型…注意给自己的任务划分节点…这不能消耗太多模型的tokens。"*
|
|
215
|
-
|
|
216
|
-
**怎么做到的**(读内核实现得出的,非猜):
|
|
217
|
-
|
|
218
|
-
| | `compactNow`(旧,安全网) | `compactRegion` + 大 retain(增量) |
|
|
219
|
-
|---|---|---|
|
|
220
|
-
| 保留最近多少 | `retainTokens = 0` ⇒ **几乎全压** | 最近 10 点**一个字不动** |
|
|
221
|
-
| 什么时候能调 | 要求 **idle** | 要求**回合开着**(所以放 pre-step) |
|
|
222
|
-
| 效果 | 水位大落、细节大片丢 | 水位小落、新近细节完整 |
|
|
223
|
-
|
|
224
|
-
**★ 「落在节点附近」是零 token 的** —— **把"写台账"本身当作节点信号**:
|
|
225
|
-
模型写台账时本就在做"什么留上下文、什么进台账"的判断,那就是它在说"我到节点了"。
|
|
226
|
-
而"压缩前强制落盘"本来就要等这个信号 ⇒ **两个需求共用同一个信号**,
|
|
227
|
-
既没让模型多输出一个字,也天然满足"允许提前或超出一点"。
|
|
228
|
-
|
|
229
|
-
**★ 你能调的三个旋钮**(设置面板):
|
|
230
|
-
|
|
231
|
-
| 设置 | 默认 | 意思 |
|
|
232
|
-
|---|---|---|
|
|
233
|
-
| 增量(分段)压缩 | **开** | 关掉就退回"到水位一次性全压"的老做法 |
|
|
234
|
-
| 增量压缩·触发涨幅 | **20** 点 | 水位比**上次压缩时**涨这么多就压一次 |
|
|
235
|
-
| 增量压缩·保留最近 | **10** 点 | 最近这段不动。20→10 ≈ 上下文能装两倍 |
|
|
236
|
-
|
|
237
|
-
★ **诚实边界**:① 仍然有损(被压那段变摘要),只是损失小得多;
|
|
238
|
-
② **20/10 这两个默认值是照用户原话定的,还没在真实长会话上校准** —— 手感不对就调;
|
|
239
|
-
③ 增量没压住、水位仍逼近内核 0.8 时,安全网那条会兜底全压一次。
|
|
240
|
-
|
|
241
|
-
### ★★★ 你手动敲的命令**立刻执行**(D40,2026-10-09 改)
|
|
242
|
-
|
|
243
|
-
★ 这一条是用户报的缺陷改出来的,原话:*"为什么这些指令都不是马上执行呢?…
|
|
244
|
-
到水位的时候主动按压缩不能进行,比较危险。"*
|
|
245
|
-
|
|
246
|
-
**根因**(读内核 `dsh-agent-loop` 真源码):`agent/status` **只在状态真的变化时才发**
|
|
247
|
-
(`:790-798` `if (status !== previousStatus) …emit("agent/status")`)。
|
|
248
|
-
我们原来排了队等这个事件 —— 但你**空闲时**敲命令,会话本来就已经 idle,
|
|
249
|
-
**那个事件永远不会再发** ⇒ 命令永远不执行。
|
|
250
|
-
|
|
251
|
-
**现在的行为**:
|
|
252
|
-
|
|
253
|
-
| 命令 | 反应 |
|
|
254
|
-
|---|---|
|
|
255
|
-
| `/ledger compact` | **当场全量压缩**(照官方 `/compact` 的做法直接 await `compactNow`)。★ 只有内核真的报 `busy`(回合正开着)才退回队列 |
|
|
256
|
-
| `/ledger light` | **当场轻量压缩**(只压最旧一段、保留最近一大块)。★ 做法不同:它**立一个"下一步强制压"的旗子 + `agent.steer()` 唤醒一轮**,真正压缩在那一轮的 pre-step 里做 —— 因为增量用的 `compactRegion` 硬编了 `owner:"current-turn"`、**要求回合开着**,而命令是在 idle 跑的 |
|
|
257
|
-
| `/ledger init` / `update` / `archive` | **当场唤醒一轮**让 AI 马上做(`agent.steer()`);失败才退回注入通道 |
|
|
258
|
-
| `/ledger status` / `show` / `log` | 本来就是只读,立即返回 |
|
|
259
|
-
|
|
260
|
-
#### ★★★ 两个压缩命令的区别(别选错)
|
|
261
|
-
|
|
262
|
-
| | `/ledger compact` | `/ledger light` |
|
|
263
|
-
|---|---|---|
|
|
264
|
-
| 性质 | **全量**(安全网那条) | **轻量/增量**(常态那条) |
|
|
265
|
-
| 压多少 | 几乎全压,只留摘要 + 指针 | **只压最旧一段**,保留最近 `deltaKeepPoints` 点 |
|
|
266
|
-
| 走哪个 API | `compactNow`(`owner: null`,要求 idle) | `compactRegion`(`owner: "current-turn"`,要求回合开着) |
|
|
267
|
-
| 什么时候用 | 水位很高、想彻底收一次 | 想顺手把水位降一点、又不丢新近细节 |
|
|
268
|
-
|
|
269
|
-
★ 两者都会先抓**逐字胶囊**(你的原话 / 报错原文 / 改过哪些文件 / 去哪重读),压完注回来。
|
|
270
|
-
|
|
271
|
-
#### ★★ 一个容易漏掉的地方:`/ledger light` 靠"旗子"过桥
|
|
272
|
-
|
|
273
|
-
命令在 **idle** 跑,而 `compactRegion` **要求回合开着** —— 中间隔着一次唤醒。所以:
|
|
274
|
-
命令 → 立旗子 → `agent.steer()` 唤醒一轮 → 那一轮的 `pre-step` 看到旗子 → 压。
|
|
275
|
-
|
|
276
|
-
★ 这里有个**我自己当场发现的坑**(D40 那个坑的翻版):`pre-step` 里那段压缩原本
|
|
277
|
-
由 `text !== undefined` 把关("本轮有东西要注入才动手")。而你按 `light` 唤醒的那一轮,
|
|
278
|
-
**可能没有任何别的东西要注入** ⇒ `text` 是 `undefined` ⇒ 压缩段**整段跳过** ⇒
|
|
279
|
-
压缩**永远不会发生**,可命令已经告诉你"立刻执行"。
|
|
280
|
-
⇒ 现在条件是 `(enableDelta && text !== undefined) || forcedLight`,
|
|
281
|
-
且**手动那条若没压成会如实告诉你**,绝不静默。
|
|
282
|
-
(自检 30h 专门造出 `text === undefined` 的场面来钉这一条 —— 因为实测发现
|
|
283
|
-
「把 `|| forcedLight` 拆掉后 30g 照样绿」,那半个条件原先**根本没被任何用例测到**。)
|
|
284
|
-
|
|
285
|
-
★ **顺带修掉一个方向性错误**:原来"压缩前强制落盘"那道闸门也拦**你手动按的**压缩。
|
|
286
|
-
于是水位高时你按下去会被拦回来 —— 而**拦下自己这次安全的压缩**(有逐字胶囊),
|
|
287
|
-
等于把机会**让给内核 0.8 那次抓不到胶囊的压缩**,方向反了。
|
|
288
|
-
现在两条旁路:**人手动发起的不拦**(你按下就是授权,`light` 同样适用)、**水位 ≥75% 不拦**。
|
|
289
|
-
两条都会在返回值里**如实标注**,不假装合规。
|
|
290
|
-
★ 注意:AI 自己调的 `ledger_compact` 工具**仍然**受闸门约束 —— 那才是闸门要约束的对象。
|
|
291
|
-
|
|
292
|
-
#### ⚠ 一个被修掉的缺陷:命令补全里**看不到** `compact`
|
|
293
|
-
|
|
294
|
-
★ 你报的原话:*"你没有给我可以手动点击让模型压缩的命令啊,也就是这个命令没有进入选择里面"*。
|
|
295
|
-
|
|
296
|
-
**根因**:弹窗卡片读的是 `LEDGER_SUBCOMMANDS` 那张表(**本来就有 compact**),
|
|
297
|
-
但 `input.hint` 与命令 `description` 是**手抄**的字符串:
|
|
298
|
-
`"[回车=选择卡片 | status | show | init | update | archive | log]"` —— **抄漏了 compact**。
|
|
299
|
-
⇒ 你敲 `/ledger` 时看到的补全提示里没有它,自然以为没这个功能。
|
|
300
|
-
|
|
301
|
-
**修法与防回归**:两处都改成**由 `LEDGER_SUBCOMMANDS` 生成**(单一来源),
|
|
302
|
-
并加了断言"**每一个**子命令都必须出现在提示与描述里"。
|
|
303
|
-
★ 这条断言经过可失败验证(把提示改回手抄版 ⇒ 必红)。
|
|
304
|
-
|
|
305
|
-
### ★★★ 取 `compaction` 服务:只能用 `agentPresets.serviceFor`(D39,踩过两次)
|
|
306
|
-
|
|
307
|
-
这一段值得单列,因为**取不到时的表现是静默的**(不报错、只是永远不压)。
|
|
308
|
-
|
|
309
|
-
本 profile 里 `compaction` 挂在 **preset 的 `isolate` 组**内
|
|
310
|
-
(`dsh-web-app/presets/standard.patch.yml`,`isolate: {compaction: true, toolResultPruner: true}`),
|
|
311
|
-
host 平面那份被 `disabled: true` 关掉。
|
|
312
|
-
`isolate` = `LocalRealm` ⇒ 服务被隔离在 **preset 子树**里 ⇒
|
|
313
|
-
**外层 `ctx.get("compaction")` 与 `agent.ctx.get("compaction")` 都拿不到**
|
|
314
|
-
(agent 的 ctx 在子树**之外**)。
|
|
315
|
-
|
|
316
|
-
正确取法(**内核自己的做法**,实现在
|
|
317
|
-
`dsh-agent-preset-registry/lib/types/mount.js` 的 `serviceForAgent()`):
|
|
318
|
-
|
|
319
|
-
```js
|
|
320
|
-
ctx.get("agentPresets")?.serviceFor(agent, "compaction") // ← 正路
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
它经 agent 的 **scope parent** 找到该 agent 保留的 preset revision,
|
|
324
|
-
再用 `withinFiber(impl.fiber, mount.fiber)` 取出子树里的实现。
|
|
325
|
-
内核真实用例:`dsh-api-session-controller` 的
|
|
326
|
-
`presets?.serviceFor(live, "skills") ?? this.ctx.get("skills")`。
|
|
327
|
-
|
|
328
|
-
★ 本插件按 **① `serviceFor` → ② `ctx.get` → ③ `agent.ctx.get`** 的顺序取
|
|
329
|
-
(②③ 是给"compaction 挂在 host 平面"的其它部署留的兜底)。
|
|
330
|
-
★ **怎么自查**:`ledger_status` 的返回里有
|
|
331
|
-
`compaction: { reachable, via, hint }` —— `via` 会**点名**走的是哪条路。
|
|
332
|
-
够不着时它会说明原因,不再是你对着"功能配好了却不工作"发呆。
|
|
333
|
-
|
|
334
|
-
### 指针清单 `.dsh-ledger.json`
|
|
335
|
-
|
|
336
|
-
放在项目根、纳入 Git。**这是插件唯一"知道读哪个文件"的途径**:
|
|
337
|
-
|
|
338
|
-
```json
|
|
339
|
-
{
|
|
340
|
-
"version": 1,
|
|
341
|
-
"active": "LEDGER.md",
|
|
342
|
-
"decisions": "DECISIONS.md",
|
|
343
|
-
"archiveDir": "_ledger-archive",
|
|
344
|
-
"maxActiveLines": 300,
|
|
345
|
-
"autoGitAdd": true,
|
|
346
|
-
"archiveWeekMode": "month",
|
|
347
|
-
"createdAt": "2026-10-08 03:20",
|
|
348
|
-
"updatedAt": "2026-10-08 03:20",
|
|
349
|
-
"revision": 3,
|
|
350
|
-
"logDir": "_ledger-log",
|
|
351
|
-
"logLevels": { "L0": true, "L1": true, "L2": true, "L3": false, "L4": true }
|
|
352
|
-
}
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
`logDir` / `logLevels` 是**可选**的:没登记 `logDir` 就不启用日志,插件**不会**往项目里
|
|
356
|
-
擅自建目录(`ledger_log` / `ledger_log_write` 会软失败并告诉你先初始化)。
|
|
357
|
-
★ **`logLevels` 里缺 `L4` 时按默认(开)处理** —— 所以本机**已存在**的老清单
|
|
358
|
-
(它们只写了 L0–L3)升级后会自动开始记用户消息,不需要手工补字段。
|
|
359
|
-
`revision` 由插件自动维护(每写一次 +1,归档也算),用来做多会话保护。
|
|
360
|
-
|
|
361
|
-
### 从容器工作区开会话时:用 `root` 点名子项目
|
|
362
|
-
|
|
363
|
-
本插件的项目根取**会话工作目录**(`session.header.cwd`)。如果你像平常那样从
|
|
364
|
-
一个**容器工作区**(装着多个项目、自己没登记台账的目录)里开一个会话,那么所有台账工具默认都指向**容器根** ——
|
|
365
|
-
而容器根恰恰是**禁止初始化**的那个目录,于是插件对它最主要的那些项目**全都不可用**。
|
|
366
|
-
|
|
367
|
-
所以每个工具都接受一个可选 `root`:
|
|
368
|
-
|
|
369
|
-
```
|
|
370
|
-
ledger_status { root: "20-DSH插件/04-其余插件/10.项目台账" } # 相对路径按会话 cwd 解析
|
|
371
|
-
ledger_init { root: "D:/.../10.项目台账", active: "LEDGER.md", ... }
|
|
372
|
-
ledger_log { root: "...", view: "failures" }
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
两条刻意的约束:
|
|
376
|
-
|
|
377
|
-
1. **`root` 不绕过容器闸门** —— 它只回答"哪个目录",不回答"该不该在这里初始化"。
|
|
378
|
-
明确点名容器根要求 `ledger_init`,**仍然被拒**。
|
|
379
|
-
2. **`root` 必须是已存在的目录**,否则抛错。打错一个字母不能变成"静默写到别处",
|
|
380
|
-
更不能变成"在错误的目录里 `git init`"。
|
|
381
|
-
|
|
382
|
-
**发现顺序**(`lib/index.js` 的文件头注释里也写了):
|
|
383
|
-
|
|
384
|
-
1. 有 `.dsh-ledger.json` ⇒ **按它指的文件名走**(AI 自定名的唯一通路)
|
|
385
|
-
2. 没有 ⇒ 用**保守的候选名**猜:只认名字里带 `ledger` / `台账` / `status` / `progress` 的
|
|
386
|
-
3. 都没有 ⇒ 判"未初始化",走需求 ① 的提示路径
|
|
387
|
-
|
|
388
|
-
> ★ 发现顺序 2 刻意**不认** `TODO.md` / `README.md` / `PROJECT.md`。
|
|
389
|
-
> 误认的后果是"把用户的普通文档当台账覆盖",比漏认严重得多。
|
|
390
|
-
|
|
391
|
-
---
|
|
25
|
+
## 三层上下文压缩
|
|
392
26
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
台账回答"现在是什么状态",日志回答"**过去发生过什么**"。两者的取舍完全不同:
|
|
396
|
-
台账要被**读进上下文**所以必须精简,日志不注入所以可以细 —— 但也因此**不能靠"多记点"
|
|
397
|
-
来弥补设计**,必须有档位可选。
|
|
398
|
-
|
|
399
|
-
### 五档粒度(体积是在本机一场真实会话上量的)
|
|
400
|
-
|
|
401
|
-
L0–L3 量的是 `session-fada6e5f`(52.6 小时、2,248 事件、解压 18.46 MB);
|
|
402
|
-
L4 量的是全机 115 个会话(769 条真实用户消息):
|
|
403
|
-
|
|
404
|
-
| 档 | 记什么 | 谁记 | 实测 | 默认 |
|
|
405
|
-
|---|---|---|---|---|
|
|
406
|
-
| **L0 里程碑** | 交付 / 压缩 / 命令完成 / 工作区变更 / 回合边界 | 插件自动 | 9 条 · 1.4 KB(一场会话) | ✅ 开 |
|
|
407
|
-
| **L1 回合摘要** | 要什么 / 做了什么 / 改了哪些 / 卡点 / 下一步 | **AI 写** | ~220 B/回合 | ✅ 开 |
|
|
408
|
-
| **L2 文件流水** | 每个文件的增改读(按路径可去重) | 插件自动 | 31 文件 · 2.0 KB(一场会话) | ✅ 开 |
|
|
409
|
-
| **L3 工具流水** | 每次工具调用一行 + 失败标记 | 插件自动 | 482 行 · 30.8 KB(一场会话) | ❌ **关** |
|
|
410
|
-
| **L4 用户消息存档** | **用户本人说的每一句话** + 会话编号 + 发送时间 | 插件自动 | p50=51 字符/条 | ✅ 开 |
|
|
411
|
-
|
|
412
|
-
**为什么 L3 默认关**:它随工具调用数**线性增长**(这场会话 482 次调用 / 52 小时),
|
|
413
|
-
长期跑会失控。它真正值钱的地方是**失败标记**(这场里 6 次),适合按需打开排查
|
|
414
|
-
"这个 bug 是哪一步引入的"。需要时在清单里把 `logLevels.L3` 改成 `true`。
|
|
415
|
-
|
|
416
|
-
**为什么 L1 必须由 AI 写**:其它四档(L0/L2/L3/L4)全是机器采集的,它们只能告诉你**发生了什么**,
|
|
417
|
-
永远说不出**为什么** —— 那个只能由当事的 AI 写。这是全套设计里唯一花 token 的一档,
|
|
418
|
-
所以刻意做得很小(一条 L1 ≈ 220 B,200 回合的项目也就 44 KB)。
|
|
419
|
-
|
|
420
|
-
### L4:用户消息存档(需求 ⑳)
|
|
421
|
-
|
|
422
|
-
用户的原话是「**自动**记住用户发的**所有**消息……带上**来源的会话编号和发送时间**,
|
|
423
|
-
多会话项目很有用」。逐条对应:
|
|
424
|
-
|
|
425
|
-
| 要求 | 怎么做的 |
|
|
426
|
-
|---|---|
|
|
427
|
-
| 自动 | 挂在与 L0/L2/L3 同一条 `session/event` 上 —— **不花 token、不靠 AI 记得写**。若要靠 AI 每轮调一次工具,那就不叫"自动记住"了 |
|
|
428
|
-
| **所有**消息 | 判据是 `data.source.kind === "user"`。**不能用 `role: "user"`** —— 实测系统注入(`time-context`、`agent-instructions`、`runtime-context`、`skill-catalog`、本插件自己注入的台账…)**全都以 `role:"user"` 落盘**(全局计数:user 769 / time-context 1396 / agent-instructions 274…)。用 role 判会把系统的每一条注入记成"你说的话" |
|
|
429
|
-
| 会话编号 | 放进**结构字段**(`(user:9a3f)`)而不是正文,这样能按编号精确过滤/分组;编号与台账条目行首那个 `[9a3f]` **是同一个** |
|
|
430
|
-
| 发送时间 | 取**事件自己的 `time`**,不是落盘时刻(落盘是去抖的,一批消息会共享同一时间戳、顺序与间隔全丢)。实测 769 条里 `ev.time` **没有一条不是数字** |
|
|
431
|
-
| 不注入上下文 | 与 L0–L3 同规矩。注入的话每轮都要把历史消息再抄一遍进 prompt —— 纯亏钱、还挤占窗口 |
|
|
432
|
-
| 有问题时可以查 | `ledger_log({view:"users"})`,可按 `since`/`until`/`grep` 过滤,返回 `bySession` 汇总 |
|
|
433
|
-
|
|
434
|
-
**一条消息 = 一行**:换行被转义成字面 `\n`。用户消息**经常是多行的**(实测 769 条里
|
|
435
|
-
72 条含换行、8 条换行超过 50 个),原样写进去会把一条消息炸成几十行,破坏整个日志的
|
|
436
|
-
硬不变量。转义而不是丢弃 —— `\n` 可读、可 grep、可还原。
|
|
437
|
-
|
|
438
|
-
**图片等非文本也留痕**:实测出现过 1 个 `image` part(带 attachmentId / 尺寸 / 字节数)。
|
|
439
|
-
只记文字会让"你发过一张图"这件事**彻底消失**,而那往往是那一轮的起点。图本身不落盘
|
|
440
|
-
(尊重"不复制附件"),只留一行 `[图片 image.png 578x431 59KB]`。
|
|
441
|
-
|
|
442
|
-
> ⚠ **L4 默认不在读取档位里。** 它量最大(769 条 vs 工具流水几百条),默认混进
|
|
443
|
-
> `timeline` 会把别的档位刷出屏幕。要看它得显式说 `view:"users"` 或 `levels:["L4"]`。
|
|
444
|
-
|
|
445
|
-
> ⚠ **它是从启用那一刻开始记的,不回溯**。启用之前的对话没有被补写进 L4 ——
|
|
446
|
-
> 那些原话仍在官方会话日志里(`$DSH_HOME/sessions/…`)。这个插件**不去读官方的
|
|
447
|
-
> 会话日志**(它是只读证据链,红线),所以不做回溯补齐。
|
|
448
|
-
|
|
449
|
-
### 采集是怎么做到的(不花一个 token)
|
|
450
|
-
|
|
451
|
-
插件订阅官方自己的落盘事件 `session/event`。这个事件在 `Session.append()` 内部
|
|
452
|
-
**同步**触发,而官方自己的持久化也是订阅它(`dsh-session/lib/index.js` 开头原话:
|
|
453
|
-
*"Persistence is a plugin concern (subscribe to `session/event`, drain on `session/flush`)."*)。
|
|
454
|
-
|
|
455
|
-
⇒ `tool/call`(含参数)、`tool/result`(含结构化字段 `message.isError`)、
|
|
456
|
-
**`user/message`**、交付、压缩、回合边界,插件**全都看得到**,
|
|
457
|
-
不需要内核提供任何额外能力,**也不调用模型**。
|
|
458
|
-
|
|
459
|
-
**失败判据用结构化字段、不用正则**:`message.isError` 实测 490 条结果里
|
|
460
|
-
6 true / 484 false / **0 缺失**。(早期用关键词正则估出"108/481 失败"是**错的尺子** ——
|
|
461
|
-
它匹配到了 `read` 返回的文件正文里的 error 字样。所以这里刻意走字段。)
|
|
462
|
-
|
|
463
|
-
### 目录结构与归档联动
|
|
464
|
-
|
|
465
|
-
```
|
|
466
|
-
_ledger-log/
|
|
467
|
-
2026-10/
|
|
468
|
-
week2/
|
|
469
|
-
L0-milestones.md
|
|
470
|
-
L1-turns.md
|
|
471
|
-
L2-files.md
|
|
472
|
-
L3-tools.md
|
|
473
|
-
L4-user-messages.md
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
期间键(`YYYY-MM/weekN`)与 `ledger_archive` **用的是同一个函数**。所以查
|
|
477
|
-
"2026-10 第 2 周发生了什么",能一次同时拿到**该周的归档台账条目**和**该周的日志** ——
|
|
478
|
-
两条时间线若各算各的分桶,回溯时就必然对不上。
|
|
479
|
-
|
|
480
|
-
### 怎么回溯
|
|
481
|
-
|
|
482
|
-
```
|
|
483
|
-
ledger_log # L0–L3,按时间序,默认 200 条
|
|
484
|
-
ledger_log { view:"users" } # ★ 你说过的话(L4),按会话编号汇总
|
|
485
|
-
ledger_log { view:"users", grep:"压缩" } # 在你说过的话里找关键词
|
|
486
|
-
ledger_log { levels:["L1"] } # 只看回合摘要(带"为什么"的那档)
|
|
487
|
-
ledger_log { view:"files" } # 按路径去重:改动过哪些文件、各几次
|
|
488
|
-
ledger_log { view:"failures" } # 只看失败的工具调用
|
|
489
|
-
ledger_log { since:"2026-10-08", grep:"台账" } # 时间 + 关键词
|
|
490
|
-
ledger_log { limit:50 } # 返回 nextCursor,直接当 since 传回来续读
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
四个刻意的设计:
|
|
494
|
-
|
|
495
|
-
1. **默认返回窗口内最早 N 条,不是最近 N 条** —— 返回的 `nextCursor` 可以当 `since`
|
|
496
|
-
传回来续读。若返回"最近 N 条",一旦超出 `limit`,中间那段就**永远拿不到**了。
|
|
497
|
-
2. **`view:"files"` 天然去重** —— 一场会话 482 行 L3 里可能只有 31 个不同文件。
|
|
498
|
-
3. **失败详情不存副本** —— 插件只记"哪一步失败",报错全文让你回官方会话日志查
|
|
499
|
-
(它本来就有)。存副本 = 一个会无限膨胀的重复品。
|
|
500
|
-
4. **L4 不并进 L0/L1** —— 它们是"机器/agent 的行为"这条轴,用户消息是"**人说的话**"
|
|
501
|
-
另一条轴。混在一起会让两者互相挤占 `limit`,而且读取意图完全不同
|
|
502
|
-
(回溯行为 vs 回溯原话)。
|
|
503
|
-
|
|
504
|
-
### 只追加,永不改写
|
|
505
|
-
|
|
506
|
-
L1/L2/L3/L4 全部是 `appendFileSync`,**没有任何改写或替换路径**。这一点和活跃台账相反
|
|
507
|
-
(活跃台账会被 `replace`、会参与归档),所以日志单独用 `ledger_log_write` 写,
|
|
508
|
-
而**不复用** `ledger_write` —— 共用迟早会有人拿 `replace` 去写日志。
|
|
509
|
-
|
|
510
|
-
> ★ **一行 = 一条**是这个日志的硬不变量(解析、`grep`、`limit`、`nextCursor` 全建立在
|
|
511
|
-
> 它上面)。L1 因此被刻意压成**一行**(`- 时间 (L1) 标题 | 字段 | 字段`)而不是漂亮的
|
|
512
|
-
> `### 标题` + 多行 bullet —— 后者实测会同时坏三件事:`#` 开头的行被当注释跳过导致
|
|
513
|
-
> **整条 L1 读不出来**、每条 bullet 变成一条"没有时间戳的条目"往 timeline 里灌噪音、
|
|
514
|
-
> `limit` 按 bullet 而不是按回合计数。L4 的多行消息出于同一理由被转义成 `\n`。
|
|
515
|
-
|
|
516
|
-
### ⚠ 日志只有**登记过**才在工作(一个实测踩过的自相矛盾)
|
|
517
|
-
|
|
518
|
-
日志目录**必须登记在清单里**(`logDir`)才会采集。**目录存在并不等于已启用** ——
|
|
519
|
-
本插件不因为"看到 `_ledger-log/` 就往下写"。
|
|
520
|
-
|
|
521
|
-
这里修过一个**两个出口给两个答案**的缺陷(2026-10-09 实测):旧版把"猜到的目录"
|
|
522
|
-
也写进 `logRel`,于是 `ledger_status` 报「日志目录 = `_ledger-log`」,
|
|
523
|
-
而 `ledger_log` / `ledger_log_write` / 采集器一律回「还没有启用活动日志」——
|
|
524
|
-
用户按 status 去查必然扑空。现在 `logRel` **只来自清单**(与写入口径单一来源),
|
|
525
|
-
猜到的目录另放 `logDirFoundButNotEnabled` 并**明确说明"有目录但没在采集、怎么打开"**。
|
|
526
|
-
|
|
527
|
-
### ⚠ 另有一条:**采集类**日志只认会话的工作目录(D42)
|
|
528
|
-
|
|
529
|
-
同一个项目里,**不同档位的日志走的不是同一条路**:
|
|
530
|
-
|
|
531
|
-
| 文件 | 谁写的 | 落到哪 |
|
|
27
|
+
| 层 | 触发 | 做什么 |
|
|
532
28
|
|---|---|---|
|
|
533
|
-
|
|
|
534
|
-
|
|
|
535
|
-
|
|
536
|
-
⇒ **如果你的对话是开在容器根 / 上级目录**(那个目录自己没登记台账),
|
|
537
|
-
那么"采集类"那三档**一条都写不出去**,而 `L0-ledger-changes` 与 `L1-turns` 照常工作。
|
|
538
|
-
★ 实测:本项目 `_ledger-log` 里只有前两种,而在**对话开在自己目录里**的项目里
|
|
539
|
-
有完整的五种,含真实的 L4 用户消息。
|
|
540
|
-
|
|
541
|
-
**为什么不"往下找个子项目写"**:采集器**没有**"用户指定根"这个输入,而在容器根下
|
|
542
|
-
猜一个子项目 = 可能把 A 项目的日志写进 B 项目,**比不写更糟**。所以保持"只认 cwd",不猜。
|
|
543
|
-
★ 缓解:容器会话的注入里会**明说**"L4 在这个目录没在工作"并给出出路
|
|
544
|
-
(换到子项目目录开对话,或 `ledger_init({allowContainer:true, gitInit:false})`)。
|
|
545
|
-
|
|
546
|
-
### 与 Git
|
|
547
|
-
|
|
548
|
-
日志与台账同一套规矩:`autoGitAdd` 开着时只 `git add -- <明确路径>`(**绝不 `-A`**)。
|
|
549
|
-
但**只有收尾时刻才 stage**(回合末 / 卸载)—— 去抖落盘每 1.2 秒可能冲一次,
|
|
550
|
-
每次都 spawn 一个 git 进程并重写 index 是纯浪费。
|
|
551
|
-
|
|
552
|
-
### 日志与台账的分工(一句话)
|
|
553
|
-
|
|
554
|
-
| | 注入上下文? | 可改写? | 参与归档? | 谁的 |
|
|
555
|
-
|---|---|---|---|---|
|
|
556
|
-
| 活跃台账 | ✅ 每次会话注入 | ✅ replace | ✅ 超阈值归档 | 项目现状 |
|
|
557
|
-
| 决策记录 | ❌ | ✅ append | ❌ 永久保留 | 长期决策 |
|
|
558
|
-
| 活动日志 | ❌ **永不** | ❌ **只追加** | 按周分桶 | 历史流水 |
|
|
559
|
-
|
|
560
|
-
---
|
|
561
|
-
|
|
562
|
-
## 五、多个对话做同一个项目
|
|
563
|
-
|
|
564
|
-
这在你的会话树里是常态,但**没有会话树也一样会发生** —— 任何"两个对话先后碰同一个项目"
|
|
565
|
-
都会撞上。所以它是插件的责任,不是靠纪律回避的。
|
|
566
|
-
|
|
567
|
-
### 问题是**静默丢数据**,而且**不需要并发**
|
|
568
|
-
|
|
569
|
-
实测:两个会话各自在开头读了一份台账,A 先写、B 后写。
|
|
570
|
-
B 用的是**它自己看过的旧副本**整份重写 ⇒
|
|
571
|
-
|
|
572
|
-
```
|
|
573
|
-
A 的进展:★ 彻底消失
|
|
574
|
-
两边报错:都没有
|
|
575
|
-
并发: 没有(只是两次顺序写)
|
|
576
|
-
```
|
|
29
|
+
| ① 常态 · 增量 | 水位比上次压缩涨约 **20 点** | 只压**最旧一段**,保留最近约 10 点不动 |
|
|
30
|
+
| ② 兜底 · 全量 | 水位到 **70%**(内核默认是 80%) | 全量压缩,但保留**指向所需内容的指针**:用户原话 / 报错原文 / 改过哪些文件 / 去哪重读 |
|
|
31
|
+
| ③ 终局 · 交接 | 你要换会话时 | 交接单让新对话无缝接上 |
|
|
577
32
|
|
|
578
|
-
|
|
33
|
+
围绕这三层有一条铁律:**先记台账,再压缩**。水位提醒发出的那一刻,插件会记住
|
|
34
|
+
「**已经要求它写台账了**」,在它写完之前**不压缩** —— 否则压掉的正是这一轮唯一的那份进展。
|
|
579
35
|
|
|
580
|
-
|
|
36
|
+
## 安装
|
|
581
37
|
|
|
582
|
-
|
|
583
|
-
> 用户指出 —— *"如果版本不一样就拒绝改动的话,那样的话,AI 的工作就会非常麻烦。"*
|
|
584
|
-
> 对:那样 AI 每次撞上都得重读、重新合并、再写,还可能再撞一次,
|
|
585
|
-
> 把**并发问题变成了 AI 的工作量**,而被挡下的写还要人来善后。
|
|
38
|
+
需要 **Node.js ≥ 22.19**。
|
|
586
39
|
|
|
587
|
-
|
|
588
|
-
|
|
40
|
+
```bash
|
|
41
|
+
# 走 npm
|
|
42
|
+
npm install dsh-ledger-memory
|
|
589
43
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
> 下面是**别的会话**写进台账、而这次重写没包含的条目。插件自动留在这里,
|
|
594
|
-
> 合并进正文后可以整段删掉。行首的 `[xxxx]` 是写入者的会话编号。
|
|
595
|
-
|
|
596
|
-
- [aaaa] A 完成了一件事
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
返回值里会如实报告 `preservedEntries` 与 `preservedNote`,
|
|
600
|
-
告诉写的人"**你不需要重写**,但下次维护时把这段并进正文"。
|
|
601
|
-
|
|
602
|
-
### 会话编号:一眼看出"这条是谁写的"
|
|
603
|
-
|
|
604
|
-
条目行首带 `[xxxx]`,取自 `session.header.id` 的**中段前 4 位**
|
|
605
|
-
(id 形如 `session-31798360-22b7-…`,前缀都一样,取中段才有区分度)。
|
|
606
|
-
|
|
607
|
-
> ★ **编号本身防不住丢数据** —— 我实测验证过:编号只改变**写进去的内容**,
|
|
608
|
-
> 不改变"整份覆盖"这个**动作**。所以编号与自动保留**互补,不是替代**。
|
|
609
|
-
|
|
610
|
-
### 查"谁改过这份台账":`ledger_log({ view: "audit" })`
|
|
611
|
-
|
|
612
|
-
每次写台账、**以及每次归档**都会往 `_ledger-log/<期间>/L0-ledger-changes.md` 追加一行:
|
|
613
|
-
|
|
614
|
-
```
|
|
615
|
-
- 2026-10-08 17:12:05 (ledger-write) [bbbb] active replace → `LEDGER.md` 11 行 · revision→2 · `session-bbbb2222-2222`
|
|
616
|
-
- 2026-10-09 03:08:33 (ledger-archive) [aaaa] active archive → `LEDGER.md` 3 行 · revision→1 · `session-aaaa1111-0001`
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
两种 kind 分开是有用的:**`ledger-archive` 回答"这几条哪去了"**,
|
|
620
|
-
`ledger-write` 回答"谁改的"。混成一个就分不出"台账少了两条"是有人精简、还是有人归档了。
|
|
621
|
-
★ 但 `audit` 视图**两种都收** —— 分开标只是更精确,绝不能让归档从审计里消失。
|
|
622
|
-
|
|
623
|
-
查回来按会话编号汇总:
|
|
624
|
-
|
|
625
|
-
```json
|
|
626
|
-
{
|
|
627
|
-
"view": "audit",
|
|
628
|
-
"matched": 2,
|
|
629
|
-
"bySession": [ { "tag": "aaaa", "count": 1 }, { "tag": "bbbb", "count": 1 } ],
|
|
630
|
-
"changes": [ … ]
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
**这就是「另外一个对话找不到的时候就去查日志」**:台账里的条目没了,
|
|
635
|
-
日志里还有"是哪个会话、什么时候、重写过哪个文件"。
|
|
636
|
-
|
|
637
|
-
> ★ **归档同样涨 revision**(这一点是后来补的):归档**删掉了条目**,
|
|
638
|
-
> 若版本号不动,另一个会话拿着归档前的旧号仍会被判成"当前版本",
|
|
639
|
-
> 于是它那份旧副本整份重写时**不会触发自动保留** —— 保护恰好在最容易丢东西的
|
|
640
|
-
> 那个操作上漏掉。规则是:**一个操作只要改了台账文件,就受"版本号 + 审计"管,
|
|
641
|
-
> 不管它叫 write 还是 archive。**
|
|
642
|
-
|
|
643
|
-
> `audit` 视图会**强制打开 L0**(审计行写在 L0 那个文件里),
|
|
644
|
-
> 否则你传 `levels: ["L1"]` 会读到一个空结果、看起来像"没人改过台账"。
|
|
645
|
-
|
|
646
|
-
### `ifRevision`:什么时候保、什么时候允许删
|
|
647
|
-
|
|
648
|
-
这是整套机制里**最容易做错**的地方 —— 判据是**写的人有没有声明"我基于哪个版本"**:
|
|
649
|
-
|
|
650
|
-
| 你传的 `ifRevision` | 插件行为 |
|
|
651
|
-
|---|---|
|
|
652
|
-
| 传了,且**过期** | **保**:把你会弄丢的条目留在保留区(你确实没见过它们) |
|
|
653
|
-
| 传了,且**是最新** | **不保**:你看过最新内容,**你有权删** |
|
|
654
|
-
| **没传** | **不保**:保持原语义(你写什么就是什么) |
|
|
655
|
-
|
|
656
|
-
★ 为什么必须有后两行:否则 AI **有意精简台账**(把 5 条并成 1 条、删掉过时条目)
|
|
657
|
-
会被插件当成"丢了 4 条"再塞回去 —— **那是跟用户对着干**。
|
|
658
|
-
实测这个错误立刻把 `行数统计` 这类正常用例打红,所以留了反向断言守着。
|
|
659
|
-
|
|
660
|
-
`ledger_read` 与 `ledger_status` 都会返回当前 `revision`;
|
|
661
|
-
注入文案里也会写明"本项目当前修订号是 N,写的时候带上 `ifRevision: N`"。
|
|
662
|
-
|
|
663
|
-
### 与"只追加"的日志配合
|
|
664
|
-
|
|
665
|
-
台账是**可覆盖的当前状态**,日志是**只追加的历史**。丢掉的条目在日志里还在,
|
|
666
|
-
所以两件事一起才成立:**编号回答"谁写的",日志回答"哪去了"**。
|
|
667
|
-
|
|
668
|
-
---
|
|
669
|
-
|
|
670
|
-
## 六、给已有的项目做台账重构
|
|
671
|
-
|
|
672
|
-
需求 ① 原本只管"**全新**项目"。但现实里更常见的是:**项目已经在跑了、也有自己的
|
|
673
|
-
状态文档,只是不符合本插件的计划**。这一节补上那一半。
|
|
674
|
-
|
|
675
|
-
### ★ 先说设计前做的勘察 —— 它推翻了"宽松启发式"
|
|
676
|
-
|
|
677
|
-
我扫了整个容器工作区,想找出"像台账但没登记"的项目。结果是:
|
|
678
|
-
|
|
679
|
-
| 做法 | 结果 |
|
|
680
|
-
|---|---|
|
|
681
|
-
| 用宽松启发式(名字或内容里出现 目标/已完成/下一步) | 匹配到 **24 份** |
|
|
682
|
-
| 逐份核对 | 绝大多数是**误报**:README、CHANGELOG、交接单、调研报告、备份 |
|
|
683
|
-
| 真正像台账的 | **1 份**(上传器 `进度台账-2026-10-01-….md`,249 行) |
|
|
684
|
-
| 而那份该改造吗 | **不该** —— 它开头写着「✅ 已完成…保留下来当这一轮的工作底稿」,是**历史档案** |
|
|
685
|
-
| "像台账但没登记"的 | **0 个** |
|
|
686
|
-
|
|
687
|
-
**而真正"不符合计划"的样本是已登记的那两个项目**(都缺 `revision`;本项目自己的台账
|
|
688
|
-
还超过了自己声明的行数上限)。
|
|
689
|
-
|
|
690
|
-
⇒ 所以**不做**"猜哪个文件是台账、然后改它" —— 猜错的代价是**破坏用户文档**。
|
|
691
|
-
做的是**对着自己的硬约束做合规检测**,把该修的列清楚,再问你要不要修。
|
|
692
|
-
|
|
693
|
-
### 判据(对着四条硬约束 + 清单登记状态)
|
|
694
|
-
|
|
695
|
-
| 检查 | 判定 |
|
|
696
|
-
|---|---|
|
|
697
|
-
| 活跃台账能读到 | 读不到 ⇒ 要 AI 重建内容 |
|
|
698
|
-
| 有独立决策记录 | **没有**,或**只在清单外被猜到**,或**清单登记了但文件不存在** ⇒ 不合规 |
|
|
699
|
-
| 有归档目录 | 同上 |
|
|
700
|
-
| 台账未超自声明上限 | 超了 ⇒ 不合规(**但插件不替你删条目**,见下) |
|
|
701
|
-
| 清单有 `revision` | **只提示,不影响判定** —— 它会自愈 |
|
|
702
|
-
| 启用了活动日志 | **只提示** —— 它是可选功能 |
|
|
703
|
-
|
|
704
|
-
> ★ 后两条是本功能里最容易做错的地方,我一开始就做错了:把"可选的"和"会自愈的"
|
|
705
|
-
> 也塞进 `reasons`,于是**为一个自己会好的字段弹卡片让你拍板**。
|
|
706
|
-
> 这正是你上次否决"版本不一致就拒绝"时说的道理 —— 别把能自动处理的事变成人的工作量。
|
|
707
|
-
>
|
|
708
|
-
> 现在分两个数组:`reasons`(真不符合,触发卡片)与 `notes`(仅提示)。
|
|
709
|
-
|
|
710
|
-
### 修复**只做加法**
|
|
711
|
-
|
|
712
|
-
```
|
|
713
|
-
ledger_conform # 默认 dry-run:只报"将会做什么",一个字不写
|
|
714
|
-
ledger_conform { apply: true } # 才真执行
|
|
715
|
-
```
|
|
716
|
-
|
|
717
|
-
| 它**会**做 | 它**绝不**做 |
|
|
718
|
-
|---|---|
|
|
719
|
-
| 清单补 `revision: 0` | 改写台账正文 |
|
|
720
|
-
| 把**已存在**的决策记录/归档目录**登记进清单**(文件一个字不动) | 删除任何条目 |
|
|
721
|
-
| 创建**空的**决策记录 / 空的归档目录 | 覆盖任何已有文件 |
|
|
722
|
-
| —— | 替你决定"哪些条目过时了" |
|
|
723
|
-
|
|
724
|
-
**超阈值那条特别说明**:台账超限时插件**只报告**(`needsAI`),
|
|
725
|
-
让 AI 用 `ledger_archive` 挑**最旧的已完成条目**移进归档。
|
|
726
|
-
**哪些条目还有用是判断,不是流程** —— 插件不碰。
|
|
727
|
-
|
|
728
|
-
### 卡片
|
|
729
|
-
|
|
730
|
-
第一轮结束时会检测。**不合规**才弹:
|
|
731
|
-
|
|
732
|
-
```
|
|
733
|
-
项目台账
|
|
734
|
-
这个项目的台账不符合本插件的计划:…。要不要现在改成符合的?
|
|
735
|
-
○ 修复台账(只补缺的)
|
|
736
|
-
○ 先给我看要改什么
|
|
737
|
-
○ 不用改
|
|
44
|
+
# 或走 DSH 自己的插件命令
|
|
45
|
+
dsh plugin add dsh-ledger-memory
|
|
738
46
|
```
|
|
739
47
|
|
|
740
|
-
|
|
48
|
+
装完**重启客户端**才生效 —— 内核对已加载的插件不会因文件改动自动重载。
|
|
741
49
|
|
|
742
|
-
|
|
50
|
+
## 用法
|
|
743
51
|
|
|
744
|
-
|
|
745
|
-
而且 `git init` 会把所有子项目卷进一个错误的父仓库。
|
|
746
|
-
实测第一版把容器根也报成"没有台账"并建议 `ledger_init`,那是**错的引导**。
|
|
747
|
-
现在会明确说"这是容器工作区,先问清是哪个子项目"。
|
|
748
|
-
2. **保守命名** —— `README.md` / `CHANGELOG.md` / `交接单.md` **不认**成台账。
|
|
749
|
-
特别地:`Gelheim` 的 README 里就有「已完成 / 下一步」结构,
|
|
750
|
-
按**内容**判会把面向人的 README 认成台账并去"改造"它。
|
|
52
|
+
不用配置什么。打开一个项目目录开始工作,插件会:
|
|
751
53
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
**你问得对,而且这一半原本是缺的** —— 我只做了"怎么写",没做"你怎么知道"。
|
|
757
|
-
补之前的实际情况是:
|
|
758
|
-
|
|
759
|
-
| 通道 | 补之前 |
|
|
760
|
-
|---|---|
|
|
761
|
-
| 工具返回值(`renderResult` → `textOut`) | **只发给 AI,你看不到** |
|
|
762
|
-
| 审计日志(`L0-ledger-changes.md`) | 记了,但要你**知道去查、还主动敲命令** —— 那是考古,不是"知道" |
|
|
763
|
-
| 回合末卡片 | **恰好在你最该收到确认的那一刻被抑制**(见下) |
|
|
764
|
-
|
|
765
|
-
> ★ 第三条是最隐蔽的:你选「只记录台账」⇒ AI 去写 ⇒ 那一轮结束时卡片被
|
|
766
|
-
> `skipAutoOffer` **故意抑制**(防止"选完→干活→又被问"的套娃)。
|
|
767
|
-
> 也就是说:**确认动作本身把确认消息也一起挡掉了。**
|
|
768
|
-
|
|
769
|
-
### 现在是两条路
|
|
54
|
+
1. 发现这里**还没有台账** ⇒ 注入一句提示,让 AI **来问你**要不要初始化(它不会自己动手);
|
|
55
|
+
2. 你同意后,AI 按这个项目的类型**设计一套台账结构**并登记;
|
|
56
|
+
3. 之后每个会话自动加载、每轮结束自动回写、超长自动归档。
|
|
770
57
|
|
|
771
|
-
|
|
58
|
+
### 手动命令
|
|
772
59
|
|
|
773
|
-
|
|
774
|
-
> 而回执原本**只挂在卡片上** —— 卡片一关,这一节讲的整个功能就**永远送不出去**了。
|
|
775
|
-
> 是自检情形 22 当场全线转红才发现的。⇒ 改走注入:给 AI 一段指令,
|
|
776
|
-
> 让它**在回复里用一句话告诉你**,并明确写着
|
|
777
|
-
> **"这不是让你停下来提问:继续手上的工作"**。
|
|
778
|
-
|
|
779
|
-
注入给 AI 的那段长这样(它据此转述给你):
|
|
60
|
+
在对话里敲 `/ledger`(不带参数会弹一张可点的卡片),或用子命令:
|
|
780
61
|
|
|
781
62
|
```
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
63
|
+
/ledger status 看状态(有没有台账、用的哪个文件、几行、Git、日志开关)
|
|
64
|
+
/ledger show 把当前活跃台账全文打出来
|
|
65
|
+
/ledger init 初始化台账(由 AI 为这个项目设计一套结构)
|
|
66
|
+
/ledger update 让 AI 把本次会话的进展写回台账
|
|
67
|
+
/ledger archive 把最旧的已完成条目移进归档目录
|
|
68
|
+
/ledger compact 立刻做一次全量压缩(保留指针)
|
|
69
|
+
/ledger light 轻量压缩
|
|
70
|
+
/ledger log 查活动日志
|
|
71
|
+
/ledger handoff 写一份交接单
|
|
790
72
|
```
|
|
791
73
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
- **不弹卡、不等答复** —— 你明确要求「工作不能被影响」,所以它只借 AI 的一句回复捎带。
|
|
795
|
-
- **编号与台账条目一致** —— 括号里是 `[abcd]`,正是条目行首那个编号,
|
|
796
|
-
所以你看到后能**直接在台账里对号入座**。用原始 session id 会对不上。
|
|
797
|
-
(这里修过一次:卡片原来说「会话 s22a」、条目写 `[abcd]`,等于给了个查不到的东西。)
|
|
798
|
-
- **只告知一次**:攒着的回执在被注入时消费掉,不会每步都念一遍。
|
|
799
|
-
- **★ 必须是"追加"而不是"没有别的注入时才发"** —— 我第一版写成后者,结果回执被**饿死**:
|
|
800
|
-
第一次 pre-step 必然注入台账全文,于是回执永远轮不到。那种"偶尔才送达"的确认等于没有确认。
|
|
74
|
+
### 设置面板
|
|
801
75
|
|
|
802
|
-
|
|
76
|
+
**设置 → 项目台账(台账 / 压缩 / 日志)**,可调十二项:回合末卡片开关、高保真压缩水位、
|
|
77
|
+
增量压缩的涨幅与保留量、活动日志五档开关、活跃台账行数上限、变更登记条数、水位提醒档位等。
|
|
803
78
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
> 完整记录:让 AI 调 `ledger_log`(`view:"audit"`)。
|
|
809
|
-
```
|
|
79
|
+
> 一条要知道的语义:`maxActiveLines` / `changeRegisterMax` / `logLevels` **以每个项目
|
|
80
|
+
> 自己清单里的值为准**(日志是只追加的证据,全局设置改不动它)。
|
|
81
|
+
> 面板里这三项是**新项目初始化时写进去的默认值**。
|
|
810
82
|
|
|
811
|
-
|
|
812
|
-
> 两条路互补,才叫"你总能知道"。
|
|
813
|
-
|
|
814
|
-
### 为什么是"回执"而不是"每次写都弹一张卡"
|
|
815
|
-
|
|
816
|
-
每次写台账都弹卡片会**把确认变成噪音** —— 你会开始无脑点掉它,那比不告知更糟。
|
|
817
|
-
所以:**改动照常静默发生(不打断工作),但在下一次自然停顿时告诉你**。
|
|
818
|
-
这与你最初对需求 ② 的要求一致 ——「不要打断工作」。
|
|
819
|
-
|
|
820
|
-
---
|
|
821
|
-
|
|
822
|
-
## 七之一、台账**自己**记着"我什么时候被谁改过"(需求 ①)
|
|
823
|
-
|
|
824
|
-
### 你提的问题
|
|
825
|
-
|
|
826
|
-
> *"他现在这里有改动的话,必须查日志才能看得出来,我觉得可以考虑多登记一点,
|
|
827
|
-
> 比如说某个执行是什么时间点进行的,然后什么时间点进行追加的。"*
|
|
828
|
-
|
|
829
|
-
**问题说得很准**:改动确实都进了活动日志,但**日志是不注入的** ——
|
|
830
|
-
想知道"这份台账什么时候被谁改过",得先知道日志在哪、再去查一遍。
|
|
831
|
-
而且台账一旦被**单独复制走 / 发给别人**,时间线就**不在它身边**了。
|
|
832
|
-
|
|
833
|
-
### 做法:台账末尾多一张小表,由插件自动维护
|
|
834
|
-
|
|
835
|
-
```markdown
|
|
836
|
-
## 变更登记(插件自动维护)
|
|
837
|
-
|
|
838
|
-
> 这张表由插件**自动**维护:每次写台账 / 归档追加一行,只留最近 12 条。
|
|
839
|
-
> 完整历史在活动日志的 `L0-ledger-changes.md`(用 `ledger_log({view:"audit"})` 查)。
|
|
840
|
-
|
|
841
|
-
- 2026-10-09 10:54:18 (archive) [beef] -1 行 → 3 行 · revision→3
|
|
842
|
-
- 2026-10-09 10:54:17 (replace) [beef] +0 行 → 4 行 · revision→2
|
|
843
|
-
```
|
|
83
|
+
## 十二个工具
|
|
844
84
|
|
|
845
|
-
|
|
85
|
+
AI 在对话里调用,你也可以让它调用:
|
|
846
86
|
|
|
847
|
-
|
|
|
87
|
+
| 工具 | 作用 |
|
|
848
88
|
|---|---|
|
|
849
|
-
| `
|
|
850
|
-
| `
|
|
851
|
-
| `
|
|
852
|
-
|
|
|
853
|
-
| `
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
写入的返回值里有两个数:
|
|
868
|
-
|
|
869
|
-
- `lines` —— **落盘后的真实行数**(含登记区)。**行数闸门看的是它**,
|
|
870
|
-
否则"正文没超、加上附注超了"会被放过去,台账照样被撑爆。
|
|
871
|
-
- `changedBy` —— **正文**的增减(登记区不参与)。
|
|
872
|
-
不分开的话,每次写入都恒定 +十几行,那个数字就彻底没意义了。
|
|
873
|
-
|
|
874
|
-
⇒ 顺序上,登记区必须**先合并、后过闸门**。
|
|
875
|
-
|
|
876
|
-
### ★★ 实现时撞到并修掉的两个真 bug(都是自测抓出来的,**不是想出来的**)
|
|
877
|
-
|
|
878
|
-
1. **`append` 会静默丢内容**:登记区在文件末尾,而 `append` 原本是"接在文件末尾"
|
|
879
|
-
⇒ 新内容落在登记区**之后**,紧接着被"剥掉登记区重建"这一步**整段吃掉** ——
|
|
880
|
-
而调用**返回 `ok`**、行数也对。**这是真丢数据**。
|
|
881
|
-
修法:`append` 先剥掉登记区,往**正文末尾**追加,登记区最后由插件重建。
|
|
882
|
-
(自测 25d 守着这一条。)
|
|
883
|
-
2. **登记表永远只有 1 行**:新内容里**不含**旧登记(AI 不抄、append 又被剥掉),
|
|
884
|
-
于是每次都算出"这是第一条" —— 连写 20 次只剩 1 行,**需求 ① 等于没做**。
|
|
885
|
-
修法:旧条目必须从**磁盘上那份**捞回来(`withChangeRegister(text, line, priorText)`)。
|
|
886
|
-
(自测 25c 守着:写 20 次必须留满 12 条。)
|
|
887
|
-
|
|
888
|
-
### 顺带修掉的一个隐患
|
|
889
|
-
|
|
890
|
-
登记行的形状与"条目"**完全一样**(都是 `- ` 开头)。而多会话保护要判断
|
|
891
|
-
"哪些条目会被这次重写弄丢" —— 不剥掉登记区的话,旧文件里那十几条登记会被
|
|
892
|
-
**统统算成"要丢的条目"**,于是每次写入都**虚报**"保住了 N 条",
|
|
893
|
-
还把一堆时间戳复制进「其他会话的进展」保留区。
|
|
894
|
-
⇒ 比对前先剥掉它:**它是插件写的附注,不是 AI 写的进展。**
|
|
895
|
-
|
|
896
|
-
---
|
|
897
|
-
|
|
898
|
-
## 七之二、70% 高保真压缩(需求 ㉑)
|
|
899
|
-
|
|
900
|
-
### 你提的两句话,字面照做
|
|
901
|
-
|
|
902
|
-
> *"因为我们拦不住官方内核的自动压缩,所以说我们尽量在 70% 左右就把我们的压缩做好"*
|
|
903
|
-
> *"AI 执行完我们插件的功能之后,要继续工作,工作是不能被影响的"*
|
|
904
|
-
|
|
905
|
-
⇒ 所以这一档的动作是:**固化了就继续干**,压缩由**插件自己**在回合末做掉,
|
|
906
|
-
AI 不需要停、不需要问、也不需要为此准备什么。
|
|
907
|
-
|
|
908
|
-
### ★ 先说清楚我**能**做什么、**拦不住**什么
|
|
909
|
-
|
|
910
|
-
我把内核的压缩实现从 `app.asar` 里**解出来读过**(`dsh-compaction-basic`),不是猜的:
|
|
911
|
-
|
|
912
|
-
| | 事实 |
|
|
89
|
+
| `ledger_status` | 报告这个项目的台账状态(只读) |
|
|
90
|
+
| `ledger_read` | 读台账 / 决策记录 / 归档 |
|
|
91
|
+
| `ledger_write` | 写活跃台账或决策记录 |
|
|
92
|
+
| `ledger_init` | 登记台账结构(四条硬约束:一个活跃台账 / 一份独立决策记录 / 一个只追加归档目录 / 行数上限) |
|
|
93
|
+
| `ledger_archive` | 把旧条目移进只追加的归档 |
|
|
94
|
+
| `ledger_conform` | 检查一个既有项目的台账是否符合预期,能修的**只做加法** |
|
|
95
|
+
| `ledger_note` | 写一份**问题记录**(非项目场景,有终点的那种) |
|
|
96
|
+
| `ledger_log` | 查活动日志(五档,**从不注入上下文**) |
|
|
97
|
+
| `ledger_log_write` | 追加一条回合小结 |
|
|
98
|
+
| `ledger_handoff` | 写交接单,供下一个会话接手 |
|
|
99
|
+
| `ledger_compact` | 请求一次高保真压缩 |
|
|
100
|
+
| `ledger_checkpoint` | 把这一刻的选择交给用户(记台账 / 记+压缩 / 记+交接 / 跳过) |
|
|
101
|
+
|
|
102
|
+
## 活动日志(五档)
|
|
103
|
+
|
|
104
|
+
挂在你为项目登记的日志目录下,**只追加、永不改写**,且**从不注入上下文**(按需查):
|
|
105
|
+
|
|
106
|
+
| 档 | 内容 |
|
|
913
107
|
|---|---|
|
|
914
|
-
|
|
|
915
|
-
|
|
|
916
|
-
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
② 压缩后把一份**逐字胶囊**重新注入(官方与社区都认的 rehydration);
|
|
920
|
-
③ 保证官方 80% 压下来时,**确定性副本已经存在**。
|
|
921
|
-
|
|
922
|
-
**★ 拦不住的**(必须说清,不糊弄):
|
|
923
|
-
- 内核 80% 那次压缩是它**自己挂的 `agent/pre-step`**,插件**无法取消**;
|
|
924
|
-
- **替换不了**它的摘要器 —— 运行期去改 `ctx.get("compaction").summarize` 属于猴补丁,
|
|
925
|
-
会踩到"每次现取 `this.summarize`"的实现细节,内核一改版就**静默坏掉**。
|
|
926
|
-
|
|
927
|
-
⇒ 所以这个功能的定位**不是"更好的摘要"**,而是调研里唯一被反复验证的那一招:
|
|
928
|
-
**摘要 + 一份逐字、可重读的副本**。摘要是"大致记得",磁盘与逐字副本是**确定性**的。
|
|
929
|
-
|
|
930
|
-
### 胶囊里装什么(逐字 vs 摘要)
|
|
931
|
-
|
|
932
|
-
| 段 | 内容 | 为什么 |
|
|
933
|
-
|---|---|---|
|
|
934
|
-
| **用户原话** | 最近 40 条,**逐字** | 你的**约束与纠正**衰减最厉害:社区实测软性约束 +50 pts、硬性规范只有 +6(**8.3×**);Anthropic 自家实测"附录里的具体数字 **0/3** 保留" |
|
|
935
|
-
| **失败与报错原文** | 最近 20 条,**逐字** | 报错字符串差一个字符就查不出来 |
|
|
936
|
-
| **器物索引** | 改过哪些文件(路径 + 动作 + 时间),**结构化不摘要** | "哪些文件被改过"是**跨厂商最弱项**(评测 2.19–2.45 / 5),摘要救不回来 |
|
|
937
|
-
| **可重读的路径** | 台账 / 决策 / 归档 / 日志的**绝对路径** | 关键不是"记得",是**知道去哪重读** —— 重读是确定性的 |
|
|
938
|
-
|
|
939
|
-
### 一个刻意的结构决定
|
|
940
|
-
|
|
941
|
-
**胶囊的采集器独立于日志采集器,而且无条件创建。**
|
|
942
|
-
|
|
943
|
-
日志是**可选**的(没登记 `logDir` 就不采集),而高保真是**每个会话都需要**的兜底。
|
|
944
|
-
实测 **104/112 个会话**开在容器根、**没有日志** —— 若把胶囊挂进日志采集器,
|
|
945
|
-
它们就完全没有高保真能力。(第一版我真是这么写的,探针一撞就露了。)
|
|
946
|
-
|
|
947
|
-
### 只压一次
|
|
948
|
-
|
|
949
|
-
`hfCompacted` 每会话**只压一次**。反复压 = summary-of-summary 退化
|
|
950
|
-
(Codex 为此重写过核心代码,官方警告 *"multiple compactions can cause the model to be
|
|
951
|
-
less accurate"*)。
|
|
952
|
-
⇒ 所以掉回线下**不**重新武装这一条 —— 与水位提醒(每档可重发)刻意不同。
|
|
953
|
-
|
|
954
|
-
### ★★ 胶囊现在会**落盘**(2026-10-10,D45)
|
|
955
|
-
|
|
956
|
-
**改前的真缺口**(查出来的,不是推测):`capsuleSnapshot` 全部 10 处用法里,
|
|
957
|
-
**与写盘有关的 0 处** ⇒ 胶囊只活在内存 Map:**进程一重启就没了、被压缩消费掉也没了**。
|
|
958
|
-
而它恰恰是最该留存的东西 —— 尤其**器物索引**那一节,正是上表里
|
|
959
|
-
"跨厂商最弱项(2.19–2.45 / 5)"的那一维。
|
|
960
|
-
|
|
961
|
-
**现在每次压缩都会存一份原件**:
|
|
962
|
-
|
|
963
|
-
```
|
|
964
|
-
<logDir>/<YYYY-MM>/weekN/capsules/<时间戳>-<会话tag>.md
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
四个刻意的决定:① **一个胶囊一个文件**(日志的硬不变量是"一行 = 一条",而胶囊是多行块;
|
|
968
|
-
一文件一份 ⇒ 文件名即时间线、**可 diff**、**永不覆写**);② 两条压缩路径共用一个入口
|
|
969
|
-
(`captureCapsuleBeforeCompact`),不会分叉;③ **只在启用了活动日志时写**(守住
|
|
970
|
-
"没登记就不往项目里写任何东西"的承诺),没启用时**如实说没存**、不假装成功;
|
|
971
|
-
④ 落盘失败**不挡压缩**。
|
|
972
|
-
|
|
973
|
-
★ 它还有一个副作用是**抗"压缩链"失真**:胶囊本来就是**带外**生成的(来自我们自己的
|
|
974
|
-
累积 Map,不经过摘要器)⇒ 落盘之后,**下一次压缩读的是原件**,而不是上次摘要的摘要。
|
|
975
|
-
|
|
976
|
-
---
|
|
977
|
-
|
|
978
|
-
## 七之三、★ 让**新对话无缝衔接**:交接单(2026-10-10,D46)
|
|
979
|
-
|
|
980
|
-
这是"三层策略"的**第三层**。前两层(每 20 点分层筛选 / 70% 全量压缩)解决的是
|
|
981
|
-
**同一个会话别忘**;这一层解决的是**换个会话也能接上**。
|
|
982
|
-
|
|
983
|
-
> 你的战略判断(我把它记成了项目的总纲):
|
|
984
|
-
> *"对于大多数的日常工作,甚至是核心工作而言,并不会去触碰模型上限,
|
|
985
|
-
> 而他们更看重的是**模型下限** —— 让一个模型能够摆脱上下文的约束,持续稳定的、
|
|
986
|
-
> 准确的、可回溯、可追查的推进工作,要比让模型学会如何工作要更好。"*
|
|
987
|
-
|
|
988
|
-
### 改之前,这条路是**断的**(查出来的)
|
|
989
|
-
|
|
990
|
-
- `renderCapsule` 里那行"上一次交接单"读的是 `opts?.handoffRel` ——
|
|
991
|
-
一个**全文件没有任何地方传**的参数 ⇒ **死代码,从来没显示过**。
|
|
992
|
-
- 回合末卡片只对 AI 说*"建议放在项目里的 docs/ 或交接单目录"* ⇒ **位置模糊**。
|
|
993
|
-
于是同行:① 同一项目两次交接落在两地;② **新会话不知道去哪找**。
|
|
994
|
-
|
|
995
|
-
★ **根因不是"忘了写一行",而是"做成了参数"** —— 参数就会有人忘记传。
|
|
996
|
-
⇒ 现在放进 `inspect()` 统一解析,忘不掉。
|
|
997
|
-
|
|
998
|
-
### 现在怎么走(三步闭环)
|
|
999
|
-
|
|
1000
|
-
| 步 | 做什么 | 在哪 |
|
|
1001
|
-
|---|---|---|
|
|
1002
|
-
| ① 约定位置 | `inspect()` 解析出 `handoffRel` / `handoffLatestRel` | 候选 `_handoff` / `handoff` / `交接单` / `交接` / `docs/交接单`;**登记优先**;**只认已存在的目录,不擅自新建** |
|
|
1003
|
-
| ② 写 | 新工具 **`ledger_handoff`**(也可 `/ledger handoff`) | `did` / `state` / `next` / `files` / `blocked` 五段,自带「去哪重读」;**永不覆盖** |
|
|
1004
|
-
| ③ **读** | 把指针放进**主注入的尾巴** | ★ 见下 |
|
|
1005
|
-
|
|
1006
|
-
### ★★ ③ 为什么是本层最容易做错的一步
|
|
1007
|
-
|
|
1008
|
-
**指针必须放进主注入,而不是只放在胶囊里。**
|
|
1009
|
-
|
|
1010
|
-
胶囊是**压缩之后**才注入的(`renderAfterCompaction`),而**新会话的第一轮**走的是主注入。
|
|
1011
|
-
交接单的全部意义就是"新会话接着上次干" —— 放在胶囊里等于
|
|
1012
|
-
**"先压缩一次才告诉你上次干到哪",顺序反了**。
|
|
1013
|
-
|
|
1014
|
-
**而且给的是文件名,不是只给目录。** 只给目录 = 把"挑哪一份"的活推回给模型,
|
|
1015
|
-
而它恰恰没有"哪份最新"的信息。这就是那条已经写进 `DECISIONS.md` 的结论:
|
|
1016
|
-
**恢复必须是「推」给模型的,不能是「等模型来查」的。**
|
|
1017
|
-
|
|
1018
|
-
> 所以新会话开在一个有交接单的项目时,注入里会出现:
|
|
1019
|
-
> `★ 接手这个项目时,先读上一次的交接单:<_handoff/2026-10-10-1530-xxx.md>`
|
|
1020
|
-
> —— 附完整绝对路径,**不需要模型自己去 ls**。
|
|
1021
|
-
|
|
1022
|
-
**"最新一份"按文件名判,不按 mtime**:文件名是 `YYYY-MM-DD-HHmm-主题.md`(日期在前),
|
|
1023
|
-
字典序 = 时间序;而 mtime 会被"复制 / 恢复备份 / 改个错字"搅乱。
|
|
1024
|
-
(自检 36d 专门把 mtime 反过来设,验的就是这条。)
|
|
1025
|
-
|
|
1026
|
-
### 交接单长什么样
|
|
1027
|
-
|
|
1028
|
-
五段 + 一节,**刻意都是"下一场真正要用的东西"**:
|
|
1029
|
-
|
|
1030
|
-
```markdown
|
|
1031
|
-
# 交接单 · <主题>
|
|
1032
|
-
> 写于 <时间> · 会话 `<tag>` | **给下一个会话看的**
|
|
1033
|
-
|
|
1034
|
-
## 这次做了什么 ← 具体到改动,不写"推进了工作"
|
|
1035
|
-
## 当前状态(什么能跑、什么不能、什么是半截的) ← ★ 最省下一场时间的一段
|
|
1036
|
-
## 下一步 ← 具体可执行
|
|
1037
|
-
## 关键文件路径 ← ★ 路径,不是描述(下一场会去打开它们)
|
|
1038
|
-
## 卡点 / 需要人拍板
|
|
1039
|
-
---
|
|
1040
|
-
## 去哪重读(真源,比本文件可靠) ← 台账 / 决策 / 日志 / 归档的绝对路径
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
★ 最后那一节是刻意的:**交接单会过时,真源不会** —— 所以交接单自己要把读者指向真源。
|
|
1044
|
-
|
|
1045
|
-
---
|
|
1046
|
-
|
|
1047
|
-
## 八、不要给"杂事"建台账:问题记录(`ledger_note`)
|
|
1048
|
-
|
|
1049
|
-
### 这个问题的由来(实测数据)
|
|
1050
|
-
|
|
1051
|
-
实测:**112 个会话里 104 个的 cwd 就是容器根** —— 也就是说
|
|
1052
|
-
**绝大多数会话根本不是"某个项目"**,而是"修一个问题 / 调研一件事 / 排查一次故障"。
|
|
1053
|
-
旧版本判出容器后**只说"别在这里初始化"就没有下文了**,等于把 93% 的会话扔下。
|
|
1054
|
-
|
|
1055
|
-
### 判据只有一条:**有没有累积状态**
|
|
1056
|
-
|
|
1057
|
-
| | 用什么 | 结构 | 有归档吗 |
|
|
1058
|
-
|---|---|---|---|
|
|
1059
|
-
| 会一直改、以后要接上、要回顾 | **台账**(`ledger_init`) | 当前目标 / 已完成 / 进行中 / 下一步 / 决策记录 | 有(超上限就归档,默认 300 行) |
|
|
1060
|
-
| 一次性、有终点、只需要"下次别踩" | **问题记录**(`ledger_note`) | 现象 → **根因** → 结论 → 做过什么 | **没有** |
|
|
1061
|
-
|
|
1062
|
-
★ **为什么"问题"不该套台账**:台账的四条硬约束(活跃台账 / 独立决策记录 / 归档目录 /
|
|
1063
|
-
行数上限)全是为**要持续维护的当前状态**设计的。而一个问题有**终点**。
|
|
1064
|
-
硬塞进去会同时坏掉两边 —— 台账被一次性内容稀释,问题本身也不需要
|
|
1065
|
-
"当前目标 / 下一步 / 归档"这些字段。
|
|
1066
|
-
|
|
1067
|
-
### 记在哪:顺着工作区已有的通道,不另起一处
|
|
1068
|
-
|
|
1069
|
-
落到 **`60-对话记录与交接\对话总结\`**、命名 **`YYYY-MM-DD-主题.md`** ——
|
|
1070
|
-
这正是你工作区 `AGENTS.md` §4 已经定下的规矩,目录里 **27 份在用的记录就是这个格式**。
|
|
1071
|
-
新功能**顺着它走**,而不是发明一个新地方。
|
|
1072
|
-
|
|
1073
|
-
```
|
|
1074
|
-
ledger_note {
|
|
1075
|
-
title: "某次故障排查", // ⇒ 2026-10-08-某次故障排查.md
|
|
1076
|
-
kind: "故障", // 问题 | 调研 | 故障 | 决定 | 其他
|
|
1077
|
-
problem: "现象 / 要解决什么",
|
|
1078
|
-
cause: "根因", // ★ 必填项的意义见下
|
|
1079
|
-
conclusion: "结论",
|
|
1080
|
-
actions: "做过什么",
|
|
1081
|
-
files: "涉及的文件",
|
|
1082
|
-
related: "相关记录"
|
|
1083
|
-
}
|
|
1084
|
-
```
|
|
1085
|
-
|
|
1086
|
-
三个刻意的设计:
|
|
1087
|
-
|
|
1088
|
-
1. **`cause`(根因)是重点**。少了这一节,这份记录就退化成一份日记 ——
|
|
1089
|
-
下周再遇到同样的问题还是得从头查一遍。工具描述里把这一点写给 AI 看了。
|
|
1090
|
-
2. **永不覆盖**。同一天同一主题再写会被**拒绝**并告诉你换个更具体的 title,
|
|
1091
|
-
原文件一个字节都不动。记录是证据,不是可编辑文档。
|
|
1092
|
-
3. **只在写的时候才建目录**。`ledger_status` 只是"报出该写哪",绝不创建任何目录。
|
|
108
|
+
| L0 | 台账变更的里程碑 |
|
|
109
|
+
| L1 | 回合小结(带会话编号) |
|
|
110
|
+
| L2 | 文件流向 |
|
|
111
|
+
| L3 | 工具调用流水(默认关) |
|
|
112
|
+
| L4 | **逐字存档你说过的每一句话**(带会话编号与时间) |
|
|
1093
113
|
|
|
1094
|
-
|
|
114
|
+
> 采集类档位只认**会话的工作目录**,不会去猜子项目。
|
|
115
|
+
> 在一个装着多个项目的目录里开会话时,采集类档位写不出东西 —— 这是刻意的。
|
|
1095
116
|
|
|
1096
|
-
|
|
1097
|
-
各个子目录各一份(**实测撞到过**:从 `proj-a` 开会话,记录写进了
|
|
1098
|
-
`proj-a\60-对话记录与交接\…`)。所以插件会**往上找工作区根**再落盘。
|
|
117
|
+
## 依赖与许可
|
|
1099
118
|
|
|
1100
|
-
|
|
119
|
+
- **零运行时依赖**(`dependencies` 为空)
|
|
120
|
+
- 唯一 peer 依赖:`@deepseek-ai/cordis`
|
|
121
|
+
- 许可:**MIT**
|
|
1101
122
|
|
|
1102
|
-
|
|
123
|
+
## 自检
|
|
1103
124
|
|
|
1104
|
-
|
|
125
|
+
仓库里带一套离线自检,不需要网络、不需要装 DSH:
|
|
1105
126
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
否则 AI 会把项目内容写进一个不属于项目的外部目录。
|
|
1109
|
-
|
|
1110
|
-
### 顺带:容器注入不再"死路一条"
|
|
1111
|
-
|
|
1112
|
-
现在容器里的注入那段话,除了"别在这里初始化",还会给出两条出路
|
|
1113
|
-
(做子项目 ⇒ 用 `root` 在子项目建台账;一次性问题 ⇒ 用 `ledger_note`),
|
|
1114
|
-
并写明判据。这段文案本身有专门的断言守着,因为它就是本功能存在的意义。
|
|
1115
|
-
|
|
1116
|
-
---
|
|
1117
|
-
|
|
1118
|
-
## 九、⚠ 容器工作区:不会在这里劝你 `git init`
|
|
1119
|
-
|
|
1120
|
-
插件会识别"**总工作区**"——自己不是 Git 仓库、但里面装着多个子项目
|
|
1121
|
-
(有 `AGENTS.md` 这类工作区标志文件,或子目录里有 `.git`)的目录。
|
|
1122
|
-
|
|
1123
|
-
在这种目录里:
|
|
1124
|
-
|
|
1125
|
-
- **不再**注入"这是一个新项目,要不要初始化"
|
|
1126
|
-
- 改注入一句"**这是容器工作区,不要在这里初始化**",并说明原因
|
|
1127
|
-
- `ledger_init` 会**直接拒绝**,除非显式传 `allowContainer: true`
|
|
1128
|
-
|
|
1129
|
-
理由:往容器目录 `git init` 会把下面所有子项目卷进一个**错误的父仓库**,
|
|
1130
|
-
而这是最难收拾的一类误操作。正确做法是把会话工作目录切到要管的**子项目**里再初始化。
|
|
1131
|
-
|
|
1132
|
-
判定细节(`lib/index.js:104-154`):
|
|
1133
|
-
|
|
1134
|
-
- 标志文件 `AGENTS.md` / `CLAUDE.md` / `.cursorrules` / `.windsurfrules`,
|
|
1135
|
-
**必须配上 ≥ 2 个子目录**才算容器证据
|
|
1136
|
-
(单个新项目也可能自带 `AGENTS.md`,只看标志文件会让它**永远收不到**初始化提示)
|
|
1137
|
-
- 或:向下最多 2 层、每层最多 300 个目录扫到 `.git`
|
|
1138
|
-
- 结果缓存 60 秒(`inspect()` 每步都跑,不缓存会在巨型目录上白扫几千次磁盘)
|
|
1139
|
-
|
|
1140
|
-
---
|
|
1141
|
-
|
|
1142
|
-
## 十、设计与安全约束(改动前必读)
|
|
1143
|
-
|
|
1144
|
-
`lib/index.js` 文件头有完整版,这里摘最要紧的几条 —— 每条都对应一次真实事故:
|
|
1145
|
-
|
|
1146
|
-
1. **不许 `import "@deepseek-ai/*"`**。插件本体在工作区,profile 里是 Junction,
|
|
1147
|
-
Node 按**真实路径**解析(内核没开 `--preserve-symlinks`)⇒ `ERR_MODULE_NOT_FOUND`
|
|
1148
|
-
⇒ **内核起不来**。内核能力全走服务调用。
|
|
1149
|
-
2. **`inject` 里没声明的服务,访问属性即抛错**(不是取到 `undefined`)。
|
|
1150
|
-
`apply()` 抛错 ⇒ **整个 profile 组合失败、内核起不来**。所以
|
|
1151
|
-
`export const inject = ["tools"]` 是必需的,可选服务(`commands`)用
|
|
1152
|
-
`ctx.inject([...], cb)`。
|
|
1153
|
-
3. **注入的消息必须带非空 string `id`**。缺 id 时当场不报错、**重载历史时才校验**
|
|
1154
|
-
(`dsh-session` 的 `lacks an identified message`)⇒ 一条坏事件能让**整个会话报废**。
|
|
1155
|
-
本插件用 `node:crypto` 的 `randomUUID()`,并在插入前再查一次。
|
|
1156
|
-
4. **永不 `git add -A`**(全局红线)⇒ 只 `git add -- <明确路径>`。
|
|
1157
|
-
5. **不碰 `$DSH_HOME\sessions\` / `storages\`**;会话态**只在内存的 Map** 里。
|
|
1158
|
-
6. 所有写入必须落在**项目根之内**(`isUnder()` 用 `path.relative` 判定,
|
|
1159
|
-
防止 `D:\proj-evil` 冒充 `D:\proj`)。
|
|
1160
|
-
7. **日志采集是同步跑在 `Session.append()` 路径上的** ⇒ 它抛错会连累官方写会话日志。
|
|
1161
|
-
所以采集全程 `try/catch` 吞错,且**失败绝不冒泡**(已用"坏事件"用例覆盖:`data: null`、
|
|
1162
|
-
非法 JSON 参数、`message: null`、空 cwd 都不许抛)。
|
|
1163
|
-
8. **日志落盘不逐条 spawn git**。去抖落盘(1.2 秒)只写文件,`git add` 只在
|
|
1164
|
-
回合末 / 卸载时做一次 —— 否则一场 490 次调用的会话会白拉几百个 git 进程。
|
|
1165
|
-
9. **工具返回值必须是 lossless JSON**(`undefined` 不是)。内核会校验,而最阴的地方是
|
|
1166
|
-
**副作用已经落盘、只有返回值不合法** ⇒ 调用看起来失败、其实写了。
|
|
1167
|
-
实测撞到:`let staged;` 在没有 Git 仓库时保持 `undefined`。
|
|
1168
|
-
⇒ 现在是 `...(staged === undefined ? {} : { staged })`,自检里有一整组
|
|
1169
|
-
(情形 17)在"无 Git"条件下把每个工具都跑一遍并递归查 `undefined`。
|
|
1170
|
-
|
|
1171
|
-
### 为什么注入走 `agent/pre-step` 而不是 `systemPrompt.context`
|
|
1172
|
-
|
|
1173
|
-
后者每次装配都重算 ⇒ 会写进 runtime-context 快照、每步都变 ⇒ **打烂 prompt 缓存**。
|
|
1174
|
-
`agent/pre-step` 只在**内容变了**时插,去重靠"指纹(`activeRel|行数|mtimeMs`)+ 文本"两道。
|
|
1175
|
-
|
|
1176
|
-
### 命令的分工
|
|
1177
|
-
|
|
1178
|
-
- `/ledger show` / `status` 是**纯读**,当场返回结果
|
|
1179
|
-
- `/ledger init` / `update` / `archive` 只**置一个待办标记**(内存 Map),
|
|
1180
|
-
由下一次 `agent/pre-step` 注入一条明确指令给 AI 去做判断
|
|
1181
|
-
|
|
1182
|
-
这么分是刻意的:命令处理器里没法可靠地"替 AI 做判断",
|
|
1183
|
-
而且**任何会话内容的写入都不该由命令悄悄完成**(本机有过写坏会话历史的真实事故)。
|
|
1184
|
-
|
|
1185
|
-
### 压缩之后为什么台账会重注(需求 ⑫)
|
|
1186
|
-
|
|
1187
|
-
★ 这一条**必须绕过指纹去重**,否则永远不触发:压缩前后活跃台账文件**一个字都没变**,
|
|
1188
|
-
指纹(`activeRel|行数|mtimeMs`)完全相同,正好会被"没变就不重注"的逻辑挡掉。
|
|
1189
|
-
|
|
1190
|
-
所以实现分两步:
|
|
1191
|
-
|
|
1192
|
-
1. 旁听 `ctx.on("session/event", …)` 的 **`compaction/end`** —— 判据是"发生了压缩"这件事本身,
|
|
1193
|
-
因此手动 `/compact`、官方自动压力压缩、本插件替用户执行的那次**都会被覆盖**
|
|
1194
|
-
2. 挂一个 `state.reinject` 标记,下一次 `agent/pre-step` 见到它就走
|
|
1195
|
-
`renderInjection(info, undefined, true)`,**无条件重注**并说明「上下文刚刚被压缩」,
|
|
1196
|
-
然后消费掉标记(只重注一次)
|
|
1197
|
-
|
|
1198
|
-
### 水位提醒为什么要分档 + 每档只发一次(需求 ⑪)
|
|
1199
|
-
|
|
1200
|
-
**为什么从"一道 80% 线"改成 30/50/70 三档**:旧的 80% 线落在官方自动压缩线
|
|
1201
|
-
(`floor(窗口 × 0.8)`)**之上** —— 等它响的时候,上下文已经被压掉了,
|
|
1202
|
-
而写台账要用的素材正好在那里面。三档的意义是**在丢之前分批落袋**:
|
|
1203
|
-
每档都把当前进展固化一次,于是即使 80% 处被自动压缩,损失也只是"最后一段"。
|
|
1204
|
-
**70% 那一档要求停手**:离 80% 只剩 10%,而"写台账 + 写交接单"本身要读文件要思考,
|
|
1205
|
-
很容易**边写边被清**。
|
|
1206
|
-
|
|
1207
|
-
**为什么每档只发一次**:提醒文案里带**实测百分比**,数值一变文本就变。若每步都发,
|
|
1208
|
-
`agent/pre-step` 每步都插一条**内容都不同**的消息 ⇒ **prompt 缓存每步都被打烂**。
|
|
1209
|
-
|
|
1210
|
-
所以:`state.pressureNotified` 记的是**已发过的档位集合**(不是一个布尔 ——
|
|
1211
|
-
布尔表达不了"30% 发过了、50% 还没")。掉回某档之下就把那一档移出集合 ⇒ 重新武装,
|
|
1212
|
-
压缩之后能再提醒一轮。
|
|
1213
|
-
|
|
1214
|
-
**一次涨过两档时只发最高那档**,并把比它低的档一并标记为已发。否则下一步会补发一条
|
|
1215
|
-
"照常干活"的 30% 档文案,而水位其实已经 75% 了 —— 那是一条**方向相反**的指令。
|
|
1216
|
-
|
|
1217
|
-
**70% 那一档还额外告诉用户**(挂在回合末卡片上):`renderPressureNotice` 是注入给
|
|
1218
|
-
**AI** 的,而 AI 完全可以不理会、或不转述。用户要的是"**提醒手动压缩**",
|
|
1219
|
-
那个"提醒"的对象是**人**。所以 70% 时再走一次卡片,让他自己看见并决定。
|
|
1220
|
-
30%/50% 不打扰用户 —— 那两档的动作是"顺手固化、照常干活",没有需要他决策的东西。
|
|
1221
|
-
|
|
1222
|
-
---
|
|
1223
|
-
|
|
1224
|
-
## 十一、文件与安装
|
|
1225
|
-
|
|
1226
|
-
```
|
|
1227
|
-
本插件目录\
|
|
1228
|
-
├── lib\index.js 宿主半边(约 6900 行,唯一实现)
|
|
1229
|
-
├── lib\client.js ★ 浏览器半边(设置面板,2026-10-09 新增)
|
|
1230
|
-
├── package.json name / dsh.bundle.patch / dsh.client / peerDependencies
|
|
1231
|
-
├── cordis.patch.yml profile bundle patch(id 用包名)
|
|
1232
|
-
├── tools\selftest.mjs 离线自检:node tools\selftest.mjs
|
|
1233
|
-
└── README.md 本文件
|
|
127
|
+
```bash
|
|
128
|
+
npm test
|
|
1234
129
|
```
|
|
1235
130
|
|
|
1236
|
-
|
|
1237
|
-
因为设置面板要在浏览器里渲染。`package.json` 里 **`dsh.bundle` 与 `dsh.client` 必须同时在**
|
|
1238
|
-
(只写 `dsh.client` 会让内核直接失败,那条教训写在 `cordis.patch.yml` 的注释里)。
|
|
1239
|
-
|
|
1240
|
-
安装位置(三处):
|
|
1241
|
-
|
|
1242
|
-
- `$DSH_HOME/profiles/<profile>/package.json` 的 `dependencies` + `dsh.profile.bundles`
|
|
1243
|
-
- `$DSH_HOME/profiles/<profile>/node_modules/dsh-ledger-memory`(Junction 或直接安装)
|
|
1244
|
-
- 若用 Junction 链:profile → 仓库 `plugin/dsh-ledger-memory` → 插件真源目录
|
|
1245
|
-
|
|
1246
|
-
> ⚠ 改代码后**需要重启客户端**才生效 —— 内核对已加载插件不会因文件改动自动重载
|
|
1247
|
-
> (`hmr` 服务没有可主动触发重载的方法)。
|
|
1248
|
-
|
|
1249
|
-
---
|
|
1250
|
-
|
|
1251
|
-
## 十一之二、设置面板(设置 → 项目台账)
|
|
1252
|
-
|
|
1253
|
-
设置导航里会多出一页 **「项目台账(台账 / 压缩 / 日志)」**,
|
|
1254
|
-
与插件市场用的是**同一种**机制(`settings.section` 这个 slot)。
|
|
1255
|
-
|
|
1256
|
-
### 能调哪十二项
|
|
1257
|
-
|
|
1258
|
-
| 设置 | 默认 | 说明 |
|
|
1259
|
-
|---|---|---|
|
|
1260
|
-
| 回合末弹四选一卡片 | **关** | 关着时回合末不问;台账回执不受影响 |
|
|
1261
|
-
| 高保真压缩 | **开** | 水位到点抓逐字胶囊再压缩,压完把台账与胶囊注回 |
|
|
1262
|
-
| 高保真压缩触发水位 | **0.70** | 可调 0.50–0.79(**上限刻意压在 0.79**,见下) |
|
|
1263
|
-
| 允许插件代你压缩 | **开** | 关掉后插件不再自动调 `compactNow`(手动的 `/ledger compact` 仍可用) |
|
|
1264
|
-
| **压缩前强制落盘** | **开** | ★ 只约束 **AI 自己发起的**压缩;你手动按的不受它拦(见 D40) |
|
|
1265
|
-
| **增量(分段)压缩** | **开** | ★ 常态那条路:只压最旧一段、保留最近一大块(见 D41) |
|
|
1266
|
-
| **增量·触发涨幅** | **20** 点 | 水位比上次压缩时涨这么多就压一次 |
|
|
1267
|
-
| **增量·保留最近** | **10** 点 | 这段一个字不动。20→10 ≈ 上下文装两倍 |
|
|
1268
|
-
| 活动日志各档 | L0/L1/L2/L4 开,L3 关 | 五档分别开关 |
|
|
1269
|
-
| 活跃台账行数上限(默认) | **300** | 见下「项目清单优先」 |
|
|
1270
|
-
| **变更登记保留条数(默认)** | **12** | 台账末尾那张表留几行(3–100);它**算在**行数上限里,台账吃紧时调小它最省事 |
|
|
1271
|
-
| 水位提醒档位 | **0.3 / 0.5 / 0.7** | 逗号分隔,可增删 |
|
|
1272
|
-
|
|
1273
|
-
### ★★ 一条必须知道的语义:**项目清单优先**
|
|
1274
|
-
|
|
1275
|
-
`maxActiveLines`、`changeRegisterMax` 与 `logLevels` **每个项目都有自己的真源**
|
|
1276
|
-
(`.dsh-ledger.json`)。所以设置面板里这三项**不是**"远程覆盖每个项目",而是:
|
|
1277
|
-
|
|
1278
|
-
> **设置面板 = 新项目登记(`ledger_init`)时写进去的默认值。**
|
|
1279
|
-
> 项目一旦在清单里登记过,就**以清单为准**。
|
|
1280
|
-
|
|
1281
|
-
为什么不做成覆盖:日志是**只追加的证据**。若全局设置能改掉某个项目正在用的档位,
|
|
1282
|
-
"某段时间为什么没有 L2"就变得无法解释 —— 那是把证据链弄坏。
|
|
1283
|
-
(这条与 `DECISIONS.md` D25「一个事实只能有一个判定出口」同源。)
|
|
1284
|
-
|
|
1285
|
-
### 为什么水位上限是 0.79 而不是 1.0
|
|
1286
|
-
|
|
1287
|
-
内核自己的自动压缩阈值是 **0.8**。我们的高保真压缩的全部意义就是**比它早一步**
|
|
1288
|
-
(在细节被抹掉之前把状态落袋)。所以面板会把越界值**夹到 0.79**,
|
|
1289
|
-
并在保存时如实告诉你"已夹紧" —— 而不是静静接受一个会与内核撞在同一刻的值。
|
|
1290
|
-
|
|
1291
|
-
### 它是怎么接进官方设置的
|
|
1292
|
-
|
|
1293
|
-
```
|
|
1294
|
-
浏览器(lib/client.js) 宿主(lib/index.js)
|
|
1295
|
-
└─ ctx.slots.inject("settings.section") └─ export const Config(手写 StandardSchemaV1)
|
|
1296
|
-
└─ 面板 UI └─ webServer 路由 /dsh-ledger-memory-settings/config.json
|
|
1297
|
-
├─ GET ← 读当前设置 + 元数据 ├─ GET 读
|
|
1298
|
-
└─ POST → 提交差异补丁 ────────────────────────┘
|
|
1299
|
-
└─ POST 写 ⇒ configEditor.edit()(官方唯一写入入口)
|
|
1300
|
-
```
|
|
1301
|
-
|
|
1302
|
-
三条设计要点:
|
|
1303
|
-
|
|
1304
|
-
1. **面板不自建存储**。读写都打到宿主,宿主用官方的 `configEditor.edit()` 落盘
|
|
1305
|
-
⇒ 唯一真源是 Loader entry 的 `config`,与内核实际在用的那份**是同一个**。
|
|
1306
|
-
自己改 profile 的 patch 文件是红线(那是官方家目录)。
|
|
1307
|
-
2. **`Config` 是手写的、零 `import`**。内核只要一个 `~standard.validate`
|
|
1308
|
-
(`@deepseek-ai/cordis/src/registry.ts:107`),所以够用;
|
|
1309
|
-
而**必须**手写 —— 插件目录是 Junction 目标,Node 解析真实路径,
|
|
1310
|
-
`@deepseek-ai/schemastery` 在这里 `MODULE_NOT_FOUND`(实测)。
|
|
1311
|
-
3. **只提交改过的键**。整份提交会把"面板打开期间别处改过的键"覆盖回去。
|
|
1312
|
-
4. ★★ **写回时只并"本层已有的值",绝不并"上层给你的值"**。
|
|
1313
|
-
`configEditor.edit()` 的 `change(current, inherited)` 会给你两个:
|
|
1314
|
-
`current` 是**本行自己**的、`inherited` 是**上层**(bundle 层 / 家级 patch)的。
|
|
1315
|
-
把 `inherited` 并进写回值,等于把上层的值**钉死在 profile 层** ——
|
|
1316
|
-
从此上层再改也改不动它。内核自己的 README 就警告了这件事:
|
|
1317
|
-
*"完整配置覆盖保留普通字段,但会在 profile 层**固定其当前原始值**"*。
|
|
1318
|
-
(这条是我**读完内核实现**才发现自己写错了,见 `DECISIONS.md` D38 末节。)
|
|
1319
|
-
|
|
1320
|
-
### ⚠ 两条已知边界
|
|
1321
|
-
|
|
1322
|
-
- **没有 `webServer` 或 `configEditor` 时**:面板**只能看不能改**,
|
|
1323
|
-
并会**明说原因**(不画一堆点了没反应的控件)。**台账功能完全不受影响。**
|
|
1324
|
-
- **`settings.section` 不投影 `icon`** —— 第三方分区都顶着官方的齿轮图标。
|
|
1325
|
-
插件市场是用 MutationObserver 改官方 DOM 硬塞图标的,**我们没学那个**(官方一改选择器就失效)。
|
|
1326
|
-
取而代之:导航名里带上「项目台账」四个字,一眼能认出。
|
|
1327
|
-
|
|
1328
|
-
---
|
|
1329
|
-
|
|
1330
|
-
## 十二、自检
|
|
1331
|
-
|
|
1332
|
-
```
|
|
1333
|
-
cd <本插件目录>
|
|
1334
|
-
node tools\selftest.mjs
|
|
1335
|
-
```
|
|
1336
|
-
|
|
1337
|
-
**当前:通过 709 项,失败 0 项。**
|
|
1338
|
-
|
|
1339
|
-
自检用**假 ctx 严格模式**(模拟"访问未声明服务即抛错"),覆盖**二十七个**情形组:
|
|
1340
|
-
模块契约与全新项目检测 / `ledger_init` 全流程 / 已有台账的注入 /
|
|
1341
|
-
`ledger_write` 与阈值闸门 / `ledger_archive` / `/ledger` 命令 /
|
|
1342
|
-
**`/ledger` 裸敲弹选择卡片** / 服务缺失容错 / 注入路径健壮性 / 候选名判定保守性 /
|
|
1343
|
-
**容器工作区** / **`ledger_checkpoint` 四选一** / **上下文水位分档提醒(30/50/70)** /
|
|
1344
|
-
**回合末四选一(默认关 · 开关仍可用)** /
|
|
1345
|
-
**活动日志(五档采集 · 只追加 · 回溯读 · 永不注入 · L4 用户消息)** /
|
|
1346
|
-
**`root` 参数** / **问题记录 `ledger_note`** / **返回值 lossless JSON** /
|
|
1347
|
-
**第一轮先问「建项目 or 不建」** / **多会话同项目** / **已有项目的台账重构** /
|
|
1348
|
-
**台账改动回执(走注入)** / **70% 高保真压缩(逐字胶囊 + 压缩后重注)** /
|
|
1349
|
-
**手动高保真压缩(工具 + 命令 + 卡片三条入口都有胶囊)** /
|
|
1350
|
-
**台账自身的变更登记(需求 ①)** / **四条压缩入口在日志里分得清(需求 ②③)** /
|
|
1351
|
-
**官方设置面板(`Config` + 路由 + 面板契约,78 条断言)**。
|
|
1352
|
-
|
|
1353
|
-
关键断言包括:注入消息有非空 id、`source.kind === "project-ledger-notes"`、
|
|
1354
|
-
`.gitignore` 合并而非覆盖、归档二次落同一文件且只追加、
|
|
1355
|
-
`newActive` 超阈值时"归档已写但活跃台账未动"、五种坏上下文下注入都不抛错、
|
|
1356
|
-
`TODO.md`/`README.md`/`PROJECT.md` 同存时**不**误判为台账、
|
|
1357
|
-
四个选项与用户原话逐字一致、**`running` 时不压缩而 `idle` 时才压缩且只压一次**、
|
|
1358
|
-
**压缩后台账强制重注(哪怕文件一个字没改)且只重注一次**、
|
|
1359
|
-
**30%/50%/70% 三档各自只发一次、每档文案不同、一次跨两档只发最高档、掉回线下可重新武装**、
|
|
1360
|
-
**★ 70% 档要求"继续工作"而不再要求"停手"**、
|
|
1361
|
-
**★ 70% 时 pre-step 不压、idle 时才由插件自己压一次**、
|
|
1362
|
-
**★ 压缩后重注里带逐字胶囊(真事件验证:最近 41 条用户原话 41/41 逐字保住)**、
|
|
1363
|
-
**★ 高保真全程不弹卡片(不打断工作)**、**★ 同一会话只压一次(防 summary-of-summary 退化)**、
|
|
1364
|
-
**★ 没有 compaction 服务时静默降级并留下告警(不假装压过)**、
|
|
1365
|
-
**★★★ `compaction` 服务必须经 `agentPresets.serviceFor` 取(本 profile 里它被 preset 的
|
|
1366
|
-
`isolate` 关在子树内,`ctx.get` 与 `agent.ctx.get` **都拿不到**;见 D39)**、
|
|
1367
|
-
**★★★ `ledger_status` 报出压缩服务可达性与走的是哪条路(让静默软失败可观测)**、
|
|
1368
|
-
**回合末立刻返回不等答复**、**选完才 steer 且 steer 消息带 id 与插件自己的 source**、
|
|
1369
|
-
**选「不记录」不 steer(不白起一轮)**、**AI 执行那一轮结束不再追问、再下一次恢复**、
|
|
1370
|
-
**卡片挂着时不重复问**、**子 agent 回合末不打扰**、
|
|
1371
|
-
**★ 默认不弹选择题、且日志照常落盘(关卡片没关别的)**、
|
|
1372
|
-
**L3 关着时不留文件**、**只有失败那次带 `[失败]` 且成功那次不被误标**、
|
|
1373
|
-
**一次 L1 只产生一行且能被读回来**(多行写法曾让它整条消失)、
|
|
1374
|
-
**日志正文与日志路径都不出现在注入里、而台账仍然注入**、
|
|
1375
|
-
**没登记 `logDir` 时项目里不多出任何目录**、
|
|
1376
|
-
**坏事件(`data:null` / 非法 JSON / 空 cwd)不让采集抛错**、
|
|
1377
|
-
**L4 只记 `source.kind === "user"`(五类系统注入以 `role:"user"` 落盘却一条都没被记)**、
|
|
1378
|
-
**L4 带会话编号与事件自身的发送时间(不是落盘时刻)**、
|
|
1379
|
-
**L4 多行消息只占一行(换行转义成 `\n`)**、**L4 记下图片非文本 part 的痕迹**、
|
|
1380
|
-
**`view:"users"` 按会话编号汇总**、**L4 默认开、默认不进 `timeline`、关掉时不留文件、永不注入**、
|
|
1381
|
-
**「有 `_ledger-log` 目录但没登记」时 `status` 不再谎报已启用、且明确说出"有目录但没在采集"**、
|
|
1382
|
-
**容器会话里 `root` 把台账落进子项目而不是容器根**、
|
|
1383
|
-
**点名容器根 `ledger_init` 仍被拒**、**`root` 不存在时抛错而非静默写错地方**、
|
|
1384
|
-
**卡片选中与手打同一子命令结果逐字一致**、**没有卡片 UI 或卡片被丢弃时 `/ledger` 回退帮助而不抛错也不挂住**、
|
|
1385
|
-
**问题记录永不覆盖同名文件(原文件逐字节不变)**、
|
|
1386
|
-
**从子目录开会话时记录落到工作区根而不是子目录里另起一份**、
|
|
1387
|
-
**Git 项目里 `ledger_note` 被拒并指向 `ledger_write`**、
|
|
1388
|
-
**只看 status 不创建任何目录**、**容器注入既说"别初始化"又给出 `ledger_note` 这条出路**、
|
|
1389
|
-
**第一轮弹的是「建项目 or 不建」而不是四选一、且那一轮只问一张**、
|
|
1390
|
-
**容器里选「建项目」时明确禁止在当前目录 `git init` 并要求先问哪个子项目**、
|
|
1391
|
-
**第一轮只问一次**、**已有台账时 turn=1 仍会弹常规四选一(不能一张都不弹)**、
|
|
1392
|
-
**多会话旧版本重写不被拒绝、且对方的条目被自动保留**、
|
|
1393
|
-
**有意精简台账时不被硬塞保留区(反向用例)**、
|
|
1394
|
-
**audit 视图认出两个会话都改过、且 levels 限定别的档位时仍可读**、
|
|
1395
|
-
**合规项目被正确判 ok、不合规才弹卡片**、
|
|
1396
|
-
**修复只在加法:已存在的决策记录内容逐字不变、清单原有字段与 revision 不被重置**、
|
|
1397
|
-
**超阈值时台账正文一个字都没动(插件不替 AI 删条目)**、
|
|
1398
|
-
**「没登记」与「登记了但文件不存在」两种不合格都能抓到**、
|
|
1399
|
-
**容器工作区不被建议在这里 ledger_init**、**只缺 revision 时不弹卡片(它会自愈)**、
|
|
1400
|
-
**README / CHANGELOG / 交接单不被认成事实台账**、
|
|
1401
|
-
**写完台账后下一次弹卡片会带上回执、且编号与条目行首一致**、
|
|
1402
|
-
**回执只告知一次(不每轮重复念)**、**归档也产生回执**、
|
|
1403
|
-
**没写过台账时不出现回执**、**`/ledger status` 列出最近改动**、
|
|
1404
|
-
**归档也涨 revision 且留审计(kind 是 `ledger-archive`)、
|
|
1405
|
-
且"拿归档前旧号写一份漏掉现存条目的内容"会被判过期并保住那些条目**、
|
|
1406
|
-
**两个会话(写 + 归档)都被审计认出**、
|
|
1407
|
-
**★ 台账写入后台账里出现变更登记表,带"到秒的时间戳 + 操作类型 + 会话编号 + 行数变化 + 修订号"**、
|
|
1408
|
-
**★ 归档也记一行且行数变化是负数**、**归档文件里不出现登记区(不污染历史)**、
|
|
1409
|
-
**★ 登记表有界(连写 20 次只留 12 条)且只有一份(不会被复制成两份)**、
|
|
1410
|
-
**★★ `append` 的内容不会被登记区吃掉(真事故的回归测试)**、
|
|
1411
|
-
**★★ 登记行不算"条目"(不会虚报 `preservedEntries`、时间戳不会进保留区)**、
|
|
1412
|
-
**★ 决策记录不加登记区、台账文件不被顺手碰**、
|
|
1413
|
-
**★ AI 整份 replace 抹掉登记区后,下一次写入自动重建且旧条目没丢**、
|
|
1414
|
-
**★ 行数闸门算的是含登记区的真实行数,被拒时台账一个字都没落盘**、
|
|
1415
|
-
**★ `lines`(落盘真实行数)与 `changedBy`(正文增减)是两个数**、
|
|
1416
|
-
**★★ 重注里声明「这是历史素材,不是用户此刻的要求」并要求执行前先按用户最近一次实际要求确认**、
|
|
1417
|
-
**★★ 四条压缩入口(自动 70% / 工具 / 命令 / 卡片)在 L0 日志里各自标出来源**、
|
|
1418
|
-
**★ 没有来源时不凭空造词(退化成「上下文已压缩」)且不写 `undefined`**、
|
|
1419
|
-
**★ 来源用完即删(第二次压缩不沿用上一次的来源)**、
|
|
1420
|
-
**★★ `export const Config` 是合法 StandardSchemaV1(`~standard.validate` 同步返回 `{value}`/`{issues}`,不抛)**、
|
|
1421
|
-
**★★ 不传配置时七项默认值与旧行为逐字一致(默认值回归)**、
|
|
1422
|
-
**★ 未知键丢弃 / 越界夹紧(水位夹到 0.79)/ 类型错丢弃,都不抛**、
|
|
1423
|
-
**★ 路由带自己的前缀、`kind: "exact"` 不吞邻近路径**、
|
|
1424
|
-
**★ 没有 `webServer` 时插件照常注册工具(面板是锦上添花,不是运行前提)**、
|
|
1425
|
-
**★★ 没有 `configEditor` 时可读不可写,且明说原因**、
|
|
1426
|
-
**★★ POST 走 `configEditor.edit()` 且是按补丁合并(改一个键不抹掉别的字段)**、
|
|
1427
|
-
**★★ 非法值 / 坏 JSON ⇒ 400 且绝不落盘**、
|
|
1428
|
-
**★★ 设置真的接进行为(自定义水位档位生效 / 日志档位与行数上限写进新项目清单)**、
|
|
1429
|
-
**★★ 项目清单优先于插件级设置(行数上限与日志档位都以项目清单为准)**、
|
|
1430
|
-
**★ 客户端半边是经典脚本(无顶层 export/import)、用 `__ModuleLoader__.load` 注册、只 require react**、
|
|
1431
|
-
**★★ CSS 只引用真实存在的 `--dsw-*` 变量(对着主题令牌清单核过)**、
|
|
1432
|
-
**★ 客户端注册失败记进 diagnostics(不静默吞)**、
|
|
1433
|
-
**★★★ `autoCompact: false` 只拦"自动"、不拦"手动"(工具照常排队且真的压了 —— 闸门放错位置的判别式)**、
|
|
1434
|
-
**★★ `highFidelityCompact: false` 时不自动压,但**水位提醒照发**(两件事不连坐)**、
|
|
1435
|
-
**★★ 压缩水位可调(0.7 时 0.65 不排队;0.6 时排队)且注入文案报的是设置的水位、不是写死的 70**、
|
|
1436
|
-
**★★★ 上层的值不会被钉进 profile 层(`inherited` 不参与写回 —— 由内核源码揪出的真 bug)**。
|
|
1437
|
-
|
|
1438
|
-
---
|
|
1439
|
-
|
|
1440
|
-
## 十三、为什么**不**学 billion-context 关掉内核压缩(D32)
|
|
1441
|
-
|
|
1442
|
-
你让我参考的那个插件(`billion-context`),本机有一份完整备份,我读了它的
|
|
1443
|
-
`README.zh-CN.md`、`package.json`、`dsh.bundle.patch.yml` 与许可证,也把**内核**相关源码
|
|
1444
|
-
从 `app.asar` 里解出来核对过。结论是一条**架构红线**。
|
|
1445
|
-
|
|
1446
|
-
### 它怎么做的(逐字证据)
|
|
1447
|
-
|
|
1448
|
-
它的 `dsh.bundle.patch.yml` 全文只有 10 行,关键在第 8–10 行:
|
|
1449
|
-
|
|
1450
|
-
```yaml
|
|
1451
|
-
- insert:
|
|
1452
|
-
- id: bili-native
|
|
1453
|
-
name: billion-context
|
|
1454
|
-
- id: compaction-basic # ← 非 insert、只写 id ⇒ 覆盖内核那一行的 config
|
|
1455
|
-
config:
|
|
1456
|
-
auto: false # ← 关掉内核的自动压缩
|
|
1457
|
-
```
|
|
1458
|
-
|
|
1459
|
-
⇒ 它**关掉了内核自己的自动压缩**,换成自己那套"把旧消息换成可回查引用"的方案。
|
|
1460
|
-
|
|
1461
|
-
### ★ 它为什么被抛弃(你的原话,第一手证据)
|
|
1462
|
-
|
|
1463
|
-
> *"这个插件的一个缺点就是超长上下文的情况下,模型会记忆力缺失和分散注意力。
|
|
1464
|
-
> 而且关掉插件又不可逆(对话无法压缩和继续工作),所以说他们有很大的缺点,
|
|
1465
|
-
> 但是值得借鉴,我们的台账日志系统记忆方面比较擅长。"*
|
|
1466
|
-
|
|
1467
|
-
拆成两条,两条都是硬伤:
|
|
1468
|
-
|
|
1469
|
-
1. **它并不真正缩小上下文** ⇒ 超长上下文下**记忆力衰退、注意力分散**。
|
|
1470
|
-
"可回查的引用"**仍然占着上下文**,模型仍要带着它跑。**压缩没有换来更短的上下文** ——
|
|
1471
|
-
这是它失败的**根因**。
|
|
1472
|
-
2. **它是一扇单向门**:对话的压缩路径从此依赖它 ⇒
|
|
1473
|
-
**关掉插件之后,既压不了、也接不下去**。一个"关不掉"的插件,用户迟早要付出代价。
|
|
1474
|
-
|
|
1475
|
-
### ★ 我们的定位(对照,也是本插件立身之本)
|
|
1476
|
-
|
|
1477
|
-
| | billion-context | **本插件** |
|
|
1478
|
-
|---|---|---|
|
|
1479
|
-
| 对内核压缩 | **关掉**(`auto: false`) | **不碰**,照常工作 |
|
|
1480
|
-
| 禁用插件后 | ❌ 对话压不了也接不下去 | ✅ **一切照旧**,压缩继续由内核做 |
|
|
1481
|
-
| 缩不缩上下文 | 不真缩(换成引用) | **内核负责缩**,我们不管这一步 |
|
|
1482
|
-
| 我们管什么 | —— | **保住"不能丢的东西",压缩后重注回去** |
|
|
1483
|
-
|
|
1484
|
-
**证据(本插件是纯加法)**:我们的 `cordis.patch.yml` 只有一条 `insert`,
|
|
1485
|
-
**只插自己的 id、从不按 id 改别人的条目**;`package.json` 的 `dsh.bundle` 只有自己那份 patch。
|
|
1486
|
-
|
|
1487
|
-
⇒ 所以我们跟它**不是同一个思路**:
|
|
1488
|
-
**内核负责把上下文压短(它做这件事做得对),我们负责在压缩之前把逐字副本落袋、
|
|
1489
|
-
压缩之后再重注回去。** 这正是调研结论里唯一被反复验证的那一招:
|
|
1490
|
-
**摘要 + 一份逐字、可重读的副本,并在压缩后重读它。**
|
|
1491
|
-
|
|
1492
|
-
### 附:顺手核实了那条"patch 别人配置"的机制(万一将来要用)
|
|
1493
|
-
|
|
1494
|
-
我把内核的 patch 语义从 `cordis-plugin-include/src/index.ts:44-129` 解出来读了。
|
|
1495
|
-
`- id: <别人的 id>` + `config:` 这种写法**确实成立**,但有两条必须知道:
|
|
1496
|
-
|
|
1497
|
-
- ⚠️ **`config` 是整份替换,不是深合并**(`target[key] = value`,`config` 也在内)。
|
|
1498
|
-
要 patch 别人的 `config`,**必须先把原有字段抄全**,否则静默抹掉。
|
|
1499
|
-
- ⚠️ **匹配不到只是警告 + 跳过**,不报错。所以写错了不会当场发现。
|
|
1500
|
-
|
|
1501
|
-
★ 而且**即便**将来要用,也**只**考虑用它**调阈值**(`thresholdRatio`,默认 0.8),
|
|
1502
|
-
**绝不**用它关掉压缩。详细核实过程与复现命令:
|
|
1503
|
-
`_调研-内核bundle-patch机制.md`。
|
|
1504
|
-
|
|
1505
|
-
### ⚠️ 一个会绊人的补充(第二份独立调研发现)
|
|
1506
|
-
|
|
1507
|
-
`compaction-basic` 在内核里挂了**多份同名 id** —— host 一份
|
|
1508
|
-
(`dsh-base/cordis.patch.yml:341`),每个 preset 里又插一份
|
|
1509
|
-
(如 `dsh-web-app/presets/standard.patch.yml:70`),而 web 把 host 那份
|
|
1510
|
-
`disabled: true`(`dsh-web-app/cordis.patch.yml:509`)。
|
|
1511
|
-
⇒ **一行 `- id: compaction-basic` 未必命中真正生效的那一个**;
|
|
1512
|
-
要改 preset 那份得**重述整个 preset 条目**(而 patch 是**整份替换** ⇒ 风险高)。
|
|
1513
|
-
★ 这也解释了它 README 里"web profile 除外"那条限制。
|
|
1514
|
-
⇒ **又一次印证:这个机制又危险又难命中,我们不碰。**
|
|
1515
|
-
|
|
1516
|
-
---
|
|
1517
|
-
|
|
1518
|
-
## 十四、致谢与出处
|
|
1519
|
-
|
|
1520
|
-
- **`billion-context`**(<https://github.com/ranxianglei/billion-context>,MIT +
|
|
1521
|
-
`Additional Term — Attribution on Product Surfaces`):
|
|
1522
|
-
本插件**只借鉴其思路**(并从中反推出"该怎么定位"),**没有使用它的任何代码、
|
|
1523
|
-
模块,也没有使用它的 `acp-kernel`**。见 §十三 与 `DECISIONS.md` 的 D32–D36。
|
|
1524
|
-
最有价值的一条借鉴写在 D34:**恢复必须是"推"给模型的,不能是"等模型来查"的。**
|
|
1525
|
-
- **内核源码**:本文里关于阈值、`summarize()`、`auto`、patch 语义的所有断言,
|
|
1526
|
-
都来自把 `@deepseek-ai/dsh-compaction-basic`、`dsh-base/cordis.patch.yml`、
|
|
1527
|
-
`cordis-plugin-include`、`dsh-app-boot` 从 `app.asar` 解出来**读过**,
|
|
1528
|
-
不是猜的。复现脚本:`tools/_probe-asar.mjs` + `tools/_probe-asar-read.mjs`
|
|
1529
|
-
(两个都是**只读**探针,对 `app.asar` 不写一个字节)。
|
|
1530
|
-
|
|
1531
|
-
### ⚠️ 给复核者:**别用 PowerShell 数行数**
|
|
1532
|
-
|
|
1533
|
-
实测(`DECISIONS.md` D37):同一份文件
|
|
1534
|
-
|
|
1535
|
-
| 数法 | 结果 |
|
|
1536
|
-
|---|---|
|
|
1537
|
-
| `(Get-Content -Encoding UTF8).Count` | **601** ✅ |
|
|
1538
|
-
| `(Get-Content).Count`(默认) | 453 ❌ 少 25% |
|
|
1539
|
-
| `Measure-Object -Line` | 430 ❌ 最不准 |
|
|
1540
|
-
| Node `split('\n').length` | **602** ✅ |
|
|
1541
|
-
|
|
1542
|
-
PS 5.1 默认按 GBK 解码 UTF-8 ⇒ 换行被吞、行数偏少。
|
|
1543
|
-
**数行数用 Node 或 `read` 工具。** 要在这个环境读文本,**必须** `-Encoding UTF8`。
|
|
1544
|
-
|
|
1545
|
-
|
|
131
|
+
## 已知边界
|
|
1546
132
|
|
|
133
|
+
- 注入走 `agent/pre-step`(可替换本轮消息),不是 `systemPrompt.context`。
|
|
134
|
+
- 改代码后**必须重启客户端**,内核对已加载插件不会热重载。
|
|
135
|
+
- 采集类日志档位在"容器工作区"(装着多个项目的目录)里写不出东西。
|
|
136
|
+
- 压缩服务只能经 `agentPresets.serviceFor(agent, "compaction")` 取到,`ctx.get("compaction")` 恒为 `undefined`。
|