Repository navigation
Releases: TodayJin/dsh-trilogy
Release list
v0.9.9 — 记忆目录回退跟随 memoryDirName,文档与代码对齐
修复
- registry 条目缺
memoryDir时(手改过的、或旧 schema 写的),GET /trilogy/workspaces曾把记忆目录解析成<root>\memory\memory并忽略memoryDirName;/trilogy/clear与状态接口的回退也忽略配置。三处回退现在统一按cfg.memoryDirName解析,硬编码的cfgMemoryDirFallback()已删除。 - 新增 smoke 护栏:手写一条没有
memoryDir的 registry 条目,再用memoryDirName: "notes"起第二个假 web 上下文,断言/workspaces报root\notes、/clear真正删的是notes/PROJECT.md。
清理
- 删除无人引用的
ARCHIVE_POINTER常量。 - README / DESIGN 与代码对齐:74 项 smoke、默认项目根 = 工作区本身(
marker才爬.git)、PROJECT.md 第四节叫现状、工具表补上memory_search。
测试:112 项全绿(smoke 74 / client-render 9 / client-interact 29)。
v0.9.8 — 跟进 DSH 会话格式 v4:source 的 kind 自己报出生产者名
新版 DSH(0.1.7-rc.2)换了会话格式,旧写法会被直接拒收 —— 这次就是修这个。
出了什么事
DSH 0.1.7-rc.2 把会话读成 format v4。v4 废掉了 source 上那层 {kind:'plugin', plugin:<id>} 包装:dsh-session-format-v3-to-v4 的 source() 一见到它就抛
format v4 message requires a producer-owned source kind
而这个插件往会话里塞的每一样东西 —— 记忆块、更新通知、收尾提醒、收拢后的占位 —— 全是那个形状。在新宿主上,它们会在写进去的那一刻被拒。
改了什么
source 的 kind 自己报出生产者名:{kind:'plugin:trilogy', form:...}。
读的时候仍然认老形状(两个分支都收)。这不是客气:升级之前写下的会话里还躺着带 plugin 键的块,认不出来就会把同一份记忆再发一份,同一块内容在上下文里叠两层。
验证
- 111 项全绿:73 宿主 + 9 渲染 + 29 交互
- 新护栏红测过:把常量翻转回老形状,只有「injected messages carry a producer-owned source kind」这一条变红;其余用例都不读 kind,没有它这条回归会静默通过
- 活体:本机新 DSH 上的真实会话(
session.v4.jsonl.zstd)里解出plugin:trilogy的 source,SessionFormatError0 次 - 其余契约逐项核对,都没变:
agent/pre-step、agent/turn-stopping、session/event、tools/result、defineTool+presentCall、createUserMessage、surfaceOp/sourceEventSeqs、dsh.bundle/dsh.client清单字段、settings.section与conversation.input.left两个座位及其 standard props、客户端模块加载器的静态表 —— 所以客户端半边不用改
注意
本插件在 profile 里装的是 link:,改完要重启 dsh web 才会生效。
v0.9.7
这个版本做了什么
面板里那条**「已清除」的工作区,现在可以从列表里拿掉**了。
registry 是索引,不是存储 —— 记忆文件被清掉的工作区故意还会留在列表里,好让你知道它曾经有记忆。对记录来说这是对的,对列表来说是错的:一直没有出口,所以列表只增不减(我自己的探针脚本就用五条死掉的 %TEMP% 记录把它撑大过一次)。
出口刻意做得很窄:按钮只出现在「已清除」的行上,POST /trilogy/forget 只删 registry.json 里那一条键,磁盘上一个字不碰。在那个工作区再开一次会话,它会被重新登记 —— 正是这个可逆性,让这个动作只动索引成为安全的。
- 宿主:第 12 个端点
POST /trilogy/forget,同一个 loopback 围栏内 —— 缺 root 400、不在 registry 404、成功 200{forgotten, memoryDir}。 - 界面:行的操作区是独立的 flex 项(长路径压不扁它),按钮只对宿主报「已清除」的行渲染。
- 测试:宿主护栏走完 400/404/200/404 全序列,并断言事后磁盘上的记忆文件逐字节不变;交互套件的假宿主真的应答
/trilogy/forget,点击路径是端到端的。红测:改掉按钮类名 → 交互那条红;拿掉 registry 删除 → 宿主那条红。
三套合计 72 + 9 + 29 = 110 项全绿,行尾未破。
完整改动:v0.9.6...v0.9.7
v0.9.6 — 日志名额按区域分配,每个部分都有封顶
日志名额按区域分配,每个部分都有封顶;注入块头部从此常驻一行账目。
先说量到了什么
在真实工作区上量的,不是估的:
D:\Today\interface-adapter:旧的 64 KB 天花板下,日志一整段进不去 —— 一条都没有。footer 写着「预算连一条也装不下」,而且已经这样好几天了没人发现,因为账目只在丢东西时才出现。- 本工作区的 91 条日志横跨 4 个互不相干的主题(插件本体 / DSH 部署排障 / 第三方插件 / 网络环境)。「取最近 15 条」在这种日志上必然偏斜:最近 15 条里 8 条是插件开发、5 条是别的插件,一条网络相关的都没有 —— 而那唯一一条网络记录,恰好卡在切线上。
改了什么
总量:默认 64 KB → 160 KB,并按窗口封顶
生效值 = min(配置值, 窗口 × 16%)。固定字节数在两头都是错的:太小扛不住成熟工作区,太大等于给小窗口的模型塞一个吃掉它上下文的块。实测:1M 窗口给满,128K 窗口降到 20,480 并逐档降级。
第一级:按种类封顶(不是配额),余量给日志
PROJECT.md ≤ 35%、DECISIONS.md ≤ 20%,剩下全给日志 —— 某个文件用不到自己的份额时,余量流向日志而不是浪费。
PROJECT.md 再按它自己的五个小节分别封顶
这一步比整体封顶有效得多:实测 57 KB 的 PROJECT.md,整体封顶只降到 56 KB,按节封顶直接降到 18.1 KB(64 KB 预算下)/ 38.8 KB(160 KB 下)—— 小节点原样保留,只切超长的那一节,切处在注入文本里明说「本节已超出注入上限 N KB,请就地精简」。
第二级:日志名额按区域分配,三条规则
- 每个区域保底 1 条 —— 没有这条,聊到安静的区域就一条都看不到
- 其余按注意力权重
Σ 0.5^(条目年龄天数 / 14)—— 最近干得多的多拿 - 单个区域不超过公平份额的 2 倍 —— 没有这条,一个热门区域会吃掉全部日志
区域是条目上的标签,不是新文件
模型在 memory_checkpoint 的可选 area 里给名(零额外模型调用),插件只负责把它压粗:已有名字复用(大小写不敏感)→ 满 4 个之后新名字归 通用 → 留空也归 通用。宁可粗,不要碎。文件仍然是那三个 —— 每多一个文件就多一处「模型忘了更新」,而更新是这套设计唯一的命门。
旧日志行为完全不变:没有标签的条目全部落 通用(实测头部显示 区域:通用(97))。
头部常驻一行账目
区域:… / 本次注入:… / 文件实际:… —— 成熟工作区会先撞天花板,账目不能等到丢东西才出现。
验证
- 108 项全绿:71 宿主 + 9 渲染 + 28 交互
- 四条新护栏逐一红测:把「保底 1 条」「2 倍上限」「区域 ≤4」「按节封顶」分别退化,各自只让对应的那一条变红
- 端到端(真实工作区脱敏副本):64 KB → PROJECT 57→18.1 KB、日志 15/89;160 KB → 38.8 KB、DECISIONS 20/20、日志 15/89
- 重启后在真实宿主里实调:
area被接受、标题写成## 2026-09-18 · 插件开发、地图立刻变成区域:通用(97) · 插件开发(1)
注意
area 是新增的工具参数。改动宿主插件后需要重启 dsh web 才会生效(否则调用会被参数校验拒掉)。
v0.9.5 — 收尾提醒不再多出一个回合
修复:收尾兜底提醒不再「自己回答自己」,也不会再开一个回合。
症状
对话结束之后插件注入一段提醒,DSH 又输出一段话;而很多「以回合末条为准」的功能(产物行、回合导航、预览)把那句「记下了」当成本回合的输出,真正的回答被折叠到回合中间,要展开再滚动才看得见。
根因
兜底提醒用的是 agent.steer。按 dsh-agent 的接口定义,steer 是「送到最近一步,空闲驱动会开一个回合」—— 它把已经结束的回合续下去,模型对提醒的回复就成了这一回合最后一条助手消息。
两条错路都试过并实测否掉
| 做法 | 实测结果 |
|---|---|
agent.steer |
续出一步,模型的回复成为回合末条 ❌ |
agent.send(msg, "next-turn", false) |
上一回合确实干净地 turn/end 了,但 wakeup: false 只挡住「唤醒睡着的驱动」 —— 驱动仍会把它当活儿排干,日志是 turn/end 紧跟又一个 turn/start ❌ |
现在的做法
欠着,不投递:turn-stopping 只置一个待办标记,由下一次 agent/pre-step 把它追加进那一步。
于是它和用户的下一条消息在同一步被模型看到 —— 上一回合保持自己的末条、不为它开新回合、提醒本身一条没少。真机日志:
assistant/message ← 真正的回答
step/end
turn/end reason=completed ← 回合到此结束,后面没有任何 step/start
…(用户下一条消息)…
user/message src=user
user/message src=trilogy-nudge ← 同一步、同一次请求
request/header
文案随投递时机改对
同一段文本里还有两处因改时机而变得不对的地方,一并修掉:
- 开头
这一轮结束前→上一轮的收尾检查(提醒现在是下一轮才到,旧措辞描述的时机已经过去) - 路由表
这一轮做了什么→上一轮做了什么 - 末句
…就直说,然后停→…就直说;记完或跳过之后,继续处理本轮用户的消息—— 旧版提醒独占一步,说「停」没问题;现在它和用户的新消息在同一步,再说「停」会让模型把用户这一轮的任务也停掉
验证
- 护栏:新检查断言
steer与send都不得被使用,且提醒必须随下一步的消息到达;把实现退回steer立刻红 - 三套测试 67 宿主 + 9 渲染 + 28 交互 全绿
- 真机验收通过(见上面的日志片段)
代价(诚实说明):若会话再无下一回合,这条提醒就不会出现 —— 保底弱了一点,换来的是本回合的收尾永远是真正的回答。想彻底关掉用 nudgeOnTurnEnd: false。
v0.9.4 - 更新只追加通知,不再改写历史
Fixed
- 更新不再改写历史。 以前每次记忆变化都会把历史里那份记忆块原地替换掉。实测(7012 次请求):携带这种替换的请求只有 90 次(1.3%),却吃掉全部全价输入的 58%(24.25M / 42M),平均每次 279k,而普通请求只有 3k —— 贵 93 倍。 根因是 prompt cache 是前缀缓存:在历史中间改写一条消息,会让它之后的全部内容失效;而宿主还会因此开一条新的请求序列(
requestSurfaceGeneration !== surface.replaceGeneration),连系统提示词一起重写 —— 改写一次,整段上下文付两遍。 - 现在的做法:整块一个会话只发一次,之后不再动它;记忆再变时只在末尾追加一行 ~200 字节的提示(「记忆已更新,需要时用
memory_read」)。只有在 surface 上已经没有活块时(会话第一步,或块被压缩掉)才重新发整块。 - 恢复的会话不再被误报。 新进程没有 per-session 摘要,所以它的第一步看起来「像变了」—— 以前会因此发一条假的「记忆已更新」。现在改为比较 surface 上那份块的实际文本与新渲染的文本,而不是假定「新进程 = 新内容」。
- 读不出 surface 时按「已经发过」处理。 当成「没有块」会每次变化都追加一整块,把这版修复的意义抵消掉。
- 存量收拢保留,但只在多于一份时触发,且只能逐条单节点替换 —— 一个大 range 会被
assertProvenance要求列出其中所有被遮蔽的节点,而旧块之间夹着 assistant / tool 消息,那等于把对话一起删掉。
Tests
- smoke 的假宿主现在真的模拟 session 的 append / replace 语义并统计改写次数。新增三条护栏:更新绝不改写历史(
replaced() === 0)、占位和通知不算活块、通知不会触发收拢。 - 顺带修好一条已经失去牙的旧护栏:「块被压缩掉后重注入」原来只断言消息数为 1 —— 而一条通知也是 1 条。现在它会检查
form。 - 三条都实测会红。三套件 67 + 9 + 28 = 104 项全绿。
Docs
- README / DESIGN 的注入说明与测试计数已同步;
PROJECT.md的注入语义小节记录了「为什么更新必须是追加」,以及这次成本是怎么量出来的(解会话日志里的usage,其中inputTokens是未命中缓存、按原价计费的那部分)。
真实环境已验:重启后 miss = 0k(修复前携带替换的请求平均 279k)、0 次 replace、live surface 仍是 1 份整块。
67 宿主 + 9 渲染 + 28 交互,全部通过。
v0.9.3 - 注入原地替换(不再按次累积)+ 面板三处修复
Fixed
- 注入不再按次累积。 以前记忆内容一变,就往会话里追加一整份全量快照 —— 一个真实会话攒到了 18 份,占其 surface 约 40%。根因:宿主
agent/pre-step的messages不是会话历史,loop 用inbox.claim(...)建输入与next()的默认值,然后把返回的每一条都session.append(..., {surfaceOp:"append"})追加。在那里返回消息只能「加」、不可能「换」。 - 改成原地替换:把新块以
surfaceOp: {op:"replace", startSeq, endSeq}+sourceEventSeqs直接写到 session 上,会话里始终只有一份。替换被宿主拒绝时退回追加,最坏不比上一版差。 - 顺手收拢存量:注入时检查 live surface 上自己所有的块,最新那条换成新块,其余每一条各发一次单节点 replace,换成一行的 ~30 字节占位(
form: "trilogy-superseded",因此不会被再认成活块)。实测某会话 22 份 → 1 份 + 21 行占位,回收约 680 KB。 - 面板:「移除 Memory 段」后编辑器不再残留旧文本。
bootAction只刷新了徽章、没刷新instruction,整份编辑器打开时拿到的仍是删除前的文本 —— 而照它保存会把刚删掉的段原样写回去。 - 面板:指令文件的编辑框不再顶出卡片。 插件样式表从没设过
box-sizing,而宿主是逐组件重置、没有全局重置,于是编辑框的width:100%把 padding 与 border 加在外面,比卡片宽出 22px。一条作用域内的重置同时修掉编辑框、左栏筛选框与工作区行。 - 面板:归档页签的「导出 / 导入」回到同一排。 两者原本是同一条换行行里的独立兄弟元素,归档时的长提示把「导入」挤到了第二行;现在它们是一个
flex:0 0 auto的整体,由提示先让出宽度。
Tests
- smoke 的假宿主现在真的模拟 session 的 append / replace 语义,并且按 loop 的方式把每步的消息落盘。不做这件事,「追加」和「替换」在测试里看不出区别 —— 这正是上一版改动测试全绿、生产环境完全没生效的原因。
- 两条新护栏:原地替换(短路替换即红:
a replacement must go to the session, not the step list)与收拢旧副本(短路收拢即红:exactly one full block may survive, saw 5),后者同时钉住占位必须 < 400 字节,因为收拢的全部意义就是腾空间。 - 渲染套件钉住
box-sizing重置与按钮分组;交互套件钉住「编辑器显示的是现在的文件」与「归档页签两按钮同行」。 - 收拢只能用逐条单节点 replace:
assertProvenance要求sourceEventSeqs列出区间内所有被遮蔽的节点,而旧块之间夹着 assistant / tool 消息 —— 一个大 range 会把这段对话一起删掉。这条已在真实会话上验证(21 条收拢全是startSeq === endSeq)。
Docs
- README / DESIGN 写清注入路径,包括为什么替换不能走 pre-step 的消息表;测试计数 62 → 64、7 → 9、26 → 28。
64 宿主 + 9 渲染 + 28 交互,全部通过。
v0.9.2 - 归档跨轮不再倒序
Fixed
- 归档跨轮顺序。
ARCHIVE_HEADER写着「最新在最上,形状和SESSIONS.md一样」,但每轮从活日志搬出来的批次是追加到归档正文末尾的:第一轮看着没问题,从第二轮起归档变成「老批次在前、新批次在后」(后搬的批次在时间上更新),设置页「归档」页签的排列也跟着乱。 - 修法按你选的 (a):用宿主侧那套认围栏的
splitEntries拆出归档的头部与正文,写成头部 + 本轮搬出的批次 + 原有条目。
Tests
- 新增「连搬两轮」的护栏:整份归档必须全局最新在最上,头部不能重复也不能被顶下去,指针要把两轮的条数累计报出来。实测会红 —— 换回追加顺序时它报
["first 3","first 4","first 5","second 3",…]。
Docs
- README 的「归档与恢复」写明归档也是最新在最上;测试计数 61 → 62。
62 宿主 + 7 渲染 + 26 交互,全部通过。
v0.9.1 - 面板里记忆文件只读,「重新读取」挪进页签条
Changed
- 三个记忆文件在面板里变成只读。
现状/决策/日志页签不再提供「编辑」,手改记忆文件的那条路径整体删掉(编辑器、保存 / 取消、编辑中,未保存的提示、editing/draft状态),不留死代码。写入的正路是会话里的memory_checkpoint,或者导入记忆包。 - 「重新读取」挪进页签条,就在
归档左边 —— 它重新读取的正是这些页签选中的那个文件。 - 归档本来就只读,现在四个页签一视同仁。
- 顺带修掉一个连带问题:页签切换过去会关掉打开着的指令文件编辑器(旧代码里两个编辑器互斥)。互斥关系没了之后再那样做,只会静默丢掉没保存的草稿,所以切页签不再动指令编辑器。
Unchanged
AGENTS.md的「编辑整份文件」照旧保留,它是界面里唯一的编辑器。- 宿主
POST /save的白名单没变 —— 只是界面不再提供记忆文件的入口。
61 宿主 + 7 渲染 + 26 交互 = 94 项,全部通过。两条新护栏都实测会红:把「重新读取」的插入点从 归档 前改成 现状 前 → 页签顺序测试红;在工具栏塞回一个「编辑」按钮 → 只读测试红。
v0.9.0 - 删掉跨工作区总览
Removed
- 「全部工作区」页签。 用户反馈没用。界面删掉之后
GET /trilogy/overview就是死代码,所以端点一并删除 —— 端点从 11 条减为 10 条。 - 客户端一并清掉:页签按钮、它的内容分支、
overview状态与loadOverview、以及三条.dsh-pm-overview-*样式。归档分支在三级三元里上移一级并重新缩进,不留悬空层。
Docs
- 设置页功能表里的那一行、端点数量与清单、以及测试计数。
- 顺手校准了测试计数 —— 它们从 v0.7.0 起就没跟上(README 还写着 59/6/27)。
61 宿主 + 7 渲染 + 26 交互 = 94 项,全部通过。