Skip to content

Releases: xDJTomato/deepseek-harnessed

v0.1.11 —— 面板不再拖着宿主 GPU 一直烧;顺带发三条积压的委派规则

Choose a tag to compare

@xDJTomato xDJTomato released this 29 Sep 03:24

0.1.11 —— 面板不再拖着宿主 GPU 一直烧,顺带发三条积压的委派规则

  1. 修复:子代理面板与设置窗同时打开时界面卡死

现象是你报的:两个窗口一起开,界面非常卡。根因是两件事相乘 ——

  • 卡片上那层缓慢扫过的光泽是常驻无限动画。这张卡片带 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 秒才走一格(有任务在跑时仍是每秒)。

  1. 新增:禁止委派清单(四类活不派给子代理)

dsh_task 描述、initialize.instructions 与委派前言都写清了:短指令作业(预计 < 30s 且不需要独立
上下文的)、常驻/长命进程(dev server、watch、daemon、交互式终端)、不能轻易结束的进程(占端口、
开事务、拿锁、必须成对收尾的独占资源)、需要人交互的活。机制原因写在描述里:子任务结束或被
cancel/kill/deadline 收尾时都会连进程树一起收尾 —— 由子代理起的服务会跟着消失,而"某个进程
还活着"也写不成可机检的 acceptance。一次性短命服务(起测试库、用完就停)仍可派,但必须成对收尾。

  1. 新增:先拆再派,能并行就并行

一次只派一个可独立验收的产物、目标 expected_seconds ≤ 300s;要改 3 个以上文件、估时超 300s、
或属于"先调研再实现再验证"的多阶段作业必须先拆;互不依赖的子任务同一轮里并发派出,只有真存在
依赖才串行。委派前言同时告诉子代理:这次任务是从更大目标里拆出来的一小片,只做这一片。

  1. 修复:桥接进程留生命周期痕迹,未处理异常不再带走整条通道

启动(带版本号)、退出原因、未处理异常/未处理拒绝的堆栈,各写一行到 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 —— 预览窗口只读显示子代理的对话内容

Choose a tag to compare

@xDJTomato xDJTomato released this 21 Sep 12:48

0.1.10 —— 预览窗口里只读显示子代理的对话内容

  1. 新增:只读会话转录

运行中的外部任务不能点开(点开 = 宿主接管会话写权、写坏子进程正在写的日志),原先那个只读
详情页只能看到 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 两个字段(转录是外部数据),转录节点上没有任何点击或写入钩子:
只读是结构性的,不是靠约定。

  1. 修复:自检不再拿用户自己的策略文件当"未指定"用

test/selftest.mjs 原先把仓库里那份 config/model-policy.md 原样复制成临时副本,再断言
"默认模型未指定 → 会输出首次接入引导" —— 可策略文件本来就是给用户改的:用户一设默认模型,
这 4 条断言就红。现在副本里显式把「默认模型」那行改成 (未指定)(那才是要测的状态),
仓库那份只做结构性断言:(未指定) 或一个裸模型 id 都算合法。

  1. 升级后怎么确认生效

宿主里的观察器是 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 — 文档重写为朴实技术语言 + 公开仓库脱敏

Choose a tag to compare

@xDJTomato xDJTomato released this 21 Sep 06:40

0.1.9 —— 文档改写为朴实技术语言 + 公开仓库脱敏

这个版本没有新功能,改的是文档本身,以及文档里本不该公开的信息。

  1. 四份标准文档重写
    README.md、docs/clients.md、docs/configuration.md、docs/install.md 用 gemini-3.7-flash 重写为
    朴实技术语言:把"磁贴 / 硬承诺 / 响亮失败 / 口径 / 幽灵 / 垫片 / 探针 / 闸门"这类生造说法
    换成常见词(卡片 / 明确的执行上限 / 直接报错不静默 / 规则 / 残留记录 / 入口脚本 / 自检脚本 / 检查)。
    技术信息一条没少:行内记号 704 → 714,代码行 139、版本号 9、跨文档链接 3 全部保留。
    重写过程中一度掉到只剩 454 个记号,由机检闸门逐项补回 —— 该闸门同时卡
    「行内记号 / 代码块行 / 版本号 / 跨文档链接一个都不许丢」与「黑话 0 命中」,不靠人眼看。

  2. 公开仓库脱敏
    本仓库是公开的,而文档里带了内部信息。本次替换成中性占位符:provider 名、API key 环境变量名、
    内部网关地址、只在本机存在的模型别名;项目名从策略文件里去掉;
    工具描述里"本机当前是某个 provider"这类写死本机事实的句子删掉(模型清单本来就该从
    dsh_health.models 查)。模型 id 示例仍保留,示例照抄仍可用。

  3. 测试可移植性(顺带修复)
    test/selftest.mjs 原先断言"本机 43 个模型""本机默认模型是 provider/某模型",换一台机器
    clone 下来必红。现在断言结构(providers 是对象、每项都是非空 id 数组、顺序与去重与独立解析
    一致、defaultModel 的 provider 前缀必须是真实 provider),并加一条跨文件一致性断言:
    枚举结果必须覆盖策略文件里"预设默认模型"点名的模型。

升级方式:重新跑一次安装命令即可。已装的代码与配置都在 ~/.dsh 下,新版本会就地覆盖桥接层文件;
你自己那份 config/model-policy.md 若已改过,不会被覆盖。

0.1.8 — 模型选择:首次接入引导 + 用户可编辑策略文件

Choose a tag to compare

@xDJTomato xDJTomato released this 21 Sep 06:14

0.1.8 —— 模型选择改成"首次接入 + 可编辑策略文件"

这个版本解决一个真实问题:以前桥接层把"可用模型"写死在工具描述里,换机器、换 DSH 实例、换了网关,描述立刻变成错的,而调用方会照着错描述去传参数。

改动:

  1. 首次接入引导
    策略文件里还没指定默认模型时,dsh_task 与 dsh_health 的返回值末尾会带一段紧凑引导:
    先枚举这个 DSH 实例已接入的模型 -> 请用户指定一个默认模型 -> 用 dsh_setup 落盘 -> 把策略文件链接给用户。
    MCP server 不能直接跟用户对话,所以这一步由调用方 agent 转达。指定过之后引导不再出现。

  2. 新工具 dsh_setup
    把用户选定的默认模型落盘。三种用法:default_model(裸 id 或 provider/id)、preset(用策略文件里某个预设)、
    policy_markdown(整体替换策略文件正文)。只改"默认模型:"那一行,文件其余内容与用户自己的改动一律保留。
    传一个不在已接入清单里的模型会被拒绝并列出可用清单,调用方据此重新问用户。

  3. 模型清单来自实例本身
    dsh_health 新增 models(按 provider 分组,取自 DSH 设置文件里的 models 数组)与 modelPolicy,
    可用模型不再由桥接层猜测。认不出形状时给出明确 error,而不是返回空数据。

  4. 选择模型的系统提示词是一个文件
    config/model-policy.md 就是"选择模型的系统提示词",用户可以直接编辑。机器只解析两行
    (第一条"默认模型:"行、每个预设小节里的"预设默认模型:"行),其余正文随便改;
    桥接层每次调用都重读,改完立刻生效,不用重启。路径可用 DSH_SUBAGENT_POLICY 覆写。
    文件自带一套预设("本项目方案")。

  5. 监控卡片显示"这次调用实际用的模型"
    列表行显示模型,详情页增加模型 / 服务商 / 推理档三行;以任务运行时的元数据为准,缺失时退回调用方请求值。

升级方式:重新跑一次安装命令即可,无需迁移。已装的代码与配置都在 ~/.dsh 下,新版本会就地覆盖桥接层文件;
用户自己那份 config/model-policy.md 若已改过,不会被覆盖。

v0.1.7 — dsh_task 可按次指定模型/服务商/推理强度;写错即响亮失败

Choose a tag to compare

@xDJTomato xDJTomato released this 20 Sep 05:08

[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

Choose a tag to compare

@xDJTomato xDJTomato released this 18 Sep 06:17

[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
      互换后的同一位置,取第一个真实存在的,其余参数顺序不动);一个都没命中就响亮报错,错误里带
      垫片路径、缺失入口、已尝试的候选清单与处置办法(删掉垫片让它重建,或改垫片里的入口),
      不静默降级、不伪造成功。
  • 一个残留垫片不再埋掉本机其它可用的 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)

Choose a tag to compare

@xDJTomato xDJTomato released this 18 Sep 03:10

等待口径反转:先等到结果,超时才轮询

旧口径写着「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 实例跟着停

Choose a tag to compare

@xDJTomato xDJTomato released this 14 Sep 02:53

调用方停这一轮,子代理这边也停

问题:在 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 空转不再静默放过;兼容会话日志改名

Choose a tag to compare

@xDJTomato xDJTomato released this 14 Sep 02:24

修掉"任务跑了但什么都没做"被静默放过

把 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

Choose a tag to compare

@xDJTomato xDJTomato released this 11 Sep 13:08

修掉 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 时横幅出现且版本正确。