这份规格管「边界与权威」:哪一层负责什么、两个面的设置谁说了算、注入怎么进提示词。 各部分的细节在各自的规格里,这里只链接,不复制(改这一条要有理由):
| 细节 | 在哪 |
|---|---|
记录格式、精细度档位、check/lint 判定 |
worklog-spec.md |
| 配置卡片的三个页签、每个字段的归属、写回通路 | settings-spec.md |
全局记忆的目录、来源分档、索引、dream、inbox |
memory-spec.md |
目标.md 的两层表 |
goals-spec.md |
① 技能层 skills/ 无 DSH 依赖 —— 能在没有 DSH 的环境里单独跑
↓ 引擎是 journal.py(stdlib only)
② 记忆层 ~/dsh-lore-memory/ 跨工作区事实 —— 在**所有**工作区之外(`memoryDir` 可改)
↓ 写:CLI / 读:插件工具
③ 运行时层 lib/index.js(宿主半边) provider / 工具 / 注入 / 只读状态端点
lib/client.js(浏览器半边) 本 bundle 那一页上的配置卡片
↓
④ 配置层 Config.volatile() 官方通路(profile patch;headless 退回 settings.json)
依赖只能向下:技能层不知道 DSH 存在;记忆层不知道插件存在(它只是文件); 运行时层是唯一同时知道技能与记忆的地方。
门面怎么说:包根
README.md面向使用者,把这张图讲成三块能力 —— 技能层 / 记忆层 / 插件层(运行时层 + 配置层合称插件层)。 那是同一张图的粗粒度说法,架构判断一律以本节的四层为准; 两处若不一致,改的是 README,不是这一节。
为什么这条不能破:① 之所以要能独立跑,是因为记录体系对无 DSH 的用户同样有价值
(skill-only/ + dist/ 那条发行线在独立技能仓库 project-work-log 里,就是为它存在的)。一旦技能层依赖插件,
那条线就死了。
| 在哪 | 为什么 | |
|---|---|---|
| 写 | journal.py(CLI) |
写要经过来源分档、索引重建、lint。这些是技能层的能力,而且必须在无 DSH 时也能做 |
| 读 | 插件工具 worklog_memory |
结构化、不依赖 shell、也不要求技能在场;没有它,代理就只能靠 pwsh 去 cat 那些文件 |
| 取件(信箱) | 同一工具的 inbox 操作 |
见 §六:把消息整个搬进收件方工作区,正文以文件形式到达 |
| 发送(信箱) | 同一工具的 send 操作 |
薄包装 journal.py inbox put(子进程):写路径只有 CLI 一条;文件必须装进信里,不许直接放进对方目录 |
| 会话清单 | 同一工具的 sessions 操作 |
默认只列调用方 Agent 的 cwd 下的会话;显式 scope: "all" 才跨工作区(只读,每行标注 @工作区);send 仍只接受本工作区会话(见 memory-spec §十一 ①) |
| 主动送达授权 | 同一工具的 subscribe 操作 |
只给自己的工作区开/关(边界同样是调用方 cwd)。沙箱会话里 CLI 写不了记忆根,这条是主路;发件方替别人开不了 |
2026-09-30 实测纠正:这张表原先写着"代理自己的
read/grep被ctx.workspaceFiles限制在工作区内,碰不到$DSH_HOME"。在当前策略(workspace-write)下不成立 ——read工具能直接读~/.dsh/memory/INDEX.md(读不限、写受限)。所以插件的价值不是"唯一的路", 而是唯一不离开会话、不靠 shell、也不依赖技能在场的路;对"连读也受限"的策略它同样是唯一的路。 这条口径错误来自上一轮的源码阅读,没有在跑起来的宿主里验过 —— 记在这里,别再当事实用。
这条不对称是有意的,但它的代价要认:模型能用工具读记忆,却要去 shell 里跑 CLI 才能写。
所以插件另提供薄包装:send 起 journal.py inbox put 子进程,判定与落盘仍在 CLI ——
工具不自己发明格式(那会变成第二份实现,漂了以后两边都绿;见 inboxSplit 那次夹具事故)。
真正写记忆条目的路依旧只有 CLI。
2026-09-29 起,配置只有一个事实源:宿主的设置通道。 分层是宿主的:
profile patch(用户层,卡片写这里) > 本包的行配置 > 内置默认
没有 configEditor 的宿主(headless)退回插件自己的文件,此时是插件自己合:
settings.json(手工编辑) > row config(cordis.patch.yml) > 内置默认
为什么第二种里"文件赢"而不是"row 赢":bundle 补丁里写死了
skillDir 与三个开关(modelInvocable / userInvocable / verbose)。如果 row 赢,那个文件就是死的。
Config 与 settings.json 的关系(已变):Config 是唯一的声明处 ——
字段必须先在那里声明,才有官方通道可走。settings.json 只是没有
configEditor 的宿主的退路,不是第二个声明处。
要守住的一条:新增字段时只挑一个家。 一个可配置字段必须先进
Config并标.volatile()(否则官方通道看不见它),同时它的名字必须出现在客户端lib/client.js的FORM_FIELDS里 —— 否则卡片认领不到自己的 namespace, 整页静默消失。tests/audit-client-runtime.mjs从lib/index.js的源码重新解析Config的.volatile()字段与FORM_FIELDS对账。
这是本规格唯一新增的机制。宿主提供两个接口,选择依据不是内容重要与否,而是它变不变:
| 注入什么 | 接口 | 性质 | 代价 |
|---|---|---|---|
| 经验层资源提示(2026-10-19 简化) | ctx.systemPrompt.section({…}) |
稳定;进系统提示词当固定前缀,不随 memoryEnabled 关闭 |
约 265 字 / ~185 tokens/请求;文本自带「若存在 work_log/」的条件 |
| 全局记忆索引 | ctx.systemPrompt.section({…}) |
稳定;进系统提示词当固定前缀 | 在每个请求里,所以按字符预算限制 |
| 待收 N 条 | ctx.systemPrompt.context({…}) |
可变;尾注入在保留历史之后 | 只在内容变化时追加,所以空信箱零成本 |
经验层为什么要死注入:它替代的是工作区 AGENTS.md 钩子——那段提示必须每轮都在,否则
「工作区有历史记录和经验」这件事只在模型主动加载技能时才可见。做成插件段之后,新工作区不必再建文件;
具体流程(现象词搜索、_inbox.md、收尾检查)在 project-work-log 技能与 work_log/README.md,
固定段只做资源提示,不再复述操作手册。skill-only 场景仍用 AGENTS.md(见 templates.md §10)。
为什么索引用 section 而不是 context:索引变动很少(只在 memory collect / index
之后),而它每次都是同一段文本 —— section 进的是可缓存前缀。
为什么待收条数用 context:它每条消息都变;放在前缀里会让整个提示词缓存失效,
而它的正确位置是"排在保留历史之后",那正是 context 的定义。
两条硬规矩:
- 索引只放 id 与一句话,正文永远不进注入 —— 按字符计(软 800 / 硬 1500, 超硬上限报错而不是截断)。要正文就让模型调工具。
- 空的东西不产生任何输出。待收 0 条时
context返回空串;索引不存在时section返回空串。一条"这里是空的"的提示本身就是噪声,而且它会日复一日地消耗每个请求。
一个已知限制(不许假装没有):规格原本说索引"按本工作区的 applies-to 取交集",
但装配期拿不到工作区根 —— 实测 process.cwd() 是 profile 目录,
sandboxPolicy.resolve({}) 无会话时回同一个兜底根。所以第一版的 section
注入全集;索引本身按设计极小(只有 id 与一句话),全集是可接受的。
要真正按工作区过滤,得先证明装配上下文里能拿到工作区——那是另一件事,不做就不写。
模型要能把新的教训写回记忆,否则"一个工作区学到的东西能被别处用到"就只对已经写进去的
内容成立。写入的判定(来源分档、索引重建、lint)在 journal.py 里,工具不重写那套判定,
只是把它搬过来。
这不违反 §一:技能层仍然能独立跑(CLI 一直在),插件只是多给模型一条不用离开会话 的写路。两边共用同一套规则,因为规则的定义处只有一个。
投递机制用了「邮件 / 信箱」这类词,但那只是形状之一。
准则:跨工作区的目标只有一个——把信息从 A 传到 B。任何具体的载体都是手段。 所以不要为了一个通道去建一套邮政系统:不搞收件人表、不搞优先级、不搞会话管理, 除非有真实的用法要求它。
由此推出两条:
- 传信息的正路是全局记忆。 一条在 A 里确证过的教训,写进记忆后所有工作区都能读到,
而它同时有来源分档、
lint、dream收敛与索引注入。为一次性传话而建的第二套机制, 绝大多数情况下是多余的——inbox只在"这消息对投递方以外的任何人都不该存在"时才用。 inbox刻意保持小:暂存、已读即删、有过期清扫、有上限。它不承担检索、不承担归档、 不承担会话管理。不要在它上面加语义;需要检索或长期价值的内容应该进记忆。
已知的一次例外(2026-10-17):主动送达要一个固定收件会话(memory-spec §十一 规矩 3) 和它的收束规则(规矩 6)。那是"通道"的一部分,不是一个会话管理器:不按消息建会话、 不列对方的会话、不管理对方会话的生命周期(旧会话留着,由用户归档)。
插件在这里的角色是「通道」:ctx.workspaceFiles 把代理自己的 read/grep 限制在工作区内,
而工具体在宿主进程里跑,所以它是唯一能碰 $DSH_HOME 的路。要确认的是这条通道通不通,
不是它长得像不像邮箱。
设置页上任何能点但没有效果的控件,按缺陷处理:要么实现它,要么从页面上撤掉, 要么显式标注"尚未实现"。
理由不是洁癖:这个项目的失效模式反复是**"界面说会做,实际不做,而且不报错"**。 一个诚实的"没做"比一个看起来在工作的开关好得多——后者会让人以为功能坏了, 或者更糟:以为它在保护他。