Repository navigation
Releases: xDJTomato/deepseek-harnessed
Release list
v0.1.11 —— 面板不再拖着宿主 GPU 一直烧;顺带发三条积压的委派规则
0.1.11 —— 面板不再拖着宿主 GPU 一直烧,顺带发三条积压的委派规则
- 修复:子代理面板与设置窗同时打开时界面卡死
现象是你报的:两个窗口一起开,界面非常卡。根因是两件事相乘 ——
- 卡片上那层缓慢扫过的光泽是常驻无限动画。这张卡片带 mask-image 的伪元素 +
overflow: hidden,走不了纯合成路径,宿主只能按帧率重绘整张卡片; - 设置窗那层是全视口 backdrop-filter —— 背后内容一变就必须重新模糊。
实测(DSH Desktop 2.0.11):修前空闲态(没有任何任务在跑)宿主 GPU 进程稳定吃 97%~118%
单核,跨我自己空闲的 4 分钟一直在 90% 以上 —— 所以不是"有任务在跑才卡",这一点专门用独立
进程每 5 秒采样排除了。同期 renderer 只有 12%、Edge 全部进程 0%,说明瓶颈在合成/光栅,不在 JS。
三处一起改:
- 扫描光只在真有任务在跑时才动(data-busy="true"),空闲时动画整个关掉;
- 运行中磁贴的流光从动 left 改成 transform: translateX(left 是布局属性,每帧触发整份文档
重排;行程按元素自身宽度换算,视觉不变); - 面板心跳从恒定 1 秒改成「有任务在跑 1 秒 / 空闲 30 秒」,省掉空闲时每秒一次的整卡重渲染。
代价是两处故意的可见变化,别当缺陷记:空闲时卡片不再有那层光泽;空闲时「已耗时」这类数字
每 30 秒才走一格(有任务在跑时仍是每秒)。
- 新增:禁止委派清单(四类活不派给子代理)
dsh_task 描述、initialize.instructions 与委派前言都写清了:短指令作业(预计 < 30s 且不需要独立
上下文的)、常驻/长命进程(dev server、watch、daemon、交互式终端)、不能轻易结束的进程(占端口、
开事务、拿锁、必须成对收尾的独占资源)、需要人交互的活。机制原因写在描述里:子任务结束或被
cancel/kill/deadline 收尾时都会连进程树一起收尾 —— 由子代理起的服务会跟着消失,而"某个进程
还活着"也写不成可机检的 acceptance。一次性短命服务(起测试库、用完就停)仍可派,但必须成对收尾。
- 新增:先拆再派,能并行就并行
一次只派一个可独立验收的产物、目标 expected_seconds ≤ 300s;要改 3 个以上文件、估时超 300s、
或属于"先调研再实现再验证"的多阶段作业必须先拆;互不依赖的子任务同一轮里并发派出,只有真存在
依赖才串行。委派前言同时告诉子代理:这次任务是从更大目标里拆出来的一小片,只做这一片。
- 修复:桥接进程留生命周期痕迹,未处理异常不再带走整条通道
启动(带版本号)、退出原因、未处理异常/未处理拒绝的堆栈,各写一行到 state/mcp-lifecycle.log。
起因是实际踩到的故障:Codex 里 dsh_task 返回 "Caused by: Transport closed" —— "客户端关了管道"
和"桥接进程崩了"在调用方看来完全一样,当时只能靠数 node.exe 个数才敢确认桥接进程已经不在了。
另外 Node 24 默认把未处理的 Promise 拒绝当异常抛出,过去一个没包住的 await 就能让整个 stdio
服务退出,之后调用方每次调用都是 Transport closed;现在记一行留痕,继续服务下一个请求。
校验:面板自检 143 项全绿。这个缺陷肉眼看不出来,所以补了 5 条性能防回归断言挡着 ——
base 的 .sap-card::after 里不许出现 animation、扫描光只准挂在 data-busy="true" 规则上、
@Keyframes sap-sweep 只准动 transform 不许出现 left:、渲染出的 .sap-root 有任务在跑时
data-busy 必须为 "true"、空闲时必须为 "false"、心跳必须是「有任务 1000 / 空闲 30000」。
npm run test:all 397 项中 396 项通过;唯一未通过的是 wait-policy-probe 的「dsh_task_status 默认
自己等待,并等到终态」—— 那条探针的等待预算正好是它的默认值 30 秒,而本机当前默认模型
(经网关接入、reasoningEffort: high)会把叶子任务拖过 30 秒,连跑两次同样命中,与本次改动无关。
selftest 102/102 复跑通过,含版本号一致性(生命周期日志里能看到 start bridge v0.1.11)。
升级方式:重新跑一次安装命令(node install.mjs)即可,已装的文件会就地覆盖。宿主里的观察器是
file: 模块、不会热重载 —— 涉及它的改动要重启 DSH Desktop 才生效,刷新页面不够;卡片(浏览器侧)
硬刷页面即可取到新 bundle。你自己那份 config/model-policy.md 若已改过,不会被覆盖。
v0.1.10 —— 预览窗口只读显示子代理的对话内容
0.1.10 —— 预览窗口里只读显示子代理的对话内容
- 新增:只读会话转录
运行中的外部任务不能点开(点开 = 宿主接管会话写权、写坏子进程正在写的日志),原先那个只读
详情页只能看到 stderr.log 的最后几行,看不到它到底在说什么。现在观察器在算 Token 用量的
同一趟解码里把会话日志折成转录,随投影下发给卡片,详情页按四类角色只读渲染:
- 用户 / 助手:取消息体里的文本块;推理块(reasoning)不显示 —— 长且刷屏;
- 调用:折成「工具名(命令)」,参数是 JSON 时取 command / description / path,不是一坨转义 JSON;
- 返回:折成单行;空返回标成 (空返回)(沙箱把命令吞掉时的现场,最该被看见的一种返回),
isError 的加 [错误] 前缀。
投影每 2~20 秒重发一份快照,因此转录必须封顶:默认最多 60 条、总计 6000 字、单条 600 字,
超出从最旧的开始丢(可用 DSH_SUBAGENT_OBSERVER_TRANSCRIPT_ENTRIES /
DSH_SUBAGENT_OBSERVER_TRANSCRIPT_CHARS / DSH_SUBAGENT_OBSERVER_TRANSCRIPT_ENTRY_CHARS 调整)。
老投影没有这个字段时,详情页退回原来 stderr.log 的尾部,不会突然空掉。
客户端只认 role 与 text 两个字段(转录是外部数据),转录节点上没有任何点击或写入钩子:
只读是结构性的,不是靠约定。
- 修复:自检不再拿用户自己的策略文件当"未指定"用
test/selftest.mjs 原先把仓库里那份 config/model-policy.md 原样复制成临时副本,再断言
"默认模型未指定 → 会输出首次接入引导" —— 可策略文件本来就是给用户改的:用户一设默认模型,
这 4 条断言就红。现在副本里显式把「默认模型」那行改成 (未指定)(那才是要测的状态),
仓库那份只做结构性断言:(未指定) 或一个裸模型 id 都算合法。
- 升级后怎么确认生效
宿主里的观察器是 file: 模块,不会热重载:重启 DSH Desktop 才生效,刷新页面不够。
state/observer-heartbeat.json 里出现 transcriptCaps,就说明跑着的是带转录的这一版;
没有这一项 = 预览窗里不会有转录。卡片(浏览器侧)硬刷页面即可取到新的 bundle。
校验:自检 390 项,9 个套件全绿(selftest 100、panel 138、observer 49、launcher 13、ledger 20、
autostart 26、leaf 14、exec-surface 11、monitor-live 8);wait-policy 11 项中的
"dsh_task_status 等到终态"这一条在本机被同时在跑的外部任务拖慢时不稳定(这条断言只给
20 秒强制 sleep 留了约 10 秒给两次模型调用,网关一忙就超时),单独复跑通过。
文档闸门 exit=0(行内记号/代码行/版本号/跨文档链接 0 丢失、黑话 0 命中、敏感词 0 命中)。
现场验证:411 KB 真实会话日志解码 14ms、折出 22 条转录、封顶生效。
升级方式:重新跑一次安装命令即可。已装的代码与配置都在 ~/.dsh 下,新版本会就地覆盖桥接层
文件;你自己那份 config/model-policy.md 若已改过,不会被覆盖。
0.1.9 — 文档重写为朴实技术语言 + 公开仓库脱敏
0.1.9 —— 文档改写为朴实技术语言 + 公开仓库脱敏
这个版本没有新功能,改的是文档本身,以及文档里本不该公开的信息。
-
四份标准文档重写
README.md、docs/clients.md、docs/configuration.md、docs/install.md 用 gemini-3.7-flash 重写为
朴实技术语言:把"磁贴 / 硬承诺 / 响亮失败 / 口径 / 幽灵 / 垫片 / 探针 / 闸门"这类生造说法
换成常见词(卡片 / 明确的执行上限 / 直接报错不静默 / 规则 / 残留记录 / 入口脚本 / 自检脚本 / 检查)。
技术信息一条没少:行内记号 704 → 714,代码行 139、版本号 9、跨文档链接 3 全部保留。
重写过程中一度掉到只剩 454 个记号,由机检闸门逐项补回 —— 该闸门同时卡
「行内记号 / 代码块行 / 版本号 / 跨文档链接一个都不许丢」与「黑话 0 命中」,不靠人眼看。 -
公开仓库脱敏
本仓库是公开的,而文档里带了内部信息。本次替换成中性占位符:provider 名、API key 环境变量名、
内部网关地址、只在本机存在的模型别名;项目名从策略文件里去掉;
工具描述里"本机当前是某个 provider"这类写死本机事实的句子删掉(模型清单本来就该从
dsh_health.models 查)。模型 id 示例仍保留,示例照抄仍可用。 -
测试可移植性(顺带修复)
test/selftest.mjs 原先断言"本机 43 个模型""本机默认模型是 provider/某模型",换一台机器
clone 下来必红。现在断言结构(providers 是对象、每项都是非空 id 数组、顺序与去重与独立解析
一致、defaultModel 的 provider 前缀必须是真实 provider),并加一条跨文件一致性断言:
枚举结果必须覆盖策略文件里"预设默认模型"点名的模型。
升级方式:重新跑一次安装命令即可。已装的代码与配置都在 ~/.dsh 下,新版本会就地覆盖桥接层文件;
你自己那份 config/model-policy.md 若已改过,不会被覆盖。
0.1.8 — 模型选择:首次接入引导 + 用户可编辑策略文件
0.1.8 —— 模型选择改成"首次接入 + 可编辑策略文件"
这个版本解决一个真实问题:以前桥接层把"可用模型"写死在工具描述里,换机器、换 DSH 实例、换了网关,描述立刻变成错的,而调用方会照着错描述去传参数。
改动:
-
首次接入引导
策略文件里还没指定默认模型时,dsh_task 与 dsh_health 的返回值末尾会带一段紧凑引导:
先枚举这个 DSH 实例已接入的模型 -> 请用户指定一个默认模型 -> 用 dsh_setup 落盘 -> 把策略文件链接给用户。
MCP server 不能直接跟用户对话,所以这一步由调用方 agent 转达。指定过之后引导不再出现。 -
新工具 dsh_setup
把用户选定的默认模型落盘。三种用法:default_model(裸 id 或 provider/id)、preset(用策略文件里某个预设)、
policy_markdown(整体替换策略文件正文)。只改"默认模型:"那一行,文件其余内容与用户自己的改动一律保留。
传一个不在已接入清单里的模型会被拒绝并列出可用清单,调用方据此重新问用户。 -
模型清单来自实例本身
dsh_health 新增 models(按 provider 分组,取自 DSH 设置文件里的 models 数组)与 modelPolicy,
可用模型不再由桥接层猜测。认不出形状时给出明确 error,而不是返回空数据。 -
选择模型的系统提示词是一个文件
config/model-policy.md 就是"选择模型的系统提示词",用户可以直接编辑。机器只解析两行
(第一条"默认模型:"行、每个预设小节里的"预设默认模型:"行),其余正文随便改;
桥接层每次调用都重读,改完立刻生效,不用重启。路径可用 DSH_SUBAGENT_POLICY 覆写。
文件自带一套预设("本项目方案")。 -
监控卡片显示"这次调用实际用的模型"
列表行显示模型,详情页增加模型 / 服务商 / 推理档三行;以任务运行时的元数据为准,缺失时退回调用方请求值。
升级方式:重新跑一次安装命令即可,无需迁移。已装的代码与配置都在 ~/.dsh 下,新版本会就地覆盖桥接层文件;
用户自己那份 config/model-policy.md 若已改过,不会被覆盖。
v0.1.7 — dsh_task 可按次指定模型/服务商/推理强度;写错即响亮失败
[0.1.7] — 2026-09-20
新增
dsh_task暴露reasoning_effort(单次调用改推理强度):lib/tasks.mjs的startTask()
早就支持它(写进task.json的reasoningEffort并拼出--reasoning-effort <值>),但lib/mcp.mjs
完全没有暴露这个参数。现在 schema 里有它(type: 'string',位置在provider之后)、
runTask()把它转发给startTask(),工具结果的头部在有值时回显reasoning_effort: <值>
(与model:并列),调用方能自己确认"我指定的档位真的到达了 DSH"。
修复
- 工具说明里举的模型名是错的,照抄只会失败:
model/provider的description原先举例
deepseek-v4-pro/claude-sonnet-4.6,而本机 DSH 里并不存在这些模型(实测本机
%DSH_HOME%\settings.yaml的llm-pi-ai.providers.rigol.models[]只配了deepseek-v4.1-flash),
调用方照抄就拿到UNKNOWN_MODEL。现在写明取值必须是本机已配置的模型 / 已注册的 provider,
例子换成真实存在的rigol/deepseek-v4.1-flash,并把reasoning_effort的取值来源
(models[].reasoningEfforts)与失败语义(UNSUPPORTED_REASONING_EFFORT)一并写清;
README §2 的参数表与 FAQ 同步。 - 可选参数"类型不对被静默忽略"改成响亮失败:
model/provider/reasoning_effort传非字符串
(例如reasoning_effort: 3、model: {})时,旧行为是悄悄当成没传,调用方以为指定生效了 ——
与本仓库"响亮失败、不静默降级"的口径不符。现在三者在runTask()里走同一个共享校验:
类型不对返回isError: true并点名参数名与"必须是字符串";空串/纯空白仍视为未提供(沿用 DSH 默认)。
test/selftest.mjs增加了可机检的断言(档位 schema、reasoningEfforts/UNSUPPORTED_REASONING_EFFORT
文案、model的"已配置 + UNKNOWN_MODEL"、类型不对被拒)。
v0.1.6 — 垫片入口失效时自愈;安装器不再吃掉 tool_timeout_sec
[0.1.6] — 2026-09-18
修复
- DSH Desktop 更新后垫片入口失效,表现为"派活静默失败"(真实故障):本机
dsh是 DSH Desktop
生成的 cmd 垫片,里面写死了打包入口。Desktop 换打包形态后入口从
resources\app.asar\lib\desktop-cli.js变成resources\app\lib\desktop-cli.js,而垫片不会跟着
更新 —— 每次经它调 dsh 都在 0.8 秒内Error: Cannot find module '…\app.asar\lib\desktop-cli.js'- exit 1。
lib/launcher.mjs原先把垫片参数直接拿去 spawn、不校验入口存在性,故障就以底层
MODULE_NOT_FOUND 冒出来。现在resolveLauncher()解析完就校验*.js/*.mjs入口:
入口在就照原样;不在就按候选清单自愈(安装根下的resources\app\lib\、
resources\app.asar\lib\、resources\app.asar.unpacked\lib\,以及缺失路径上app.asar↔app
互换后的同一位置,取第一个真实存在的,其余参数顺序不动);一个都没命中就响亮报错,错误里带
垫片路径、缺失入口、已尝试的候选清单与处置办法(删掉垫片让它重建,或改垫片里的入口),
不静默降级、不伪造成功。
- exit 1。
- 一个残留垫片不再埋掉本机其它可用的 dsh:入口失效且自愈不命中时先回退到下一个候选
(保持旧代码"换下一个候选"的语义,%APPDATA%\DSH Desktop\host-commands\…\dsh.cmd是一份陈旧
残留时,PATH 里那个可用的dsh仍然能用),每个候选的失败原因收集起来,所有候选都失败才抛
聚合错误:一句话结论 + 逐条列出每个候选为什么不行 + 处置办法(修垫片里的入口 / 删掉失效垫片
让 DSH Desktop 重建 / 用DSH_SUBAGENT_DSH_SHIM显式指定)。"响亮报错"与可用性不再二选一。 - 安装器不再把 Codex 手写的
tool_timeout_sec删掉:install.mjs的registerCodex()现在与
README §2 口径一致地写出tool_timeout_sec = 600。原先它不写这一行,而块替换是"整块覆盖",
于是用户按 README 手写的 600 会在下一次重跑安装器时被静默删掉 —— Codex 侧长任务又被 60s 掐断,
掐断时的notifications/cancelled还会被桥接层当成"调用方停了这一轮"而杀掉正在跑的任务。
同时把"这一块要不要改写"的判据从"整块文本一模一样"放宽成"要求的那几行都在",手写的说明注释
与自己调过的值不再被吞掉(实测:真实配置里那一块的 4 行注释原先会在重跑时消失)。 --dry-run现在逐条列出将要执行的动作(其中ok= 已是目标状态、一个字都没动)。原先干跑只说
"共 N 条动作",看不出会动什么 —— 而 README §2 /docs/install.md一直承诺"只看会改什么"。
新增
test/launcher-heal-probe.mjs(13 项,只读文件系统、不 spawn 任何东西,并把 PATH/APPDATA 收紧到
夹具里):断言入口失效时自愈到resources\app\lib\(候选优先级与其余参数顺序不变)、所有候选都
失效时抛聚合错且逐条列出每个垫片的原因与处置办法、exe 不在安装根时靠app.asar↔app互换
命中、候选 1 是失效残留而候选 2 可用时回退到候选 2、本机真实垫片的入口真实存在。
npm run test:launcher,并已并入npm run test:all(全套 10 个探针)。
v0.1.5 — 委派默认先等结果,超时才轮询(45s / 30s)
等待口径反转:先等到结果,超时才轮询
旧口径写着「wait_seconds 默认 30,建议传 0 或 ≤30」,于是每次委派都变成「立刻拿 job_id
- 每 20~30 秒轮询一次」:一个 5 分钟的任务要十几次往返,而每一轮都是调用方一次完整的推理
—— 又慢又贵。
现在 dsh_task 默认自己等 45s(短超时):任务在这段里结束就把结果直接带回,零轮询;
只有到点还没结束才返回 status="running",那时才用 dsh_task_status(默认等 30s)
一轮一轮推到终态。轮询从默认路径降级为兜底。
实测:同一个「只回答两个字」的任务,DHS 侧整轮 1.4~1.7s,一次不传 wait_seconds 的
dsh_task 调用 3.5s 就带着 status: ok 返回,且回答里不再让调用方去轮询。
为什么是 45s / 30s
| 调用方 | 单次工具超时 | 结论 |
|---|---|---|
Codex(codex-mcp-client,本机最常跑的调用方) |
60s(tool_timeout_sec 默认值) |
最紧的约束 |
| Claude Code | stdio 空闲窗默认 30 分钟,超 2 分钟自动转后台 | 远宽于 45s |
| Cursor | 官方文档未公开 | 按 60s 保守处理 |
取最紧的 60s 留 25% 余量 ⇒ 45s;轮询那段更保守(30s),保证永远打不到超时。
再往上加会踩到这个分支:harness 先掐断这次调用 ⇒ 可能发 notifications/cancelled ⇒
按 0.1.4 的取消语义,那会被当成「调用方停了这一轮」而把还在跑的任务杀掉。
想让长任务也一次返回,要先把调用方的工具超时调大(tool_timeout_sec = 600),
再用 DSH_SUBAGENT_WAIT_SECONDS / DSH_SUBAGENT_STATUS_WAIT_SECONDS 同步调大这里。
变化清单
dsh_task默认等待 30s → 45s;dsh_task_status不传wait_seconds时不再「立刻返回现状」,
改为自己等 30s。- 工具文案全部改写(否则模型还会照旧文案传
wait_seconds=0):initialize.instructions、
dsh_task.description、wait_seconds字段说明、dsh_task_status描述与字段说明、
以及返回running时的下一步提示 —— 三处都在劝退0与>60。 dsh_health新增defaultWaitSeconds/statusWaitSeconds/waitModel。- 新增
test/wait-policy-probe.mjs(11 项,真起 MCP server + 真派任务),已并入npm run test:all;
自检里多一条口径回归闸(两个默认值必须等于当前策略且都< 60)。
全套自检 296/296(9 个探针:panel 117、selftest 54、observer 35、autostart 26、ledger 20、
leaf 14、exec-surface 11、wait-policy 11、monitor-live 8)。
顺带:RIGOL 模型的推理强度
README §3.1 记了一份本机的 llm-pi-ai 写法:给 deepseek-v4.1-flash 声明
reasoningEfforts,并在路由上写 reasoning: high,把思考强度固定住。
要点是没有 75 这个取值:网关只认
none|minimal|low|medium|high|xhigh|max,传 75 会 400;75 是 harness 侧的显示刻度,
落到该网关就是 high。改之前 DSH 完全不带 reasoning 参数,实测同一个请求
reasoning_tokens=103,显式 high 只有 35 —— 默认比 high 更啰嗦。
v0.1.4 — 调用方停这一轮时,子代理这边的 dsh 实例跟着停
调用方停这一轮,子代理这边也停
问题:在 Cursor / Claude Code 里按"停止本轮",harness 会发 MCP 的 notifications/cancelled,
而桥接层原先直接忽略它(代码注释写着"留给任务自己按 timeout 收尾")。后果是调用方那边已经
停了,这边的 dsh 子进程还在后台继续跑、继续烧 token,还往工作区里写文件 —— 看起来像"幽灵改动"。
修法:把四条明确的停止信号都真的执行。
| 调用方做了什么 | 桥接层的动作 |
|---|---|
取消正在等待的 dsh_task / dsh_task_status 请求 |
停掉这次请求对应的那个任务(精确,不碰别的任务) |
| 发来取消但对不上具体请求(取消晚到了) | 只在本连接恰好一个任务在跑时停它(本连接 ≈ 这个 harness 窗口) |
| 关掉 MCP 连接 / 关掉 harness | 停掉本连接名下所有在跑任务,然后退出 |
直接杀掉 MCP 进程(SIGTERM/SIGINT) |
先停掉本进程名下所有任务,再退出 |
刻意没做"按轮询间隔猜调用方还在不在":轮询间隔里调用方可能只是在思考,拿它当依据会误杀
真正在跑的活。所以只认上面这四种明确信号;客户端既不取消也不关连接时,任务仍会跑到自己的
deadline,这种情况请用 dsh_task_cancel / dsh_task_kill 主动止损。
另外把取消语义写进了 dsh_task_status 的工具描述 —— 只想看一眼状态、不希望影响任务时用
wait_seconds=0,免得在不知情的情况下取消一次等待就把任务停掉。
验证
新增 test/cancel-propagation-probe.mjs(npm run test:cancel,7 项):真起 MCP server、真派任务,
在轮询请求在途时发取消通知,断言任务被停、子进程真的消失(用 process.kill(pid, 0) 探,
不是只看状态字段)、连接断开后 server 自行退出。
计数口径订正
README / CHANGELOG 里的"全套 N/N"一直把我统计用的收尾行"…通过 ✅"也数了进去,普遍多算 1~2 项。
现在只取各探针自报的通过数:全套 284/284(panel 117、selftest 53、observer 35、
autostart 26、ledger 20、leaf 14、exec-surface 11、monitor-live 8),历史行已回订正。
代码与测试行为没有任何变化。
说明:Windows 上被
taskkill /F/TerminateProcess强杀时,进程拿不到执行收尾代码的机会,
那条路径兜不住;真正兜住的是前三类。
v0.1.3 — 沙箱档 shell 空转不再静默放过;兼容会话日志改名
修掉"任务跑了但什么都没做"被静默放过
把 Cursor 里那件事当假设去复现,结论比"spawn 层故障"更具体:切进沙箱档就空转。
同一条 Write-Output SPAWN-PING,只换权限档:
| 权限档 | shell 工具实际返回 |
|---|---|
danger-full-access |
SPAWN-PING\r\nELAPSED_MS=59\r\n ✅ 真的执行了 |
workspace-write / read-only |
"\r\n",isError: false ✗ 进程从未启动 |
对照实验(exit 3 不返回退出码、Start-Sleep 5 根本不睡、Set-Content 不生成文件、而文件读写
工具一切正常)把"输出丢失"这类解释都排除了 —— 故障点在创建进程那一层。
两点界定,都靠证据:
- 与 DSH 版本无关:升级前后各取一份真实会话日志,
tool/result里都是同样的
"\r\n"+isError:false。 - 与本插件配置无关:
subagent与desktop两个 profile 的沙箱相关行集合一致
(sandbox-local/sandbox-policy/pwsh-sandbox/fs-sandbox,且两边都没有额外配置)。
桥接层现在会替调用方说出来
任务收尾时解会话日志,把"空内容 + isError:false"的 shell 调用数出来,在答复末尾附告警
(次数、命令样例、当前权限档、出路),并落进任务记录的 shellHollowCalls / shellHollowSamples:
⚠️ 执行面告警:本次会话有 9 次 shell 调用**没有真正执行**。
证据:这些调用返回的是空内容且 isError=false(沙箱档把命令吞掉了,进程从未启动)。
当前权限档:workspace-write;本机实测 danger-full-access 下 shell 正常,
workspace-write / read-only 下必然空转(DSH 侧问题,升级前后一致)。
误报验证:真实日志里沙箱那次命中 11 次、danger-full-access 那次 0 次。
顺带修掉一个静默回归:会话日志改名
DSH 0.1.5 起会话日志从 session.jsonl.zstd 改名成 session.v3.jsonl.zstd(实测改名时刻与本机
升级时刻一致)。观察器与桥接层原先都硬编码旧名、找不到就安静返回 null,于是卡片上的 token
用量对所有新会话都成了空 —— 一个不报错的假数字。现在统一按 session*.jsonl.zstd 找、取最大者
(不依赖 mtime),并同步到各探针;leaf-only-probe 的会话级断言也不会再"静默空转"。
验证
- 全套 287/287:panel 117、selftest 54、observer 35、autostart 27、ledger 21、leaf 14、
exec-surface 11、monitor-live 8。 - 新增
test/exec-surface-probe.mjs(11 项,离线)锁死:日志改名兼容、空转判定规则、
报错不算空转、正常会话零误报、告警文案。 - 真任务端到端:沙箱档派一条
echo,答复末尾确实带上告警。
沙箱档下要跑命令(构建 / 测试 / CLI),请显式传
permission: "danger-full-access";
只做文件读写的任务不受影响。观察器的修复要等 DSH Desktop 下次启动才生效(插件不热重载),
桥接层与探针的修复立即可用。
v0.1.2 — MCP 连接不再产生 warning
修掉 MCP 连接时的 warning
Cursor / Claude Code 会把 MCP 子进程的 stderr 一律渲染成 warning/error。我们启动时打了一行
"启动成功"的信息,于是你在 Cursor 里就看到告警(Cursor 的 mcpprocess.log 原文):
[warning] [McpProcess stderr] ERR dsh-subagent: MCP stdio server ready (bridge v0.2.0)
现在的约定
- 正常路径下 stderr 一个字都不写;stderr 只留给真的出问题(例如进程栈探测不可用的降级告警)。
- 要排查就设
DSH_SUBAGENT_DEBUG=1,启动横幅与调试行会回来。 - 自检里钉了这条(
正常启动不往 stderr 写任何东西),以后不会退化。
顺带修掉
- 版本号只有一个来源:
SERVER_VERSION原先硬编码0.2.0,与package.json(0.1.1)各自
漂移,横幅里的版本和仓库对不上。现在直接读package.json。 - 公开文档去掉与特定机器环境强绑定的说明(含某台机器特有的文件头标记与十六进制查看技巧),
相关设计理由改成环境无关表述。README 章节重新编号(§1~§10),交叉引用全部同步。
验证
- 全套 275/275(panel 117、selftest 54、observer 34、autostart 27、ledger 21、leaf 14、monitor-live 8)。
- 手工握手实测:默认 stderr 0 字节,
serverInfo.version与package.json一致;
DSH_SUBAGENT_DEBUG=1时横幅出现且版本正确。