# 版本列表(由 dsh-git-push doc-version 维护) ## 版本列表 | 版本 | 内容 | |------|------| | 1.4.13 | **修 arguments 形态修复能力:必须同时改助手侧与调用侧,否则日志被判 corrupt**。同一次工具调用在日志里出现两次——`assistant/message` 的 `tool-call` 块与 `tool/call` 事件;宿主加载时用严格相等比较两者的 `name` 与 `arguments`,只改一侧会抛 `tool/call … does not match one advertised tool call`(比原来的投影 NaN 更硬:连加载都过不去)。本版:① `repairToolCallArguments` 先规范化助手侧、再用**同一个字符串**写回对应 `tool/call`,保证逐字符一致;② `findToolCallArgumentIssues` 同时检查两侧(此前只查助手侧,导致「助手已合规、调用侧仍是对象」的日志被误判为无需修复);③ 修复条目带 `where` 字段区分两侧来源。单测补到 10 项,含两侧一致、单侧对齐、双合规零改动。实测:三个受影响会话在调用侧补修 30 处(2 + 26 + 2)后,按宿主校验规则复算 tool 生命周期错误 0、未闭合调用 0、估算 NaN 0。 | | 1.4.12 | **新增「修复 tool-call 参数形态」能力**(`POST /api/session-conductor/repair-arguments`)。背景:`ToolCallBlock.arguments` 契约是「模型产出的原始 JSON 字符串」,第三方适配器若把上游 chunk 的参数对象直接写进 assistant/message,token-meter 估算器取 `block.arguments.length` 会得到 undefined,`Math.ceil(undefined/4)` 即 NaN,进而 surfaceTokens → projectedTokens 变 NaN,投影 schema 拒收 —— 表现是整条会话打不开。本版把修复做成插件能力:`lib/sessions/arguments-repair.js` 负责识别与序列化(纯逻辑,只动 arguments、不动 seq 与其它字段),`lib/features/arguments-repair-ops.js` 负责落盘(定位日志 → 解码 → 修复 → 校验 → detach → 备份 → 原子写回 → 清投影缓存)。接口支持单会话修复与全库只读扫描(`{all:true, dryRun:true}` 只给清单不写盘),修复幂等、可重复调用。实测:三个受影响的会话共 30 处对象形态全部修好(2 + 26 + 2),修复后坏块 0、NaN 0、seq 连续、每帧校验和齐全,投影可重新计算。新增 7 项单测覆盖序列化、形状识别、幂等、缺字段跳过。 | | 1.4.11 | **会话 id 写法统一:带不带 `session-` 前缀视作同一个会话**。新增 `lib/sessions/id.js`(`bareSessionId` / `sessionIdVariants` / `sameSessionId` / `matchSessionId` / `canonicalSessionId`),比较一律按裸形式(去掉 `session-`)、返回一律用磁盘/索引里真实存在的那一种写法。接入六处入口:会话目标解析(完整与前缀匹配)、会话元信息查找(内存与落盘索引)、会话操作路由与动作路由的入参归一(含批量 `sessionIds`)、`send_to_session` 投递前归一、成员定位里的三处手写前缀处理。此前磁盘上两种写法并存(主会话目录带前缀、子代理会话是裸 uuid),各入口比较口径不一致,表现就是「同一个会话换个写法就报找不到会话」。新增 9 项单测覆盖前缀归一、前缀片段匹配、多命中返回候选、未命中不臆造。 | | 1.4.10 | **修「切换模型后思考强度残留」——descriptor 同步补上 reasoningEffort 清理**。现象:把成员切到不支持推理档位的模型后,整轮直接失败并报 `UNSUPPORTED_REASONING_EFFORT`(例如目标模型本不支持 reasoning,请求里却带着上一个模型的 effort)。根因:子代理冷恢复的档位来自 `subagent/descriptor.agentReasoningEffort`(宿主从 descriptor 读取),而切换时的 descriptor 同步只改了 `agentProvider`/`agentModel`,把旧模型的档位原样带了过去。修复:descriptor 同步时删除 `agentReasoningEffort`(档位属于模型,换模型后交给目标模型默认,宿主自身在路由变更时也是这么处理的);同时把「descriptor 里还留着 effort」本身判定为需要同步的状态,即使 provider/model 已指向目标也会被清理。现场:扫描 50 个子代理会话,3 个 descriptor 残留档位,其中 1 个与当前模型不兼容并已清理,另外 2 个与模型自洽(保留),今后切换时会自动清理。 | | 1.4.9 | **「撤回最后一条消息」升级为「撤销到指定位置」(`undoToMessage`,旧名保留为包装)**。新增三种定位:`atSeq`(事件序号)、`atTime`(毫秒时间戳,取第一条不早于它的)、`matchText`(文本片段,只匹配真实用户消息,注入内容不算);都不给仍是「最后一条用户消息」。切点策略:目标消息常落在回合中间(宿主会在回合运行中把新消息 spliced 进当前 step),直接切会留下「有 turn/start 无 turn/end」的悬挂回合,因此一律向前回退到最近的 `turn/end`;退不到时退到最近的 `step/end` 并补一条 `turn/end`(reason=aborted/user)——宿主校验要求 turn/end 必须对应已闭合 step 的 open turn。配套:写回前校验、detach 防内存态回写、备份到 `.undo-backup/`、写后清理投影缓存(不清会「界面还是旧的」,或因 `inheritedEventCount` 超出日志长度拒绝恢复)。切点与重建逻辑抽到 `lib/sessions/undo-core.js`(零宿主依赖),新增 7 项单测覆盖序号定位/文本定位排除注入/找不到报错/退到 step/end 补 closer/默认最后一条/边界报错/重建文本。实测:对一条 7.9 万事件、45 MB 的会话按文本定位撤销到 14:23 之前的回合边界,删除 1789 条事件,保留 7.8 万条;写后校验 seq 连续、每帧校验和齐全、目标消息与所在回合零残留。 | | 1.4.8 | **成员清理补上「回收站窗口」:软删除后立刻从名册消失**。删除是软删除(文件保留、可还原、默认 7 天到期才真正删盘),因此只看「会话目录是否存在」时,刚删掉的成员仍会出现在名册里,要等回收站到期/清空才消失。改法:`filterDanglingMembers` 增加回收站感知 —— 会话 id 命中回收站索引即视为已删除,不参与列表与目标解析;目录存在性判定保留(回收站清空后走同一条过滤)。新增 `trashedSessionIds(ctx)`(同步读回收站索引,无新依赖)。 | | 1.4.7 | **修自动重命名整条链路失效:推理型模型只输出思考内容时正文为空,旧实现一律判失败**。现象:面板点「分析」/ 自动重命名报「LLM 输出无法解析为 JSON(模型不可用或输出异常)」,换个模型就正常 —— 说明模型本身可用,问题在取文本的方式。根因:解析只读 `text` 块;推理型模型会先输出思考内容,`maxTokens` 被思考吃掉后正文为空,解析必然失败。三处修正:① 新增 `pickParseSource`:正文非空优先正文,正文为空回退**思考块**(JSON 通常就在其中);② 输出预算 200 → 512(只要一个短 JSON,但太小会被思考挤没);③ 失败文案带上实际使用的 `provider/model`,便于直接判断该换哪个模型。同时把纯解析函数抽到 `lib/features/rename/parse.js`(不再被宿主包依赖拉住),新增 5 项单测覆盖正文优先/空正文回退/双空返回空串/代码块围栏/强制标题回退。 | | 1.4.6 | **已删成员的清理:删会话目录 + 名册读取层过滤悬空条目(父会话日志不动)**。成员记录存在两处:父会话日志里的 `subagent/catalog` 事件(名册数据源)与成员自己的会话目录。删掉成员会话目录后父日志那条记录仍在 → 名册会列出点不开、切不动的幽灵条目;而删父日志事件会造成 seq 缺口、连带压缩引用失效,代价风险都不划算。改法:名册读取层按「会话目录是否存在」过滤 —— 既无 live agent、又无会话目录的条目不进列表、不参与目标解析(`findTargetAgent` 命中悬空成员时给出明确提示,而不是误报状态);目录扫描带 5s 缓存,删除会话后立刻失效缓存,列表即时刷新。两处消费点统一走 `filterDanglingMembers`:`collectTeamMembers`(列表/批量解析)与 `findTargetAgent`(单成员定位)。 | | 1.4.5 | **修子代理切模型后冷恢复回落——descriptor 同步纳入切换流程,override 不再必需**。子代理冷恢复(`reason=resume`)的请求模型直接取 `subagent/descriptor.agentProvider/agentModel`,不经过官方 `model/selection` 投影链路,因此只写 selection 事件对未挂载/冷恢复的子代理无效,每次唤醒都回落到创建时的模型。三处修正:① 挂载路径切换后同步改写 descriptor(原只写 selection,成员卸载再唤醒即回落);② descriptor 同步提到「selection 是否已相同」判定之前(原遇重复切同一模型就提前返回,descriptor 永远修不到);③ `modelCatalog` 只用于校验目标合法,拿不到时不再静默放弃同步(原把它当硬前置,目录服务不可用即同步失效)。验证:新建子代理→不带 override 切到目标模型→唤醒,`reason=resume` 的请求头直接使用新模型,无回落 | | 1.4.4 | **修「UI 切了模型、请求不动」——模型切换改为官方链路优先,override 降级为显式参数**。事实:官方 model/selection 链路本就当轮立即生效且跨轮持久(全新未注入插件的会话全量事件实测:三次 UI 切换均紧接 `request/header reason:"change" startsSeries:true` 当轮生效;另一会话一次 selection 延续 4000+ 事件未回落),而插件原先无条件写持久 override,`agent/request` 每轮强制改写请求,优先级压过官方 UI 的选择——界面切了、请求仍走 override 里的旧模型,且不报错。改法:`switchAgentModel` 新增 `options`;`persistSwitchIntent` 仅在显式 `override:true` 时写 override,默认只写 `model/selection` 事件并回执「未写 override(走官方 model/selection 链路)」;`/model-switch` 新增可选尾参 `[override]`、`set_member_model` 新增 `override` 布尔参数(单切/批量透传);`create_teammate_with_model` 保持旧行为(override:true,创建时钉模型本就依赖持久覆盖)。存量 override 不清理,hook 仍尊重(这些会话 UI 切换走同步机制)。验证:干净会话全量事件验收——UI 切换当轮生效 ×2、跨轮持久、插件切换回执「未写 override」 | | 1.4.3 | **新增「从会话导出包恢复/覆盖会话」能力(`restore-from-export`)——把备份覆盖回运行时格式,并让新内容真正生效**。背景:DSH 导出的会话包是**明文**目录结构(`session.v4.jsonl` + `subagents//session.v4.jsonl` + `media/`),而运行时读的是 `session.v4.jsonl.zstd` —— 带校验和的**多帧** zstd 容器、首帧恰好一行 header。所以「把备份拷回去」不是 `cp` 能做的,直接拷会被宿主判「会话文件 总和检查码不匹配」或 corrupt。新增 `lib/sessions/export-restore.js`:① `encodePlainToZstd` 把明文编码成官方容器(固定 `ZSTD_c_checksumFlag`;按 `eventsPerFrame` 分帧,避免读取端一次性解压整帧的内存尖峰;写前校验 header 与事件 seq 严格连续,不连续直接拒绝);② `scanExportDir` 扫导出包并解析每个会话的目标目录(优先按 id 找现有目录,找不到就用同族目录新建,避免自己实现路径编码);③ `restoreSessionsFromExport` 默认 dryRun,真写盘时写 tmp 后 rename 原子替换、写完逐帧复核(首帧契约 + 每帧校验和 + 事件数)。**关键:恢复后必须让新内容生效**——DSH 有三层状态(日志 / projcache / 进程内存 agent),只覆盖磁盘时界面仍显示旧内容(本机实测:日志 11:20 已覆盖、projcache 停在 11:06,打开会话看到的就是替换前的内容)。因此恢复后自动清掉该会话的 projcache;传入插件 ctx 时再调 `detachSessionAgent` 把 live 会话卸载回冷状态,没传 ctx 则如实标注「未尝试 detach,需重载/重启」。新增路由 `POST /api/session-conductor/restore-from-export`(body: exportDir / sessionIds / dryRun / eventsPerFrame)。新增 `test/unit/test-export-restore.mjs` 7 项断言(每帧带校验和 / inherited 标记回显 / seq 不连续拒绝 / 首行非 header 拒绝 / dryRun 不写盘 + 产物合规 / projcache 必清 / exportDir 不存在如实报错) | | 1.4.2 | **修 1.4.0 幂等守卫在重载场景的反效果**。v1 守卫用 window 级 Set「同 id 只注册一次」,但宿主重载流程是 invalidate(只清主模块 id)→ 执行新 bundle;守卫见 Set 里有主 id 就跳过,宿主加载后校验 `factories.has(主id)` 失败,报 `loaded without registering "dsh-session-conductor"`,插件前端整体加载不出(比 duplicate 更糟)。v2 按「谁会被宿主清掉」分治:主模块 id 每次执行**无条件注册**(宿主已为它 invalidate,合法且必须);分片 id 每次 bundle 执行加周期后缀(`group@1`→`group@2`),天然不与旧表冲突;同周期内仍用 Set 防重复。代价:旧分片 factory 留在模块表(几份小闭包,硬刷新即清)。行为自测:两次 bundle 执行模拟重载,主 id 两次都注册、分片 @1/@2 不冲突、同周期重复被跳过 | | 1.4.1 | **修「UI 切了模型,AI 要回完一轮才切过去」**:`agent/request` hook 原先把「读官方 pending → 同步 override」放在 `await next()` 之后。请求组装阶段官方链路可能仍解析出旧模型(`system-prompt/assemble` 被缓存时 `selection.assembled` 停留旧值),返回阶段再拿旧 override 一盖,本次请求就定型了;残留的 pending 要等下一轮才被读到,于是切换慢一拍。改为**进入即同步**(pending 只会被请求后的 `consumeSelection` 清掉,waterfall 进入时必然还在,此时同步最可靠),返回阶段保留一次兜底(防投影尚未 fold 到最新);两条路径的日志分别标注「请求前」「返回兜底」便于排查 | | 1.4.0 | **本次四次迭代合并发布:新增成员轨迹读取 + 三处修复 + 两处健壮性改造**。——**① 新增 `read_member_trace`(读子代理完整执行轨迹)**:子代理只有最后一条回复会被宿主自动发回主代理,中间的思考与工具调用看不到 —— 实测一个执行型子代理单轮可产生 115 条 assistant 消息(全会话 1382 条 / 939 条带 reasoning),主代理只拿到一句结论。实现按 turn → step 组织事件,同时保留 reasoning 与 text,把 tool/call 与 tool/result 配对;`user/message` 用**白名单**过滤注入(只认 `source.kind` 为 `user` / `agent-message` —— 实测注入有 skill-catalog / runtime-context / time-context / tool-jobs / plugin:* 等十余种,黑名单漏一种就会让任务文本被几百 KB 注入淹没);轮次裁剪放读完统一 `slice(-rounds)`,不在 `turn/start` 里 `shift`(会让 turn/step 引用指向已移出数组的对象,表现为「某轮的思考跑到另一轮」)。参数:session(成员名/id/前缀/标题,空=自己)、rounds(默认 1,上限 20)、detail(summary/full)、include(可裁 reasoning,text,tool)、maxCharsPerItem。同时抽出流式读日志公共底座 `lib/sessions/log-stream.js`(帧流式 + 行缓冲 + 跨帧解码器收尾 + 坏行跳过计数),记忆取回 `readRecentUserTurns` 与轨迹读取共用同一份实现。——**② 会话目标解析抽公共 + 修裸 UUID 会话解析不出来**:旧 `resolveSessionTarget` 用 `dir.startsWith("session-")` 判断会话目录,而子代理/成员会话目录名是**裸 UUID**(如 `c9a6a72c-…`),导致完整 id 与前缀都命中不了(工具报「找不到目标会话」)。判据改为「目录里有会话日志文件」(`findSessionLog`,与宿主一致、与命名规则解耦)。解析抽到 `lib/sessions/resolve.js`:`resolveSessionId`(id / 前缀 / 标题)+ `resolveTargetSession`(再补 Team 成员名,动态 import 避免层级倒置),记忆取回改为薄封装共用,顺带让 `recall_memory` 也支持成员名与裸 UUID 会话。——**③ 存储域不可用时降级为文件后备**:宿主 `DomainImpl.close()` 先置 `disposing`、写链排空后才置 `closed`;关闭卡住时句柄长期停在 disposing(`closed` 仍为 false),任何读写抛「domain is closed」,而名字又被 `reserved` 占着无法重新 open ⇒ 所有依赖持久化的接口一路 500,只能重启。新增 `lib/core/domain-fallback.js`:`isUsableDomain` 同时判定 closed 与 disposing,不可用即降级为直接读写域自己的落盘 JSON(只接管 global、unit/tables 原样保留、写入串行化)。同时去掉 `pluginDomain` 里会无限递归的重建分支。——**④ 切换成员模型时同步 live agent 的 `options`**:宿主 `memberView()` 里成员模型取的是 `live.options.model`(agent 创建时的 AgentOptions,官方 `list_agents` 与「查看子智能体」都读它),与插件写的 `model/selection` 事件无关,于是切完模型后实际请求已换、界面还是老模型。宿主 `ReactLoopAgent` 构造是 `this.options = options` 直接引用、未 freeze,可安全改写。新增 `syncLiveAgentOptions()`(含 `Object.isFrozen` 兜底)。**并修掉导致它从不生效的判定**:旧条件要求 `typeof live.ctx.on === "function"`,但 agent 空闲后 `ctx.scope` 已 unwind,于是「空闲但仍在内存」的成员被误判成未挂载、永远走不到同步路径;现只看 `!live?.session`(写事件与同步 options 都只需要 session)。——**⑤ 修 `readLastRequestModel` 漏读请求头**:旧实现只解码最后 8 帧,而一个回合常有上百个事件(step/tool/assistant 各占一帧),`request/header` 往往落在倒数几十帧之外 ⇒ 跑过很多轮的会话被误报成「从未运行过」。实测某成员日志 65 条 request/header、最后一次 seq 8158、末尾 seq 8247,旧实现返回 null。尾部窗口 8 → 400 帧;仍无命中则退回全量流式扫描。——**⑥ 修前端重载崩溃 `duplicate factory registration`**:宿主 `cordis-client-runner` 用 invalidate-then-register 保证重载合法,但只清插件主模块一个 id;本插件是分片拼接,group / processing / compaction / conductor-settings 四个独立块各自 `load()` 却没被 invalidate ⇒ `client.js` 的 rev 一变,浏览器执行新 URL 时抛 duplicate 把整个前端带崩。`build.cjs` 拼接时注入幂等守卫 `__dshScLoad`(每页面生命周期内每 id 只注册一次),主块与四个独立块的 `load({` 统一改走守卫。代价是重载后拿到旧 factory(新前端代码需硬刷新),但页面不崩。——新增单测:`test-member-trace.mjs`(8)、`test-domain-fallback.mjs`(4)、`test-sync-live-options.mjs`(4)、`test-tail-request-model.mjs`(4);READM 补 `read_member_trace` 用法并把机制说明移入 `docs/模型与权限切换机制.MD` | | 1.3.18 | **修 override 优先级缺陷:官方界面切换现在会同步进 override**。背景(实测事故):本插件的 `memberModelOverrides` 每轮通过 `agent/request` 瀑布流强制改写请求配置,优先级高于官方链路,于是 override 一旦写入,用户在主会话界面用官方模型选择器切换**完全无效** —— 界面显示新模型、请求一直走 override 里的旧模型,且不报错、不提示、无清除入口。现场表现为:批量「跟随主代理」把一批会话钉在某个已失效 provider 上,界面切三次、跨三个回合、改会话日志 `request/header`、重启宿主全部无效(override 持久化在 domain 里且每轮覆盖)。修法:官方切换会往 `modelSelection` 投影写 `pending`,而插件用 override 改写请求后 `consumeSelection` 匹配不上 → pending 会保留;现在每轮请求前把它与 override 比对,不一致就**把 override 同步成 pending 的值**(不是清除)—— 官方界面选择立刻生效,同时保留 override 的跨轮持久能力。同步失败只记日志、退回旧 override,绝不打断请求。新增 `adoptOfficialSelection` / `readOfficialPendingSelection`(均导出便于单测)与单测 `test-override-official-sync.mjs`(8 项) | | 1.3.17 | **新增成员换名通道 `clone_member_as`(新建成员 + 搬会话内容)**。为什么只能这么做:宿主的 Team 成员名是**不可变身份字段** —— `team/member` 投影里 `if (prior.name !== member.name \|\| prior.provider !== member.provider \|\| prior.context !== member.context) throw new Error('changed immutable identity fields')`,且只允许 provisioning → active 这一次写入,宿主侧也没有任何 rename 接口。这条路成立的原因是成员名与会话内容分处两地:名字存在 **Lead 会话日志**的 `team/member` 事件里,内容存在**成员自己的日志**里,因此搬内容不会触碰不可变字段。工具流程:解析源成员 → spawn 新成员(占位任务,最低消耗)→ 中断并等它**真正空闲**(运行中的成员以内存态优先,此时改磁盘不生效)→ 搬运:保留目标 header 身份(id/cwd/parentSession)、丢弃源的身份类事件(`session` / `subagent/descriptor` / `subagent/model-selection-policy`)、按目标**重建 descriptor**(可传 provider/model 指定路由)、**seq 从 0 严格重排**、用官方 `encodeSessionText` 编码(**带 checksum**)→ 写前自检(解码回来核对 seq 连续)→ 备份目标原日志后原子替换。三条硬约束缺一条都会把会话写成 corrupt:帧必须带 checksum(否则「会话文件 总和检查码不匹配」)、seq 必须连续(否则 `has seq gap (expected N, got M)`)、目标必须非运行中。**搬运的正确做法是「原样复制」,这一点是三次真实 corrupt 换来的**:最初的实现会重排 seq 并丢弃「不需要」的事件,结果连续撞上宿主三种校验 —— ① `sourceEventSeqs ranges must contain unique earlier seqs`(顶层 sourceEventSeqs/surfaceOp 端点指向了未来);② `session/title messageSeqs must cite distinct earlier human user/message events`(data.messageSeqs 失效);③ `successful compaction/end requires one summary`(只丢了 compaction 的一部分,配对断裂)。根因是会话事件之间布满跨事件引用(顶层 `sourceEventSeqs`、顶层 `surfaceOp{startSeq,endSeq}`、`data.messageSeqs`、`data.shadowedSeqs`、`data.headerSeq`,以及 compaction 的 start→summary/prune→end 强配对),任何重排或增删都会打断它们。**现行实现:事件一条不改、seq 一个不动,只替换 header 身份**(源 seq 本就是 0..N 连续,且实测源事件里 0 处引用自己的 sessionId),仅允许改写 descriptor 的 label/模型路由(纯字符串,不参与引用);写前自检改为**逐条与源对比**(除 descriptor 可读字段外必须完全相等),这条自检等价于「源能加载 ⇒ 目标一定能加载」。**同批还修了拆分引入的两个静默回归**:`features/settings-routes.js` 漏 import `cfg`(导致 `registerSettingsRoutes` 调用期抛 ReferenceError,整个 webServer inject 回调中断,表现为 settings 及其之后所有路由 404)以及漏 import `TEMPLATE_SLOTS`/`TEMPLATE_DEFAULTS`/`TEMPLATE_MAX_BYTES`/`MSG_METHOD_ONLY_GET_POST`/`searchSessions`;教训是「按找未定义函数调用」的静态扫描抓不到**常量与变量引用**,拆分校验必须覆盖到标识符级。新增单测 `test-clone-member.mjs`(4 项:原样复制逐条对比/空源报错/目标不可读报错/跨事件引用原样保留)。文档同时把「让全部子代理跟我用同一个模型」的三种写法(`/model-switch * auto`、`set_member_model({})` 等)写清 | | 1.3.16 | **新增成员权限预设的查看与切换(工具 + 指令),并把模型/权限切换都升级为「参数可空 = 全部成员 / 跟随主代理」**。背景:子代理会话在界面上**无法手动切换权限预设**(界面只对主会话开放),而 preset 一旦指向当前不可用的值,该成员就永久唤不醒 —— 宿主 `permission-presets` 的 `pinInitialPermission()` 在会话发布前执行 `if (preset === AUTO_PRESET && this.autoAdmit === undefined) throw new Error('permission: cannot restore preset "auto" without its active integration')`,异常再被 subagent 的 `coldResume()` 统一包成 `subagent/not-resumable`。**此处修正 1.3.15 的判断**:当时把真因写成 `approval/policy` 的委派快照,后续实测证明不成立 —— `approval/policy` 只有 `ask`/`never` 两个合法值且 `overrideOf()` 只读 policy 不看 source,真正悬空的是 `permission/preset = "auto"`(`auto` 是「自动审查集成」的保留 preset 名)。新增能力:① `permission.js` —— 折叠读成员权限状态、读宿主 `permissionPresets.catalog()` 校验预设名、切换时**追加一条 `permission/preset` 事件**(独立 zstd 帧、带 checksum、长度 CAS、fsync,已有帧一字节不动,可删尾部回滚);② `lead.js` —— 「跟随主代理」的取值来源:模型只认请求级记录(会话日志 `request/header`),读不到就如实报错、绝不用名册静态字段冒充;权限取最后一条 preset;③ `batch.js` —— 批量应用串行执行、逐个隔离失败、回执带完整名单;④ AI 工具 `set_member_permission` / `list_permission_presets` 与聊天框指令 `/perm-switch`(无参=列成员与预设,`<成员>`=查,`<成员|*|all> <预设|auto>`=切);⑤ `set_member_model` 与 `/model-switch` 同步扩展:target 空/`*`/`all` = 全部成员(排除 Lead、provisioning、failed),provider/model 空/`auto` = 跟随主代理。新增单测 `test-member-permission.mjs`(9 项)与 `test-member-batch.mjs`(9 项)。判据(只读即可确认某个成员为何唤不醒):会话日志最后一条 `permission/preset` 是否为 `auto`,且当前`permissionPresets.catalog()` 的选项列表里**没有** `auto`。**同版本内还做了一次结构拆分(行为零变化)**:`lib/apply.js` 从 1793 行降到 457 行 —— `ctx.inject(["webServer"])` 回调原先 856 行、`ctx.inject(["tools"])` 回调 293 行,二者都被审计 `readability/max-function-length` 拦截;现按域拆出 6 个路由/工具模块:`features/base-routes.js`(语言包+会话列表,注意语言包相对路径从 `./i18n/` 改为 `../i18n/`)、`features/settings-routes.js`(模板/FTS/压缩模型/重命名模型/自动继续闸门)、`features/value-report-routes.js`(价值分析+任务汇报)、`features/session-action-routes.js`(搜索/分析/继续/消息/扫描/detach/五条 repair-*)、`features/member-model/tools.js`(5 个 AI 工具 + 2 条聊天指令注册)。拆分只搬「注册」与 handler 体,逐字保留;每个新模块的 import 路径与导出符号都有脚本校验,apply.js 另做「无未定义标识符」静态校验(因为单测不 import 它,靠测试兜不住)。拆分后 4 个 inject 回调分别为 11/35/21/5 行,全部低于阈值 50 | | 1.3.15 | **记录一次「误导性报错」的真因:`subagent/not-resumable` 的真凶是部署侧「自动审查」开关被关掉,与模型/描述符/文件全都无关**。现象:某个子代理突然再也唤不醒,界面只报 `subagent cannot be resumed (subagent/not-resumable)`;切任何模型、重启宿主、修 descriptor、修帧校验和、截断会话尾部**全部无效**。真因:子代理会话开头有一条 `approval/policy` 事件,`{"policy":"never","source":"delegation"}` —— 它是创建时从父级**快照**下来的自动审查设置。当部署侧关掉「自动审查」功能后,宿主恢复这条委派快照失败,异常在 `subagent/src/manager.ts` 的 `coldResume()` 里被统一包成 `NOT_RESUMABLE`,真实原因被 `cause` 吞掉。把自动审查打开,会话立刻恢复。判据(只读即可确认):会话日志开头存在 `"type":"approval/policy"` 且 `"source":"delegation"`。本次改动:① `switch.js` 新增 `delegationPolicyHint`,切换模型时若发现该会话带委派策略快照,就在日志里给出「先查自动审查开关」的排查提示(只读首两帧,失败返回空串,绝不影响切换);② 本版本表记录完整排查过程与**被排除的 6 项**(observeSession 失败 / descriptor 无效 / 帧 checksum / 写锁 flock 争用 / 双实例抢锁 / parent 未登记 subagent/catalog),避免下次再沿错方向查。教训:宿主把内部异常统一包成用户可见的错误码时,**报错文本指向的方向可能完全是错的**;这类「切换/唤醒」类故障应先确认部署侧的功能开关状态,再动数据 | | 1.3.14 | **修「子代理切模型后仍唤不醒(宿主报 `subagent/not-resumable`)」——切换时必须同步修正 descriptor 路由**。事实:宿主 cold resume 子代理时,provider/model 取自会话日志里的 `subagent/descriptor`(`manager.coldResume` → `materialize` 的 `agentOptions` 用 `descriptor.agentProvider` / `descriptor.agentModel`),`model/selection` 事件**不参与该路径**。因此当 descriptor 仍指向一个已被删除的 provider(实测:配置里移除 `wb` 后,成员 descriptor 仍是 `agentProvider:"wb"`),`materialize` 必然失败并被统一包成 `subagent/not-resumable`,表现为「这个成员永远唤不醒,切任何模型都没用」;而回执还写着「下次运行生效」,具有误导性。改法:新增 `readDescriptorEvent`(从日志头部帧读 `subagent/descriptor`),并在 `appendSelectionEventToLog` 写入 selection 之后调用 `repairDescriptorIfStale` —— 用 `sessionController.modelCatalog()` 取当前可用 provider 列表,若 descriptor 的 provider 已不存在(或 model 不在该 provider 目录里),就把 descriptor 的 `agentProvider` / `agentModel` 同步改成目标路由;改写前校验 seq 严格连续、改写后整份重编码并保留备份。保守原则:拿不到模型目录、或 descriptor 本就有效时,一律不动日志。新增 `test/unit/test-descriptor-route.mjs` 3 项断言(能读到 descriptor / 普通会话返回 null / 修正后路由正确且 seq 连续)。另:`session/end-seed`(无 `inherited` 标记)是**正常的 resume 标记**,不是损坏信号,排查时不要误判 | | 1.3.13 | **性能优化:成员模型切换写会话日志改为「尾部读 + 追加帧」,峰值内存降约一半、耗时降约三成**。事实:`/model-switch`、`set_member_model`、`create_teammate_with_model` 三条链路最终都走 `appendSelectionEventToLog`,原实现是「整份解码 → 取 maxSeq+1 → 拼全文 → 全量 validate → 整份重编码」,一次切换要完整解码 3 次并同时驻留多份全文副本;本机实测 5 万条事件的会话单次切换峰值 417.9MB、耗时 1959ms。新增 `lib/sessions/tail-io.js`:① `readTailEvents` 只解码**最后 N 帧**并逐行消费(帧边界残行用 carry 带到下一帧,不把尾部文本拼成大字符串)② `appendEventFrame` 把新事件压缩成**独立 zstd 帧追加到文件尾**,内含首帧契约自证(首帧必须恰好一行 session header)+ 文件长度 CAS + fsync。`switch.js` 相应改为「首读定位 → 写前尾部 CAS → 追加帧 → 写后尾部自检」,写坏时仍整份回滚;`readLastRequestModel` 改为只流式解码最后 8 帧,并给单行 `pending` 缓冲加 8MB 上限(原先无上限,遇到单行超大事件会无限增长)。**为什么追加帧是合法的**:DSH 会话日志是多帧容器,首帧之后的事件帧在解码层直接拼接,因此「尾部追加一个新帧」与「重写整份」解码后等价,且不回写已有帧——失败不破坏历史。实测收益(独立子进程 + --expose-gc + 峰值采样):5 万条 1959ms/417.9MB → 1415ms/214.7MB(耗时 ↓28%、峰值 ↓49%);2 万条 912ms/169.9MB → 632ms/88.6MB。新增 `test/unit/test-tail-io.mjs` 5 项断言(等价性 / 原有内容逐字节不变 / 首帧契约 / 连续追加不重复 / 非 zstd 文件报错且不改文件)。小日志(4 千条)耗时略升约 25%,是读整份文件与子进程的固定开销 | | 1.3.12 | **修「指定模型建队」写会话日志的并发竞争 —— 会把成员会话写成 corrupt(seq 回退)**。事实:`create_teammate_with_model` 的第 ④ 步要把 `model/selection` 写进成员会话日志,原实现是「读整份日志 → 取 maxSeq+1 → 整份写回」,而紧接着第 ⑤ 步就向该成员投递消息,宿主在同一会话上并发追加 `turn/start`、`agent/inbox/spliced` 等事件 ⇒ 写回时用的是**过期序号**,还把宿主刚写的事件盖掉;实测日志出现 `seq=26` 之后紧跟 `seq=23`,宿主随即判 `released v2 row N has seq gap (expected 27, got 23)`,整个会话历史加载失败。改法(三步防护):① **写前 CAS**:写回前重读日志对比 `maxSeq` 与行数,快照过期就重读重试(最多 3 次)② 备份后原子写回(`backupSwitchLog` 现在回传备份文件路径)③ **写后自检**:seq 必须严格 +1 连续,不合格立刻用新增的 `restoreSwitchLog` 从备份回滚,并退化为「只让选择意图生效、不碰日志」;持续存在并发写入时直接跳过日志写入(不影响切换结果)。边界:该路径要整份解码日志(几十 MB 时有耗时),并发极高时可能放弃写日志,此时模型选择依赖意图文件在成员下次挂载时生效 | | 1.3.11 | **新增「创建团队成员并让首轮真实任务就用指定模型」——宿主原生做不到,由本插件补一层流程**。事实:`spawn_teammate` 工具参数只有 name/description/prompt/context,服务层 `SpawnTeammateRequest` 也只有 `provider`,模型只能来自 agent-team 插件的 `freshProvider`/`forkProvider` 配置。新增 AI 工具 `create_teammate_with_model(name, description, prompt, provider, model, context)`,内部四步:① 占位任务创建(`PLACEHOLDER_PROMPT`,只消耗一条极短提示)② `agentTeams.interrupt` 立刻中断首轮 ③ 用宿主 `waitForChange` 等它真正空闲(取消信号不串到下一轮) ④ 写 `model/selection`(与 `set_member_model` 同一链路)⑤ `agentTeams.sendMessage` 投递真实任务。**两个 provider 是不同命名空间**:`provider`/`model` = 模型 provider(agnes/wb/sa6400…),创建成员要的是**子代理 provider**(`ctx.subagents` 注册表,通常 `spawn`),默认自动取第一个,可用 `subagentProvider` 指定;混用会报 `no subagent provider registered for "x"`(实测踩过)。另修:工具定义的 `render` 必须放在 `output` 内,放顶层会让 `defineTool` 抛错、整个 `inject("tools")` 中止(四个工具一起消失,已在宿主实测复现并修复,并给该注册加 try/catch 隔离)。关键约束:设模型失败时**放弃投递**(否则真实任务会用默认模型跑掉),投递失败如实回执不谎报;中断调用报错时按「成员可能已空闲」继续。仍存边界:占位那一轮确实发生;彻底「创建前定模型」需改宿主 agent-team。新增单测 6 条(参数校验 / 能力缺失 / 无调用方 / 四步顺序与载荷 / 设模型失败不投递 / 投递失败回执) | | 1.3.10 | **修「聊天指令 /model-switch 找不到成员、AI 工具 set_member_model 却能切」——根因是两份实现**。成员定位有两条路径各写一份「谁是成员」:AI 工具在 agent 异步链里调 `findTargetAgent`,`pickTeamCaller` 能用 `agents.currentInitiator()` 拿到发起方,Team 名册读得到;聊天指令的 handler 里这个上下文经常不存在,于是退回 `agents.list()`(Team 队友根本不在里面)⇒ 报「没有匹配的活跃成员」。抽公共:① `pickTeamCaller(ctx, preferred)` 支持显式发起方(`invocation.agent` / `exec.agent`),不再靠猜;② 新增 `collectTeamMembers(ctx, { caller })` 作为**唯一**名册来源(名册 + agents 去重合并),命令的「列成员」与工具/命令的「找成员」共用;③ 命令与工具都把发起方显式传下去,`findTargetAgent(ctx, target, { caller })` 统一入口。验证:夹具复现「不传 caller 就报没有匹配」,传 caller 后按名册命中 `frontend-rewrite`;统一名册同时含队友与普通 agent | | 1.3.9 | **新增「回收站」:删除降级为打标记,可撤销、可还原,到期才真删**。① **删除语义变更**:面板/工具的「删除」不再删文件,只在索引里写一条标记(`deletedAt` / `batchId` / 标题 / 目录快照),列表、索引直读、搜索一律隐藏被标记的会话 —— 还原因此是秒级且零风险(文件从没动过)。真正删盘只剩两个入口:**到期**(`trashRetentionDays`,默认 7 天,面板可改)与**一键清空**,由 `purgeExpiredTrash` 执行,另挂每小时一次的定时清理(unref,失败不影响宿主)。② **新标签页「回收站」**(插在「活跃」与「说明」之间):设置保留天数、撤销上次删除(按批次还原最近一次删除)、还原选中、全选、一键清空(二次确认);列表每行显示标题/目录/剩余天数,可单条还原或多选。③ **索引收敛单一位置**:`list-cache.json` / `value-index.json` / `trash.json` 三份散落文件合并为 `/storages/dsh-session-conductor/index.json`(一个版本号、一个原子写、`list`/`value`/`trash` 三个 section),首次加载自动迁移旧文件并改名为 `*.migrated` 保留可回滚。④ 新增 6 条 HTTP:`GET /trash`、`POST /trash/restore`、`/trash/restore-last-batch`、`/trash/empty`(需 `confirm=true`)、`/trash/retention`、`/trash/purge`。验证:回收站核心链路脚本实测(打标记→列表→按批次撤销→还原选中→保留期→到期识别全部通过);索引迁移脚本实测(三份旧文件合并、旧文件改名保留);面板「回收站」页渲染出 5 个操作与保留期输入;单测 26 个文件全绿 | | 1.3.8 | **筛选重做为「本地秒勾 + 低价值流式分析 + 价值索引」,并修掉一批「面板滞后 / 删除无效 / 搜索失效」的缺陷**。① **筛选(原「预览命中」)**:批量删除只留一条路径 —— 勾选 → 删除选中。归档 / 时间 / 目录三个条件在**本地列表**上过滤(行里本就带 `archived` / `cwd` / `updatedAt`),点完即勾、**0 次网络请求**;四条件为「与」关系,界面注明;零命中时清空旧勾选(防照旧结果误删)。② **低价值走流式分析**:新增 `value-analysis?stream=1`(NDJSON),每算完一条推一行,前端**分析出一个勾一个**,工具栏按事件累加进度。③ **价值索引** `/storages/dsh-session-conductor/value-index.json`:按 `updatedAt` + 有效期(默认 24 h,`valueIndexTtlHours` 可配)复用判定,命中零 I/O。真实日志实测单条成本 7.1 MB→3.3 s、29.5 MB→9.8 s、36.9 MB→12.3 s、86.4 MB→32 s,其中**价值分类仅占 0.1%**,成本几乎全在读日志;索引命中 0.02 ms/条。④ **判定只按事件驱动**:`__scFetch` 默认不再设超时(此前默认 30 秒两次把慢但正常的请求掐成「操作失败:signal is aborted…」)。⑤ **面板先读落盘索引再后台刷新**(索引只写不用等于白建:冷缓存要等 41~226 秒的全量构建才出内容),`readListIndex` 顺带带上 `archived`;流式列表**分批渲染**(150 ms 合并),避免逐条 setState 的 O(n²) 卡死;勾选收敛为公共函数 `setRowSelected` / `replaceSelected`,杜绝「计数变了、行内没勾上」。⑥ **删除修两处**:批量预检兼容宿主快照形状(`m?.id ?? m?.header?.id`,原先全被判 not-found ⇒ 全部跳过);磁盘删除改按 id 定位会话目录(原先挂在 0.2.x 已移除的 `listArtifacts` 上,是死代码 ⇒ 只 detach 不删文件、重启又回来)。⑦ **面板滞后一整类**:新增 `markSessionListStale(ctx, id)`(内存 TTL + 落盘索引一起作废),接到删除、手动/自动重命名、状态后缀刷新、归档、取消归档;删除还会清 value-index 条目,避免被索引「复活」。⑧ **搜索两处**:`searchSessions` 引用未定义变量 `q`(接口恒 400)改为 `queryText`;客户端兼容 `items[]` 与 `hits[]` 两种形状并滤掉空 id;搜索不再每次强制全量重建。⑨ `send_to_session` 的 dryRun 预检不再跑全量列表(`describeTarget` 改查内存/索引,41~226 秒 → 0.04~112 ms)。⑩ 新增 `assets/preview.spec.mjs` 使面板可被 `preview` 工具真实点击验证;mini-host 夹具修形状(`archivedSessionIds` 数组、`sessions.list()` 同步、时间基准取当天零点)。实测:minihost 各接口 200;面板点击验证「归档+时间命中 1 条、归档+7 天零命中、目录前缀精确命中」;单测 26 个文件全绿 | **筛选重做为「本地秒勾 + 低价值流式分析 + 价值索引」,并修掉一批「面板滞后 / 删除无效 / 搜索失效」的缺陷**。① **筛选(原「预览命中」)**:批量删除只留一条路径 —— 勾选 → 删除选中。归档 / 时间 / 目录三个条件在**本地列表**上过滤(行里本就带 `archived` / `cwd` / `updatedAt`),点完即勾、**0 次网络请求**;四条件为「与」关系,界面注明;零命中时清空旧勾选(防照旧结果误删)。② **低价值走流式分析**:新增 `value-analysis?stream=1`(NDJSON),每算完一条推一行,前端**分析出一个勾一个**,工具栏按事件累加进度。③ **价值索引** `/storages/dsh-session-conductor/value-index.json`:按 `updatedAt` + 有效期(默认 24 h,`valueIndexTtlHours` 可配)复用判定,命中零 I/O。真实日志实测单条成本 7.1 MB→3.3 s、29.5 MB→9.8 s、36.9 MB→12.3 s、86.4 MB→32 s,其中**价值分类仅占 0.1%**,成本几乎全在读日志;索引命中 0.02 ms/条。④ **判定只按事件驱动**:`__scFetch` 默认不再设超时(此前默认 30 秒两次把慢但正常的请求掐成「操作失败:signal is aborted…」)。⑤ **面板先读落盘索引再后台刷新**(索引只写不用等于白建:冷缓存要等 41~226 秒的全量构建才出内容),`readListIndex` 顺带带上 `archived`;流式列表**分批渲染**(150 ms 合并),避免逐条 setState 的 O(n²) 卡死;勾选收敛为公共函数 `setRowSelected` / `replaceSelected`,杜绝「计数变了、行内没勾上」。⑥ **删除修两处**:批量预检兼容宿主快照形状(`m?.id ?? m?.header?.id`,原先全被判 not-found ⇒ 全部跳过);磁盘删除改按 id 定位会话目录(原先挂在 0.2.x 已移除的 `listArtifacts` 上,是死代码 ⇒ 只 detach 不删文件、重启又回来)。⑦ **面板滞后一整类**:新增 `markSessionListStale(ctx, id)`(内存 TTL + 落盘索引一起作废),接到删除、手动/自动重命名、状态后缀刷新、归档、取消归档;删除还会清 value-index 条目,避免被索引「复活」。⑧ **搜索两处**:`searchSessions` 引用未定义变量 `q`(接口恒 400)改为 `queryText`;客户端兼容 `items[]` 与 `hits[]` 两种形状并滤掉空 id;搜索不再每次强制全量重建。⑨ `send_to_session` 的 dryRun 预检不再跑全量列表(`describeTarget` 改查内存/索引,41~226 秒 → 0.04~112 ms)。⑩ 新增 `assets/preview.spec.mjs` 使面板可被 `preview` 工具真实点击验证;mini-host 夹具修形状(`archivedSessionIds` 数组、`sessions.list()` 同步、时间基准取当天零点)。实测:minihost 各接口 200;面板点击验证「归档+时间命中 1 条、归档+7 天零命中、目录前缀精确命中」;单测 26 个文件全绿 | | 1.3.7 | **修「刚重启时面板报读取会话列表失败、过一会儿又自己好了」**。两个原因叠在一起:① 面板给列表请求套了 **30 秒 fetch 超时**,而冷缓存首次构建要逐份解压上百份日志(本机实测 41~226 秒)—— 慢但正常的请求被 30 秒准时掐断,报成网络失败;② 重启瞬间插件路由/宿主服务未必已就绪,首次请求直接网络失败,而面板**只有手动「重试」**,不点就一直停在错误态。改法:流式列表请求**不再设超时**(流式接口的正确判据是「有没有在吐数据」,不是「多久回完」;`__scFetch` 第三个参数传 0 即关闭超时,默认仍是 30 秒,其余接口行为不变);服务端写出 NDJSON 头行后**立即 flush**,浏览器不再干等;面板失败后按 3s / 8s / 20s / 45s **自动退避重试**,界面给出「N 秒后自动重试」倒计时,成功一次即清零,组件卸载时清掉定时器(不会在面板关闭后继续打后端)。新增 i18n `list.autoRetry`(中英各一条) | | 1.3.6 | **四个缺陷一起修:记忆取回轮数被吃掉、输出不给代价、面板点会话进不去、会话 id 复制麻烦**。① **`recall_memory` 要 30 轮只回 5 轮**:读侧没截断(`readRecentUserTurns` 的 `shift()` 只保留最后 N 轮,参数本身是生效的),真凶在**输出侧总长上限**——手动取回沿用自动注入的 `retrieveMemoryMaxChars`(默认 4000 字符),超限时从**最早一轮开始丢**,且**一个字都不提**,于是现象看起来像「轮数参数没生效」。现在手动通道按 `rounds × retrieveMemoryMaxCharsPerItem`(默认 800)算总长预算,并在真的被截断时明说「受总长上限 N 字符限制,最早的 K 轮未包含;需要更早的内容请调小 rounds 分次取」。② **输出末尾附 `(本次输出正文 N 字符)`**:取回代价一眼可见,也顺带成为「有没有被截断」的自证。③ **面板点会话进不去**:原实现走 `ctx.get("sessions").open(...)`,而宿主客户端**没有 `sessions` 服务**,可选链把「服务不存在」吞成静默无反应——不报错所以一直没被发现。现在按 `workspaceNavigation.openSession` → `uiWorkspace.openSession` → 旧版 `sessions.open` **三级回退**(旧宿主照样可用,新宿主走官方入口),三级都不存在才 `console.warn` 留痕,不再静默。④ 面板里的会话 id 改为**单击复制**(优先 Clipboard API,非安全上下文退回 `execCommand`),复制成功该行短暂显示 `✅ 已复制` 再复原,不依赖宿主 toast(旧版未必有)。新增两条回归测试(30 轮必须全取回、被截断必须给出提示),全量单测 26 个文件全绿 | | 1.3.5 | **修「跨会话投递裸发、接收方不知道这条是谁发的」**。原先 `send_to_session` 只把正文交给本体 `prompt`,接收方看到的是一段裸文字 —— 实测出现过「A 会话投递的消息,B 会话以为是当前这一方发的」。现在 `deliverPrompt` 在正文头部**自动**加一行 `【来自会话 <发送方 sessionId>】`,id 由插件从 `currentSessionId(exec.agent)` 取 —— **不需要 AI 自己写,也就不会漏**;不带 `from` 时保持原样(向后兼容)。两处调用点都已补 `from`:`send_to_session` 的正常投递、`create_session` 的初始消息。单测相应改为断言该前缀契约(`/^【来自会话 [\w-]+】\n<原文>$/`),避免以后被人悄悄去掉。 | | 1.3.4 | **新增用户聊天框指令 `/model-switch`——用户自己就能看/切成员(子代理)模型,不必绕道求 AI**。注册走宿主 `ctx.commands`(照官方范例 `@deepseek-ai/dsh-command-goal` 的用法,不是猜):`register({definitionId, name, description, input, handler})`,`definitionId` 须用 `@deepseek-ai/dsh-commands/brand` 的 `CommandDefinitionId()` 包一层;handler 从 `invocation.rawInput` 取参、`invocation.agent` 取发起方,返回 `{kind, text}` 交聊天框渲染。**三条形态**:**无参**列成员与各自模型(便宜来源按序取:本插件 override → **进程内最近一次请求** → 名册记录值,不逐会话扫日志,故瞬时);**一参**查该成员**实际**在用哪个模型(优先读日志 `request/header`,即请求级权威记录;日志尚无记录时回退显示进程内最近请求,不武断说「该成员从未发起过请求」);**两参**切换并回显查证快照(与 AI 工具 `set_member_model` 共用同一条切换 + 查证路径)。**守卫**:`ctx.commands` 与 brand 包一律**动态导入 + 判空**,拿不到就跳过注册并如实留痕 —— 静态引用宿主包会在导入期炸掉整个插件(本项目在 defineTool / createUserMessage 上踩过同类坑)。⚠️ 指令**在插件加载时注册**,更新后需重载实例才出现在输入框。另:新增 `test/unit/test-model-switch-command.mjs`(6 条:注册守卫两条、空名册也给人话、找不到成员报错带原因、模型写法非法给正确格式、多参只取前两段);README「使用」与使用手册新增用法节(含真实输出与三个易误解点:⏳ 不是失败 / 别拿意图当结论 / 模型要写 `provider/model`)。**真机验收**(成员未挂载场景实测):`/model-switch m0-contract … wb/cn:deepseek-v4.1-flash` 回执「已写入会话日志的 model/selection 事件(成员未挂载,已记录切换意图),该成员下次运行时生效」,并同时给出最近一次实际请求 `n/deepseek-v4.1-flash`、判定「⏳ 待下一回合生效(最近一次请求是切换前发出的,属正常)」与最近切换意图 | | 1.3.3 | **修 `/list` 路由两处缺陷(一处让人静默吃慢、一处把不阻塞入口堵死)**。① **`?fromIndex=true` 被静默忽略**:路由只判定 `job=1` 与 `stream=1`,**没有 `fromIndex` 分支** ⇒ 请求落进默认分支做全量构建(本机 201 份日志 / 1.5 GB,实测 41 秒才回,还顺手写了索引),而它本该是「读落盘索引、秒回精简列表」的快路径(与工具 `list_sessions` 的 `fromIndex` 同义)—— 不报错的降级比报错更难查。② **`?job=1` 在 HTTP 入口必然 501**:宿主 job 控制器要求 owner 是「有活 agent 的会话 id」,而 HTTP 请求**没有执行主体** ⇒ 无主注册被拒(`no job controller serves this agent`),原实现只如实 501、**没有任何回退**,于是「不阻塞地拿列表」的唯一入口是死的。两条复合 ⇒ 想快速拿列表的调用方无路可走。**共同根因**:同一能力在工具与路由上各写一套判定,加 job 分支时只补了 `job` 没补 `fromIndex`。修法:路由补 `fromIndex` 分支(复用工具那条已验的 `readListIndex`,为此把它从内部函数改为导出、经域出口再导出);job 注册失败时回退**落盘索引快照**(`200 + async:false + fromIndex:true + jobUnavailable:<原因>`,不假装成功),索引也不可用才如实 501。**实测**(重启加载新 handler 后):`fromIndex` **200 / 21 毫秒 / 201 条**、`job=1` **200 / 19 毫秒**且响应里带出失败原因。另:路由是**加载期注册**的,此类改动必须重载插件才生效(普通模块改动走 HMR 即可)。附带把版本表里两行 `1.3.1` 标清为「同版 · 接上条」(两条本是同一次发布),并清掉真实例状态文件里一条测试残留 `s1` | | 1.3.2 | **插件持久化域「重复打开」自愈——模块热重载后面板与接口不再一路 500**。宿主的 `storageDomain` 按域名字占用(facility 的 `reserved` 集合),重复 `open` 直接抛 `domain 'dsh_session_conductor' is already open`;插件模块热重载时新实例的缓存是空的、而宿主的域仍然活着 ⇒ 再 open 必然失败,实测表现是 `/list`、`/templates` 以及所有要读开关的接口一起报错(面板会话列表刷不出来),直到整个实例重启为止。现在捕获 `already-open` 之后改用宿主的诊断口子 `storage.get(name)` 复用那份 handle;宿主不提供该口子(老版本)时仍如实抛错,且失败的 `open` 不留缓存(下一次请求重试)。新增 `test/unit/test-domain-reuse.mjs`(4 条:复用已开域 / 无口子时如实抛 / 非目标错误不吞 / 服务缺失时给可读原因),全量单测 **25 个文件全绿** | | 1.3.1 | **`set_member_model` 返回值带上「切没切成」的查证**(原先只回「意图 + 生效方式」,看不出实际请求有没有换成新模型)。① 返回值**立刻**给快照:**最近一次实际请求**的 provider/model(取自 `request/header`,请求级权威记录)+ 最近一次切换意图(`model/selection`)+ 一行判定(✅ 已是新模型 / ⏳ 待下一回合生效);② **注册 job、不阻塞**:调用当下即返回,判定由 `agent/request` 事件触发(不轮询、不等待),确认结果在下一次 step 注入给发起方;③ 附**可复制的复核命令**(新增 `scripts/check-member-model.mjs `,随包发布);④ 顺带修 `setMemberModelOverride` 里一处**未定义引用**(懒加载改造漏改的调用点 —— 会让切换直接失败)。新增 `test/unit/test-member-model-verify.mjs`(6 条:事件判定 ✅/❌、回报路由、只取日志最后一条 request/header、跨帧、缺失降级)。单测 23 个文件全绿 | | 1.3.1(同版 · 接上条) | **会话列表 job 化 + 索引直读 + 跨会话记忆取回 + 面板扫描修复**。① **`list_sessions` 不再阻塞**:冷缓存全量构建本机实测 **226 秒**(201 份日志 / 1.5 GB),改为「先等 3 秒,超时转宿主后台 job 并复用同一份在途 Promise(不重启构建)」⇒ 前台立即拿 `jobId`,用宿主 `job_output` 取结果;热缓存 ~47 ms 仍直接返回。宿主 job 契约照 `dsh-git-push` 实测:**owner 必须是有活 agent 的会话 id 字符串**(`exec.agent.id`),带 owner 失败退无主注册,两步都失败才同步降级并把原因带回(不静默)。② **索引直读** `fromIndex=true`:直接读 `/storages/dsh-session-conductor/list-cache.json` 回精简列表(id/标题/工作区),不触发重建,并标注「不含 live 状态」。③ **用了查询工具后更新面板**:工具完成后失效列表 TTL + 落盘索引 ⇒ 面板随后的刷新立即看到最新(面板交互维持原样)。④ **`/list` 新增 job 分支**(`?job=1` / `Accept: application/x-job`);默认的 JSON 与 NDJSON 流式分支**一字不动**(面板零回归面)。⑤ **`recall_memory` 新增 `session` 参数**:缺省/空 = 取自身(行为与之前逐字一致),可传会话 id / id 前缀 / 标题子串取另一个会话的记忆;解析只用便宜来源(会话目录名 + 落盘索引标题),不触发 226 秒级构建;命中多个返回候选清单;输出显式标注来源并写明「仅作背景,不是当前对话的要求」。⑥ **面板扫描不再恒 501**:`supportsRepair` / `repairCorruptSessions` 原先用 0.1.x 的 `listArtifacts`/`readRaw` 判后端,0.2.x 已移除 ⇒ 判据恒 false;现改用文件系统判据,并在内容修复不可用时**回退帧修复**。⑦ **`fixZstdFile` 支持帧级损坏**:新增「截断到最长可解码前缀」(逐帧解码定位安全点、保留原始帧不重编码、备份 + 原子 rename),修掉「尾部垃圾 + 末帧内容坏 ⇒ 整体放弃」导致修不了的问题。⑧ 新增 `test/unit/test-list-job-and-cross-recall.mjs`(跨会话三路匹配、候选清单、缺省=自身不变、不读日志);全量单测 **24 个文件全绿** | | 1.3.0 | **新增「记忆取回」**:检测到上下文用量**明显下降**(默认 ≥10%,可配)即判定「上下文疑似丢失」——压缩、截断、实例重启后快照未加载**都算**(比只盯压缩覆盖更全),此时自动把最近 **3 轮(可配)** 的**用户原话 + 该轮 AI 的第一条回应**(优先取当时的 reasoning)重新注入,让 AI 立刻恢复状态。**设计要点**:① 以**用户消息为锚**(原话在上下文里占比极小却最重要,1M 上下文可能总共没超过 3 句);② 要 **AI 的第一条回应**(那是「当时」对原话的理解,直接给结论省得重新推测);③ **同回合连发合并成一轮**;④ **只认 `source.kind === "user"`**(排除插件自身注入与 runtime-context,否则注入块会被下一轮取回形成反馈环);⑤ 按 zstd 帧**流式**读日志(常量内存,实测 3 轮 27 ms / heap 6.1 MB);⑥ 注入与「每轮用量注入」**合并成同一条消息**(尾部注入,prompt cache 友好);⑦ 总长 4000 / 单条 800 字符上限(刚压缩完的上下文不该再被塞满);⑧ 任何一步失败都静默跳过,绝不阻断回合。配置项 `retrieveMemory` / `retrieveMemoryRounds` / `retrieveMemoryMinDropRatio` / `retrieveMemoryMinDropTokens` / `retrieveMemoryMaxChars` / `retrieveMemoryMaxCharsPerItem`(默认开)。**另提供 AI 工具 `recall_memory`**:让 AI 自己按需回溯(自动注入之外的手动通道),参数 `rounds` = 取几轮(默认取配置 `retrieveMemoryRounds`,上限 50),只读返回文本、不改上下文。新增 `test/unit/test-memory-recall.mjs`(16 条:阈值边界/合并/上限/反馈环排除/降级/工具注册与参数夹取)。方案文档 `docs/2026-10-15-记忆取回.MD` | | 1.2.2 | **冷会话读日志改「按 zstd 帧流式读」——把单会话内存从数百 MB 压到常量级**。① 新增 `streamDecodedFrames(buf, onChunk)`(`lib/zstd-frames.js`):逐帧解压、**把解压分片直接交给消费者,不拼接不驻留** —— 原 `decodeAllFrames` 会把所有帧拼成一个大字符串,大日志下峰值就是整份文本。仍坚持逐帧喂解压器(整体流式会丢后续帧)。② 新增 `readColdSessionSummary()`(`lib/sessions/events.js`):直接定位并读取日志文件(`findSessionLogById`,复用 `session-log.js` 的命名兼容),流式解压 + `StringDecoder` 增量切行 + 逐行 `JSON.parse`,只累加列表真正需要的三个值(最后一条 `session/title`、最后一条事件的 `time`、最后一个 `turn/end` 的 reason)并保留有界尾部(默认 2000 条,供官方 `interruptedTurnClosers` 判定开放回合);找不到文件时**回退**到原 handle 读法。③ `list.js` 冷会话解析改走摘要读。**实测**:本机最大日志(2 帧 / 23585 事件)**heap 增量 24.1 MB、峰值 29.2 MB**(原路径要把 173 MB 解压文本变成事件对象数组);与「全量解码 + `lastEventTime`/`hasOpenTurn`/`interruptionInfo`」朴素计算**逐项对拍一致**(含合成的中断闭合器与事件总数)。④ 新增 `test/unit/test-cold-session-read.mjs`(10 条:用 `zstdCompress` 造**真实多帧**夹具端到端验证,不 mock 解压;含「list.js 必须优先摘要读且保留回退」护栏)。⑤ `test/api/test-readonly-api.mjs` 的 `/list` 用例单独放宽预算并写明「耗时取决于实例规模与负载」——该端点是重活,不应把「实例正忙」判成「接口坏了」。单测 21 个文件全绿 | | 1.2.1 | **同一类 API 假设的第二处修复 + 明确支持的 DSH 版本**。① `lib/group.js` 的「最近活跃会话的 cwd」也在探测 `listSnapshots()`,失败支里**直接把 `SessionPersistenceSnapshot` 当裸 header 读**(`snapshot.cwd` / `snapshot.updatedAt` 全是 undefined)⇒ 该功能在冷会话上**静默失效**(只剩 live 会话参与比较);现统一解包 `entry.header ?? entry`。② README 新增「环境要求(支持的 DSH 版本)」:**推荐 0.2.1+**,并如实说明「更早的 0.2.x 功能可用但列表缓存不生效(`list()` 不给 `revision` ⇒ 每次都要重新解析,慢且吃内存)」——插件的解析是容错的(`entry.header ?? entry`),所以旧版本是**降级**而不是不可用。③ 顺带修正 `session-ops.js` 里过时注释(原写「用 listSnapshots()」,该方法在任何 DSH 版本里都不存在) | | 1.2.0 | **修掉会话列表的「缓存永不生效」+ 内存/超时压力**(线上实测:进程 RSS 2.33 GB、峰值 2.75 GB(堆上限 3.19 GB),`GET /list` >120s 无响应)。**根因**:宿主 `sessionPersistence.list()` 现在返回的是 `SessionPersistenceSnapshot`(`{ header, revision, eventCount?, sizeBytes? }`),而插件先探测早已不存在的 `listSnapshots()`、探测失败后退回「把 list() 的返回值当裸 header 用」并把 `revision` 置 null —— **revision 为 null 的条目永不进磁盘缓存**,于是每次列表构建都要把 164 个会话 / 1.5 GB 日志全量重解析(实测单份日志 49.2 MB 压缩 → **173.3 MB 解压**,膨胀 3.2~5.0×)。**修法**:统一按快照解析(`entry.header ?? entry` 兼容旧裸 header)并带上 `revision`,缓存恢复正常 ⇒ 同一会话只在日志变更时解析一次。**同时**:`list_sessions` 输出收敛(`query` 子串过滤 + `limit`/`offset` 分页,默认只给 20 条,避免整表塞进调用方上下文)。**另记录一次被推翻的优化**:曾把冷会话读改成「分块 `read(offset, chunk)`」以省内存,实测发现宿主 `readCurrent` 每次都会 `readStoredLog(path)` 全量解压、`offset/length` 只是内存数组切片 ⇒ 分块读**内存峰值不变、耗时变 O(n²)**(api 门禁里 `/list` 直接超时),已撤回并在 `events.js` 写明该限制与「真正可行的路子是绕过 handle 按 zstd 帧解码」。顺带:`events.js` 的宿主包改懒加载(该文件因此可被单测覆盖,新增 7 条冷会话读回归 + 2 条「禁止改回分块读」护栏);单测 21 个文件全绿 | | 1.1.1 | 三个跨会话工具的**可用性补强**(实测反馈后改):① `create_session` **缺省工作目录改为「调用方会话自己的 cwd」** —— 原样交给本体时,本体默认是它自己的运行目录(实测是 DSH 安装目录),新会话落在程序目录里毫无意义;② 两个写操作工具都加 **`dryRun` 预览**:`create_session` 的 dryRun 会**核对 provider/model 是否真在模型目录里**,`send_to_session` 的 dryRun 会**报告目标状态与限流余量且不消耗配额**,两者都执行全部本地校验但不产生任何副作用;③ 三个工具补齐**使用说明**(工具 description + skill 第十一章的标准用法三步);④ `list_sessions` 过滤缺 id 的脏条目(此前会渲染出一行 `undefined`)并如实标注跳过条数;⑤ 按「单文件 ≤400 行」把模块拆为 `logic.js`(常量/纯函数/本体调用封装)+ `tools.js`(工具注册)+ `index.js`(域出口)。新增单测 11 条(合计 34 条)。**另(同一版本内后续补入,不单独升版)**:新增**上下文用量每轮注入** —— 每轮把实时用量作为一条 user 消息追加到该步消息批次**尾部**(走 `agent/pre-step` + 官方 `createUserMessage` logged 通道),AI 每轮都能直接看到数字且注入内容落进会话日志;放尾部而非系统提示词前缀是关键(前缀保持稳定 ⇒ prompt cache 仍命中);按 **turn** 门控避免一回合刷出多条;文本用单行紧凑格式 `formatContextUsageCompact`;读不到投影时静默跳过;配置项 `injectContextUsage`(默认 true)可关 | | 1.1.0 | 新增**跨会话消息**与**指定模型新建会话**三个 AI 工具(AI 可直接 toolcall):`list_sessions`(列出会话:id/标题/工作区/状态/最后活动/是否当前会话)、`send_to_session`(给指定会话投递消息,目标空闲时由本体自动唤醒)、`create_session`(新建会话,可指定 provider/model,可选带初始消息)。实现要点:**全部走本体官方 API**(`sessionController` 的 `@Remote('create')` / `@Remote('selectModel')` / `@Remote('prompt')`)——`prompt` 受理前会先 resume 目标会话,唤醒与幂等(requestId)都由本体保证;**不自己写会话日志**(inbox 家族只有 `agent/inbox/spliced` 落盘,其 fold 对非法 splice 直接抛错,自己写等于拿对方会话完整性冒险)。安全硬约束:禁自投、协作链跳数 ≥3 拒绝、同一对会话 60s 内最多 5 条、requestId 幂等与在途去重;`create_session` 对「会话已建但模型未设/初始消息未投」如实回执不谎报成功。新增单测 23 条 | | 1.0.11 | 修**方案门禁锚点写死章节号**导致的「开启强制门禁后所有编辑被永久拒绝」:门禁锚点原为字面量 `八、是否执行`,而模板把「是否执行」从第八节移到第十一节后两者脱节——**AI 照模板写方案也过不了锚点**,deny 文案又不提示锚点不匹配。改为**语义锚点**:标题锚点(`修改方案提案` / `门禁表`)+ 确认段锚点(`是否执行` / `请回复确认` / `确认或修改方案` / `等待…确认` 正则),任一命中即成立,**均不含章节号**;同步修掉 deny 文案里的同一处写死与 skill 文档描述。新增 4 条回归护栏(核心一条:章节号变了仍认提案),门禁单测 38/38、全量 unit 18/18 通过 | | 1.0.10 | 修「按 v3 开发、未适配 DSH 0.2.1 的 v4 会话格式」导致的校验误判:v4 把 `tool/result` 的 `message.role` 从 `user` 改为 `tool`(first-class tool-role——`toolCallId` 提升到 message 层、content 不再有 `tool-result` wrapper)。原校验写死 `role === "user"`,使**全部 v4 会话**被判「role 应为 user」;凡「写回前先校验」的路径必然失败(如 `set_member_model` 对未挂载成员写 `model/selection` 事件时报「写入后校验失败」)。改为按 role 分派,两种形态各按官方口径校验(v4 对齐 `assertV4ToolResultMessage`)。真实数据验证:v4 30/30 通过(修复前全失败)、v3 10/10 通过(回归);新增单测 11 条 | | 1.0.9 | 修 dsh-session-conductor 四处「引用未定义标识符 / 状态未水合」导致的**静默失效**(同一家族,全部被 try/catch 吞成一行日志):① 收尾汇报约定的系统提示词段 `task-completion-report`(order 1000)因 `text: TASK_COMPLETION_CONVENTION` 引用的常量**自 1.0.0 起从未定义**,构造参数时抛 `ReferenceError` ⇒ 该段从未注册;改为同步读取插件自带权威文本 `skills/task-completion-report.md`(去 frontmatter)。② 方案/收尾模板的上下文注入(`agent/pre-step`)因 `collectTemplateSlotText` 未导入而每次抛错 ⇒ 从未注入过。③ 方案模板强制门禁因 `planGateAllows` / `PLAN_ENFORCE_EDIT_TOOLS` / `PLAN_ENFORCE_BASH_WRITE_RE` / `planEnforceDenyMessage` 未导入而恒走 fail-open 放行 ⇒ 设置里开着也从未拦过一次;②③六处缺失一次性补齐:`collectTemplateSlotText` / `planGateAllows` / `PLAN_ENFORCE_EDIT_TOOLS` / `PLAN_ENFORCE_BASH_WRITE_RE` / `planEnforceDenyMessage` 补导入;非 dryRun 的双格式修复原为未导入的裸调用 `repairDualFormatSessions(...)`(恒抛 ReferenceError 被 catch 成 400「repair-dual-format-failed」⇒ 该修复从未执行过),改为与 scan 同源的懒加载命名空间调用。④ `templateStateCache` 初值为 `TEMPLATE_DEFAULTS`(全关)且只被 `/templates` 路由覆写 ⇒ 宿主重启后未打开过设置页时模板不注入、门禁不生效;新增启动水合 `hydrateTemplateState()`(两个消费点前读取存储状态)。⑤ `createUserMessage`(宿主包 `@deepseek-ai/dsh-llm`)未导入 ⇒ 改为带守卫的动态导入,缺包时跳过并留痕。验证:运行时探针(假 ctx 驱动安装副本的真 `apply()`)实测系统提示词段注册 1 段/文本 1023 字、`pre-step` 注入 1 条含「门禁表」方案模板、`enforce` 下 `edit` 调用返回 deny;单测 17/17 通过,新增回归护栏(约定文本可读、接线绑定已解析文本、lib 无未定义标识符引用、启动水合调用点 ≥2、跨模块导出-引用扫描为 0) | | 1.0.8 | ① 修「全新浏览器上下文(空 localStorage)下侧边栏『会话管理』入口整块消失」:`readListCache()` 在无缓存 / 缓存损坏 / `sessions` 非数组时由返回 `null` 改为返回**空数组**,`insertSorted` 再对 `cur` 做数组兜底——原先 `sessions` 初值为 `null`,流式 `/list` 追加时 `cur.filter` 抛 `TypeError: Cannot read properties of null (reading 'filter')`,React 更新器抛错导致 `sidebar.footer.action` 插槽崩溃(入口消失 + 每次加载两条控制台红字)。补回归单测 `test/unit/test-list-cache-null-guard.mjs`(假 localStorage 执行分片:无缓存 / 坏 JSON / 非数组 / 正常往返 + `insertSorted` 结构护栏)
② 测试 helper 不再写死实例路径:`DSH_HOME` / `HOST_HOME` 解析改为 `SC_DSH_HOME` → 宿主注入的 `DSH_HOME` → `DSH_APP_ROOT` 探测(原兜底写死 `…/0.2.0-rc.2/.dsh`,含实例版本号,实例升级到 0.2.1-alpha.2 后失效) | | 1.0.7 | 新增 `context_usage` 上下文用量自查工具——AI 可按需查当前会话的占用(百分比、已用/容量 tokens)与构成(系统提示词/工具定义/对话消息),数据取自官方 `contextPressure`/`contextBreakdown` 投影(与界面圆环同源);占用达 85% 时输出附收敛建议。设计取舍:用「自查工具」而非每轮注入数值,避免系统提示词前缀每轮变化破坏 prompt cache | | 1.0.6 | dsh-session-conductor 1.0.6 —— 结构/审计低风险优化 + 移除冗余工具 + 分片可独立校验 | | 1.0.5 | 测试体系改造为「打 HTTP API」+ 修「分组下新建会话」恒 500 | | 1.0.4 | 拆分 lib/index.js(4348 → 2257 行)为按功能域分层的多目录结构 | | 1.0.3 | 成员模型切换(list_models / set_member_model)+ 会话日志 v4 文件名兼容 + zstd 改 node:zlib + 版本号统一 | | 1.0.1 | 撤回消息补测与全量审计优化——①test-undo.mjs 单测(冷会话截断最后一条用户消息及以后/活跃会话 detach 链路/dryRun/运行中拒绝/注入不误撤)②全量审计优化:tools/vendor 第三方豁免、全分片 fetch 30s 超时兜底(__scFetch)、有语义魔数提常量、test 目录 .test 豁免、空 catch 注释、docs 三兄弟独立 md(FILE-TREE/VERSIONS/FUNCTIONS)③评分 76.8→78.9、blocker 0 | | 1.0.0 | dsh-session-conductor 初始版本——会话管理增强插件(面板四视图/归档恢复/批量与条件删除/AI 自动重命名/中断自动续跑/按工作区分组/撤回消息/三类损坏修复/价值分析/全文搜索/压缩模型/模板注入/自动重命名模型),中英双语 i18n 外置,真实后端预览页(快照免重启+同源路由),README 文件树/版本/函数列表由 git-push doc-* 工具维护,全部历史整理为一次提交 |