- Home
- Plugins
- UI Enhancements
- dsh-status-turbo
dsh-status-turbo
dgy970516/dsh-status-turbo
A status bar in the composer footer showing the current phase, how long it has been running, the current step token count and a no-progress warning, projected from session logs without polling the host, plus a detail panel that opens on click.
Install
dsh plugin --profile web add github:dgy970516/dsh-status-turboREADME
dsh-status-turbo
English · 中文
让「现在到底卡在哪」变成 composer 上方一行会跳的字。
dsh-status-turbo 是 DeepSeek Harness 的插件。它在对话输入框上方常驻一条极窄的状态条,实时显示:
● 执行工具(bash) 2:14 3.1K tok
长命令跑到第 3 分钟那种「页面一动不动、日志一行没有、它到底死没死」的时刻,这一行就是答案。
它显示什么
它并进 DSH 自带的 composer 底部状态行(就是「6 轮 283 步 · 266 tok/s / 27.9M tok · 缓存命中 96% / 10%」那一行的右侧), 所以它不额外占一行、也不会突然出现在输入框上方。只补原生那行没有的信息:
| 字段 | 含义 | 例子 |
|---|---|---|
| 阶段 | 组装提示词 / 请求模型 / 接收流 / 执行工具(<名字>) / 等待批准 / 压缩上下文 | ● 执行工具 bash · 2:14 |
| 本阶段已耗时 | 每秒刷新。不依赖任何推送 —— 客户端用本地时钟算,所以日志静止时它照样跳 | 同上 |
| 本步 token | 当前 step 累计输入+输出(0.19.2 起只留数量,完整说明进 title) | 3.1K tok |
| 档位 | 0.19.2 起不再占状态条 —— 原生推理档选择器已经写着同一个值,它只留在详情面板。值是 B1 这一步实际发出去的推理档位(取自会话日志的 request/header,不是本地推测) | —— |
| 无进展 | 超过 noProgressMs(默认 90s)没有任何新产出 → 该格变黄并标注卡了多久(0.19.2 起只留四个字,判据全文进 title) | 2:49 无新输出 |
宿主把这套东西投影成 turbo wire 视图,版本 v: 13、共 37 键(v1 的 19 键 → v2 的 mountStatus、
prefixEpoch、prefixCause、prefixAt、lastTurnMs、topTools、hookFaults、idleLine、lastSpill → v3 的 activity
→ … → v9 的 rotation;v7 与 v10 不在顶层加键。v13 加顶层 newSession,承载「新会话」页的开场话术
({ text, oldId, cwd },见「新会话页(0.18.0)」)。客户端把
EXPECTED_V 钉死在 13:版本对不上时,状态条位置显示红字「投影版本错配(宿主 vN ≠ 客户端 vM)」,而不是静默消失。
刻意不重复原生那行已有的:会话轮/步、tok/s、总 token、会话级缓存命中、上下文占比。
每步缓存命中率仍留在投影数据里(hitRate),但不再单独占一格 —— 同一行里放两个"缓存命中"只会让人分不清。
空闲时这一格不再消失 —— 它会保留一条「上回合摘要」:阶段名 + 上回合耗时(● 请求模型 · 上回合 42.3s)。
宿主把它作为 idleLine / lastTurnMs 下发,所以没有新事件时摘要也还在。
点一下这一格会在输入框下方展开详情面板(槽位 conversation.input.dock,id dsh-status-turbo.detail,order 0);
再点一下收起。收起状态 —— 或第一份快照还没到 —— 面板完全不渲染。面板从上到下常驻 11 个分区:现在(这一步在干嘛,
阶段中文名 · 已耗时 · 工具名)、活动流(最近若干条「模型想了什么 / 调了什么工具 / 结果如何」,最新的在最上面)、
本回合 / 上回合 / 缓存与用量 / 前缀 / 最慢的工具(取 maxMs 前 5)、等待占用 / 会话轮换 / 工具增强 / 交接文档;
最近瘦身(lastSpill)与最近的故障(hookFaults)是条件分区 —— 没有内容就不出现。
活动流只进内存(环缓冲,不落盘、不新增依赖、不改发出去的请求):assistant/message 记一条思考摘要(截到
reasoningChars 字符)、tool/call 记工具名与已脱敏的参数、tool/result 记结果 / 报错的前 resultHeadLines 行、
turn/end 记结束原因。工具参数里命中 redactKeys(大小写不敏感、子串匹配)的键值上屏前换成 '***',循环引用降级为
[Circular],绝不因为脏参数报错。
安装
# 装进 profile(推荐)
# 用 DSH 的插件管理器安装,或在 profile 的 cordis.patch.yml 里 insert 本包
手工安装(profile 拼装)——把本包放进 profile 的 node_modules,然后在 profile 的 cordis.patch.yml 里:
- insert:
- id: dsh-status-turbo
name: 'dsh-status-turbo'
config:
statusBar: true
noProgressMs: 90000
downgrade: true
downgradeFloor: low
minQuietSteps: 2
spillThresholdBytes: 32768
bulkInjectionBytes: 32768
evidenceWhitelist:
- '/exit code:?\s*0\b/mi'
- '/(^|\n)\s*(#\s*(pass|tests|suites)\s+\d+|\d+\s+passing|Tests:\s+\d+\s+passed|Test Suites:\s+\d+\s+passed|ok\s+\d+\s+-)/m'
- '/(^|\n)\s*(Successfully compiled|Compiled successfully|Found 0 errors|\d+\s+files?\s+checked,?\s*0\s+errors|Build succeeded|Done in \d)/mi'
stuckAction: warn
stuckRounds: 3
stuckRepeats: 3
mojibakeMinRuns: 2
每个键都可省;上面只是一段最常用的示例(12 键)。完整的 46 键与默认值见下文「配置」与
「交接文档自动导出」「到量轮换」两节的表 —— 默认值的唯一源是 lib/state.js · DEFAULT_CONFIG(46 键),
根目录 cordis.patch.yml 逐字镜像它。
⚠️ 不要把同一个包同时列进
dsh.profile.bundles和 profile 的cordis.patch.yml:会被加载两次。
省算力:按步降推理档(B1)
插件会盯着工具轨迹,在这一步明显不需要深想的时候把这一步的推理档降一档:
- 连续 ≥2 个已结束的步骤里只跑了只读工具(
read/glob/grep/web_search…)、且一次错都没报 → 降一档; - 一旦出现写入类工具(
bash/edit/write/sql_exec…)或任何报错 → 立刻回到原档; - 原档是你当前为该会话选定的档位,插件只降不升,也不改你的默认模型或提供方。
降档会在两个地方留下证据:DSH 日志里的 B1 降档:…,以及详情面板里多出来的档位行(0.19.2 起状态条不再重复它)。
| 键 | 默认 | 说明 |
|---|---|---|
downgrade | true | 总开关。只有显式写 false 才关闭(关掉后连 llm 服务都不依赖) |
downgradeFloor | low | 最多降到哪一档。按该模型自己公布的有序档位表取下标比较;模型没公布这一档就退化为一档一档降 |
minQuietSteps | 2 | 连续几个"干净只读步"才降档。设 1 更激进,也更容易误降 |
readOnlyTools | 10 个工具名 | 0.9.2(P1-4):B1 的只读工具白名单 —— 只有明确只读的工具才算「纯读取 / 列举 / 检索」;不在表里的工具(bash / edit / write / sql_exec / univer_execute…)一律视为「可能写」,任何一次这类调用都会打断连续段(刻意保守)。元素必须是非空字符串,非法元素被过滤;过滤后为空则回落本表([] 的语义是「未配置」,不是「没有只读工具」)。默认:read / read_image / glob / grep / list_dir / search_context / web_search / web_fetch / cordis_inspect_list / cordis_inspect_query |
档位名不写死在代码里 —— 一律问 ctx.llm.resolveModelInfo()。任何一步只要它拿不准(读不到档位表、
模型没公布当前档、服务缺失、判据抛错),就按原档执行,绝不因为插件让请求失败。
省上下文:超大工具结果瘦身(B3)
单条工具结果太大时,宿主半会在它送进模型前先瘦身:
- 先请宿主自己的
toolResultPruner.pruneContent削一遍正文; - 削完仍达到
spillThresholdBytes(默认 32768)时,交给宿主既有的 spill 后端(spillStore.saveText)落盘, 瘦身后的正文里带一行通知,内含落盘文件的绝对路径 —— 这条路径可以直接read(配offset/limit)或grep。
通知文案由 lib/spill.js 原样渲染:
(已省略 {bytes} bytes;完整结果落盘于:{locator}。用 read 配 offset/limit,或 grep 这个路径继续。)
后端给出的 locator 若不是绝对路径,插件降级处理而不是猜一个(绝不印假路径)。落盘那一下是宿主的动作 ——
见下面「设计约束」里的说明:dsh-status-turbo 自己从不为了写而打开文件。
看得见缓存前缀失效(B4)
缓存前缀作废会以状态条上的 前缀已失效 胶囊显示(只在作废还新鲜时显示,即最近 5 分钟内)。宿主统计三个触发源:
| 触发源 | 判据 |
|---|---|
compaction-start / compaction-end | 出现 compaction/start 或 compaction/end 事件 |
bulk-injection | 单条 tool/result 载荷 ≥ bulkInjectionBytes(默认 32768) |
wire 视图里对应 prefixEpoch / prefixCause / prefixAt。在一份真实回放会话里这个计数读出来是
17 次前缀作废(bulk-injection ×9、compaction-start ×4、compaction-end ×4)—— 见 tools/replay-log.mjs。
塞进同一次工具返回里的提示(C1 / C2 / C3)
三条廉价校正都追加在当前这次工具返回里 —— 不新增任何往返,会话步数也不变:
- C1 —— 没有可核对证据:整条结果里找不到任何证据行(不命中
evidenceWhitelist,恰好 3 条正则源,例如exit code: 0、N passing、Found 0 errors)时,追加「上一条没有可核对的证据……别据此下结论。」 - C2 —— 读被截断:一次
read用了offset/limit而文件后面还有内容时,追加 「你只看了 N / M 行(绝对路径);后面还有内容没读,别基于这个片段下结论。」 - C3 —— 乱码 / 噪声:中文损坏(连续 U+FFFD 达到
mojibakeMinRuns段)或输出被噪声淹没时,追加 「这段输出里的中文已经损坏……」/「这次输出被噪声淹没(x / y 行不是目标输出)……」
交接文档自动导出(0.7.0)—— 默认开
会话变长时,自动把当前会话导成一份 Markdown 交接文档落进工作区。为什么需要它:bili 的 preflight 护栏按
单次请求核心文本判死(payload core text alone is ~… bytes … vs the model window …),会话太长时连转发机会
都没有;而 bili 官方的迁移路径就是 bili export 出交接文档(CONFIGURATION.zh-CN.md:1791、:2123-2132)。
判据与执行都在 lib/handoff.js(纯逻辑 + 可注入 I/O,测试不真起进程、不写盘):
- 阈值口径:
usage.inputTokens(本步发出去、被上游计费的输入 token)—— 它量的是 bili 压缩之后的量, 与 bili preflight 那条「单次请求核心文本」判死不是同一个口径:preflight 拒发的那一次没有上游响应、 也就不会产生 usage 事件,所以只有字节口径(handoffExportBytes)看得见那个失败。 - 节流:距上次导出又长了
handoffExportMinDeltaTokens才再导一次(长会话不会每轮都导)。 - 命令探测(显式配置一级 + 兜底两级):
handoffExportBiliBin绝对路径直接用 → 否则用process.execPath(node) 跑包内的billion-context/dist/index.js(先require.resolve('billion-context')定位包根,再兜底拼 profile 的node_modules/billion-context/dist/index.js)。都拿不到是「缺席」,不是故障:面板显示一行灰色「未找到 bili」, 绝不进故障列表。(.bin/bili.CMD这条路已弃用:CVE-2024-27980 之后 Node 拒绝无 shell 启动.cmd,实测spawn抛EINVAL;而require.resolve('billion-context/dist/index.js')抛ERR_PACKAGE_PATH_NOT_EXPORTED。 证据见docs/review-0.7.0-handoff.md的 P0-1 / P0-2。) - 导出命令:
bili export <sessionId> [--full] --output <会话 cwd>/<handoffExportDir>/handoff-<时间戳>.md(spawn,非阻塞,120 s 超时;本进程最多handoffExportMaxPerProcess次,默认 64 —— 0.7.5 从硬编码的 8 抬高)。 - 不做:交接导出本身不切会话 —— 界面上打开哪条会话是客户端状态,宿主侧插件改不了。
(「切会话」是另一个能力:到量轮换会建空白新会话、再让客户端
sessions.open过去,见下文「到量轮换」。 0.9.0–0.9.2 客户端那半边其实一次都没执行,0.9.3 才修好,见CHANGELOG.md · ## [0.9.3]。)
配置(默认值唯一来源是 lib/state.js · DEFAULT_CONFIG,cordis.patch.yml 逐字镜像)
| 键 | 默认 | 含义 |
|---|---|---|
handoffExport | true | 总开关。默认开(装上就工作);显式写 false 时它真的什么都不做(不探测、不起进程、不建目录) |
handoffExportTokens | 240000 | 本步输入 token 达到它才考虑导出(0.8.1 从 400000 降 —— 本机实测体感卡点是 341421) |
handoffExportMinDeltaTokens | 60000 | 距上次导出又长了这么多才再导(0.8.1 从 100000 降) |
handoffExportDir | 'handoff' | 导出目录(相对会话 cwd) |
handoffExportFull | false | 是否带 --full(附每个块的原始消息;文档会大很多) |
handoffExportBiliBin | '' | bili 可执行路径;空串表示走上面的两级探测 |
handoffExportTimeoutMs | 120000 | 0.9.2(P1-4):单次 bili export(以及车道列表读取)的硬超时(毫秒)。超时路径先 kill,两个管道 destroy 之后宽限期到了再升级 SIGKILL(lib/handoff.js · runBili);两个调用点(runExport / listLanes)共用这一个键,非法值回落 120000 |
handoffExportWindowPercent | 25 | 阈值 = 窗口的百分之多少(窗口已知时)。token 与字节两个口径共用这一个旋钮(0.8.1 从 40 降:旧值 40%×1M 恰好等于旧 token 旋钮 ⇒ 不 binding) |
handoffExportMaxPerProcess | 64 | 本进程最多起多少次导出;用满会记 failed + 一条 warn |
handoffExportSubLanes | false | 是否连该会话的子车道(|<sub:…>)一起导。0.7.8 默认关(一次越桶会按车道数乘一份) |
handoffExportBytes | 1800000 | 字节口径:模型可见体量达到它就触发(与 token 口径取或);0.8.1 从 3000000 降 = 240000 × 7.5 |
handoffExportMinDeltaBytes | 450000 | 字节口径的节流(0.8.1 从 8000000 降 —— 旧值大于阈值,字节口径的第二次导出是条死路) |
handoffExportFreshMs | 300000 | 只对新鲜事件触发:折到的事件距现在超过这么多毫秒就不动作(挡冷启动重放的批量触发) |
handoffExportRefreshBytes | 0 | 同一车道是否允许重复导出。0 = 每车道最多 1 份(0.7.8 默认);设成字节增量才恢复「随水位桶刷新」 |
rotation | 'auto' | 到量轮换模式:off / manual(面板一行 · 可点)/ auto。默认 auto(0.9.1 起) |
rotationCooldownMs | 600000 | 两次轮换的最小间隔(进程级生效,防连环轮换) |
rotationOpenChild | true | 0.9.0:宿主建好新会话后是否自动切过去(auto 模式下即自动切) |
rotationFreshMs | 300000 | 0.8.1:只对新鲜事件触发轮换判据(挡冷启动重放,同 handoffExportFreshMs) |
rotationRequireIdle | false | 0.9.2:是否要求「当前没有开放轮次」才轮换。默认 false —— 建一个空白新会话不打断正在跑的回合;设 true 回到 0.8.0 的老行为(实测后果:从不空闲的会话一个也轮换不了) |
rotationArchiveOld | false | 切换成功后是否归档旧会话(0.8.1 定为 false:归档后侧边栏与 @ 引用读同一份列表投影 ⇒ @{old} 指引会失效;界面里没有取消归档入口) |
rotationIncreaseTitle | true | 轮换建出的子会话是否按旧会话标题做 (N) 递增改名(0.20.0 起复活:0.9.0 起是空白新建,子会话没有标题事件 —— 0.20.0 之前回退规则只能拿首条种子模板截断 ⇒ 每一代同名,0.21.1 起连那条首条消息也没有了,自动命名更加认不出亲;宿主现在自己成了消费者,命名与官方 sessions.fork({ increaseTitle: true }) 同一套约定) |
rotationCreate | false | 到量时是否真的建新会话(0.20.0 起默认 false:只提醒)。判据照旧走 shouldRotate,但只落一条 remind 记录(ROTATION_STATUS.REMIND)、不建会话不切界面;设 true 恢复 0.19.x 的自动建会话 |
rotationSeedMessage | '继续旧会话 {old}(工作目录 {cwd})。…' | 0.9.0 起是开场话术的模板({old} = 旧会话 id,{cwd} = 工作目录)。0.21.1 起不再自动写进新会话 —— 它只用于状态条「复制话术」与设置页话术卡那段可复制文本;这段文字是「把旧会话 id 送进上下文」的唯一来源(光有 parentSession 元数据不保证进模型上下文) |
「模型可见体量」是累计吃进去的字节数(累积的消息体 + 最新一次请求头),刻意不回落: 本部署里 compaction 并没有把 payload 压下去(炸掉的那条车道压缩了 4027 次、最终仍到 ~17 MB), 所以累计量才是能提前报警的信号。另外
handoff.message是稳定 code(面板按语言渲染,未知 code 原样显示)。触发策略(0.7.8 起):只有正在跑的会话会触发(
handoffExportFreshMs),且每个车道最多只出一份 (handoffExportRefreshBytes: 0),默认还不导子车道 ⇒ 一个会话通常就 1 份。 0.7.7 之前是「3 MB 出第一份、之后每多 8 MB 补一份」,0.7.6 更早时是每 3 分钟一份 (当天实测写掉 64 份、把进程上限用尽)。
详情面板的「交接文档」分区会显示 未开启 / 等待达到阈值 / 正在导出… / 已导出 → <路径> / 导出失败 · <原因> / 未找到 bili,路径可直接选中复制。
到量轮换(0.8.0 起;0.9.0 改为方案 B;0.9.1 起默认 auto)
越过阈值且折到的事件是新鲜的(rotationFreshMs;0.9.2 起不再要求会话空闲,见 rotationRequireIdle)时,宿主半直接把轮换做完(不再只写一个 armed 让客户端去 fork):
-
宿主建一个空白会话(新会话的 header 带
cwd与parentSession,但不继承旧历史)。0.19.0 起走ctx.agents.create({ sessionId, meta })—— 在代理生命周期里发布,持久化层才会给它挂写句柄,会话因此落盘、能在会话列表里看到(拿不到agents服务时退回ctx.sessions.create的老行为:会话只活在内存里,列表里看不到);0.19.3 起建会话时一并带上模型选择(沿用旧会话正在用的provider/model,取不到用宿主默认选择)与旧会话的agentPreset(setup里挂同一套组合)—— 不带模型的话,子会话一跑就是「本轮运行失败」(系统提示里的{{model}}渲染时是空的); -
0.21.1 起不再往新会话写任何消息(用户 2026-10-16:「这个应该改成手动复制,请你把这个开启新会话 自动填充这个的去掉」):建出来的就是空白会话。原先那一步(append 首条
user/message,文字来自rotationSeedMessage模板,也曾是「把旧会话 id 送进上下文」的唯一落点)已删除;那段开场话术现在只从状态条「复制话术」按钮或设置页话术卡手动复制; -
把
childId/message写进旧会话的轮换记录(wire v10;0.21.1 起记录里不再有seedOk),再由客户端切过去; -
0.19.1 起补最后一步:把它挂进工作区。 左侧会话列表是按各工作区的
sessionIds列的,落盘不等于列出来 —— 于是建完再workspaceRegistry.resolveByPath(cwd)找目标工作区、attachSession(childId)(宿主自己的新建 / 分叉路径同样这么做)。挂载结果(attached/no-workspace/attach-failed…)进rotation-trace.jsonl;挂不上只少一个列表项,落盘与播种不受影响。
为什么废掉 fork:sessions.fork 会把整份旧事件日志复制进新会话 —— 上下文积压正是要解决的问题,
fork 等于把它原样搬过去。方案 B 只把一个 id 送过去,需要细节时让模型用 @<旧会话id> 查。
客户端那一半只做一件事:切到新会话。0.19.4 起走 uiWorkspace.openSession(childId) —— 本机客户端运行时(0.2.0-rc.2)的 sessions 服务面里没有 open(只有 retain / using / retainInfo / refreshProjections / search / fork / scope / binding),旧的 sessions.open 只作回落。切过去要求该 id 已在客户端会话列表里
(宿主建完会话会推 host/session-added 帧),而帧到达与投影更新之间有竞态 ⇒ 有界重试(0.19.3 起 40 次 × 500 ms ≈ 20 秒);
仍失败就把那一行写成「新会话已就绪 · 点这里重试」,那一行可点(客户端没有 toast API,提示只能落在面板/状态条)。
rotationArchiveOld 默认 false:开场话术里的 @<旧id> 要靠旧会话留在列表投影里才解析得到,归档后 @ 列表里就看不到它了。
0.20.0 起子会话的名字认得出是谁开的:宿主建好子会话后,读旧会话的持久化标题(ctx.sessionTitle.get(sessions.get(旧 id))),按官方 sessions.fork({ increaseTitle: true }) 的同一套约定递增(尾部 (N) / (N) 保留括号样式递增,其余追加 (1)),再用 ctx.sessionTitle.rename(子会话, 名字) 写回 —— 写进去的是 source: { kind: 'user' } 的 session/title 事件,会把标题钉住,session-title 的回退生成不再覆盖它。开关是 rotationIncreaseTitle(默认 true,与 0.8.0 同名同义);服务缺席 / 没有标题 / 改名失败一律只降级,结果进 rotation-trace.jsonl 的 titleOk / title / titleReason(不改 wire)。
0.9.1 起默认就是全自动(rotation: 'auto');想改成「先给我看一行、点了才切」就把 rotation 设成 'manual'。判据、四条硬约束与全部 API 证据见 docs/design-rotation.md。
0.9.3 修好了「自动会切」这件事本身(0.9.0–0.9.2 只写在文档里):那两版把 auto 的触发点写在详情面板的一个分区里,
而详情面板默认是收起的(collapsed 初值 true,收起时直接 return null)⇒ 实际一次都没自动切过。
现在触发点挂在常驻的状态条上(React.useEffect,渲染返回 null 时也照样执行)。同一处还修了两个连带问题:
轮换结果落地后会广播一次通知重渲染(此前结果存在单槽位里,面板会一直停在「正在轮换…」);
ready / failed 那一行可点重试,且 wire 换了 childId / at 就作废本地结果(此前一次失败 = 该会话永久不再尝试)。
0.9.2 起不再要求「空闲」才建会话(rotationRequireIdle: false),水位账也从「进程内 Map」改成写进投影 state 的 ledger(跨重启保留,不进 wire)—— 0.9.0/0.9.1 实测轮换全史 0 次的两条原因(重启归零:1.66 MB vs 阈值 1.80 MB;从不空闲:4.32 MB 却一直在同一回合)就在这里。auto 模式下客户端会等会话空闲再切,未空闲时那一行显示「新会话已就绪 · 等当前回合结束再切」。 0.20.0 起那行不再只能干等:点击它 = 立刻切过去(不必等这一轮跑完),并且等待态会多出一行「切过去并归档当前会话」(performRotation(snapshot, { force: true, archiveOld: true }))。 0.20.0 起默认只提醒:判据照旧认「到量」,但宿主什么都不建 —— 只写一条 remind 记录,界面提示「建议开新会话」(开场话术在设置页可复制);要恢复自动建会话把 rotationCreate 设成 true(0.21.1 起它建出来的也是空白会话:话术要你自己粘)。
0.9.3 收紧了账里装什么:只装只增不减的水位(payloadBytes / headerBytes / payloadSeq / laneWatermarks)与一个终态事实 rotationChildId,handoff / rotation 的 status 一律不进账。0.9.2 把「正在导出(120 s 窗口)」「找不到 bili / 没有 cwd」这类临态也写了进去,而宿主对这两种状态直接 return ⇒ 一次异常就被跨进程永久化,该会话此后再也不导出、也不轮换(这是 0.9.2 的回归;旧的持久化 ledger 仍被容忍,只读新字段)。
新会话话术卡(0.18.0,0.18.2 并入设置页)
轮到换一具身体时(到量轮换,见上一节)新会话是空的:0.21.1 起宿主不再往它写任何消息(0.20.0 之前会 append 一条 rotationSeedMessage 渲染出的开场话术,于是「每次开新会话都冒出同一段」)。
开场话术改成手动取:状态条上的「复制话术」按钮,或「新会话」页那段可复制的文本 —— 粘进新会话,不必再让它从零猜「接着做哪件事」。
- 话术由模板拼出:
{old}换成旧会话 id、{cwd}换成工作目录。默认模板 = 「继续旧会话 {old}(工作目录 {cwd})。需要它的上下文时用 @{old} 查阅,不要假设我记得;先跟我确认要接着做的那件事。」 - 页面内容:轮换分区 + 开场话术(
<pre>,可直接选中)+ 一个复制按钮(复制成功显示「已复制」, 拿不到剪贴板或写入被拒就说「复制失败,请手动选中」)+ 工作目录一行 + 一行提示「在新会话里粘贴这段文本」。 - 它长在哪:本插件设置页的最下面(
dsh-status-turbo.settings,紧挨配置表单)。0.18.2 把 0.18.0 的左侧栏入口整条撤掉 —— 一页顶两页,也没有侧栏按钮要跟着 id 同步。 - 取数走标准钩子:设置页是 root 作用域、没有会话绑定,用
useSessions(state => state.current)取当前会话, 再读state.byId[id]?.projectionValues?.turbo—— 宿主已经把每个会话的投影值放进会话列表, 没有新订阅、没有 HTTP 路由、没有宿主 RPC。 - 三态分得清:没有当前会话说「先在左侧选一个会话」;投影还没到说「等待该会话的投影」;
宿主没报出话术文本时只给提示行,不给
pre与复制按钮。不把「还没到」说成「拿不到」。 - 它不发起轮换:自动轮换仍由宿主半完成(建一个空白新会话,0.21.1 起不再写任何消息),这个页面只把话术摆出来给你复制。
- 0.21.0:这一页说人话,状态条能「复制这一段」。设置页里每个字段名与分组名都有中文描述(不再显示
rotationCreate这种原始键名 —— 词典缺条目时会先查DICT.zh再退兜底,不再直接吐键名)。状态条右侧多一个复制按钮,复制四行:会话名称:…/会话 ID:…/会话日志目录:…/继续处理未完成的任务;某一项拿不到就整行省掉(不留空占位),按钮本身始终在(最差也能复制到最后那句「继续处理未完成的任务」)。会话名称取自宿主的session-title,日志目录按<DSH_HOME>/sessions/<工作目录键>/<会话 id>拼出(没有工作目录就给空串,不猜路径)——两者都是 0.21.0 新进 wire 的字段。 - 0.21.1:不再自动写进新会话。宿主半删掉了「建完会话再
append首条user/message」那一步(seedQuietly/seedMessage两个函数一并删除),rotationCreate: true建出来的也只是空白会话;rotationSeedMessage仍然生效 —— 它现在只负责渲染给人复制的那段文本。连带收紧:轮换记录不再带seedOk,客户端不再渲染「种子消息已写入 / 失败」那行小字。 - wire 上是顶层
newSession({ text, oldId, cwd };v13 的 37 键里它排在最后)。
卡死自愈(D)—— 只做到 warn 层
stuckAction 默认是 warn,零副作用(只记一条故障,别的什么都不做)。nudge 和 stop 必须显式配置才会发生;
而 stop 还额外要求宿主的 workspace/session-activity 报告会话仍在活动 —— 否则它会顺手杀掉整夜构建这类长任务。
已知缺口: stuckRounds 目前没有消费者 —— 宿主半只能看见判据的一半(连续同一聚合键达到 stuckRepeats),
所以 D 不是完整实现。
本轮没做:B2
长命令后台化(10 秒内返回 job id 而不是阻塞对话)本轮未实现。集成目标在本机不存在(没有
dsh-plugin-background-tasks,且 PreToolDecision 没有替换结果的分支),也没有受支持的钩子可用。理由与证据、
以及三条接续排查点见 docs/plan-B2.md。请不要去找并不存在的 B2 用法。
配置
| 键 | 默认 | 说明 |
|---|---|---|
statusBar | true | 状态条挂载开关。宿主把它投影成 wire 字段 mountStatus('enabled' / 'disabled');客户端有三态:enabled → 正常显示;disabled → 完全不渲染;字段缺失(老宿主不下发这个字段)→ 显示红格「状态条开关缺失:配置未送达」,而不是静默什么都不做。关掉的含义是「不显示状态条」,不是一个悄悄失效的开关 |
noProgressMs | 90000 | 无进展判定阈值(毫秒)。超过则状态条转黄并显示卡了多久 |
downgrade | true | 见上一节 |
downgradeFloor | low | 见上一节 |
minQuietSteps | 2 | 见上一节 |
spillThresholdBytes | 32768 | B3:结果正文达到这么多 UTF-8 字节才考虑落盘(与宿主 tool-result-pruner 是 AND 关系) |
localTrimHeadChars | 4096 | 0.6.5:拿不到宿主 pruner 时本地裁剪保留的头部字符数(外层 spillThresholdBytes 决定「要不要落盘」,这三个键决定「给模型看多少」) |
localTrimTailChars | 1024 | 0.6.5:本地裁剪保留的尾部字符数;被省掉的中间段替换成一行「此处省略 n 个字符」 |
localTrimMinChars | 8192 | 0.6.5:最长文本块短于它就不裁(与 spillThresholdBytes 无关,单位是字符);裁剪结果不短于原文时同样不裁 |
bulkInjectionBytes | 32768 | B4:单条 tool/result 载荷达到这个字节数即算一次「大块注入」,会作废缓存前缀 |
evidenceWhitelist | 3 条正则源 | C1:3 条完整正则字面量(含定界斜杠与 flag)的字符串形式;命中任一条即视为有可核对证据 |
stuckAction | warn | D:warn(默认,零副作用)/ nudge / stop |
stuckRounds | 3 | D:连续多少个 turn/end 没出现新工具名。目前没有消费者(已知缺口) |
stuckRepeats | 3 | D:同一聚合键重复多少次算「卡住」 |
mojibakeMinRuns | 2 | C3-a:连续 U+FFFD 的最长连段达到它才提醒编码损坏 |
activityLimit | 24 | 0.6.0:活动流环缓冲最多保留几条(超过从头部丢) |
activityBytes | 8192 | 0.6.0:活动流环缓冲总字节上限(UTF-8,超过从头部丢) |
reasoningChars | 400 | 0.6.0:单条模型思考截断到的字符数 |
resultHeadLines | 6 | 0.6.0:工具结果 / 报错保留的首行数 |
redactKeys | 11 键 | 0.6.0:工具参数脱敏键(大小写不敏感、子串匹配):key / token / secret / password / passwd / pwd / authorization / cookie / dsn / apikey / credential |
subagentWaitTools | ['wait_agent'] | 0.6.1:等待类工具名(等子代理)。命中者不参与「最慢工具」,而是单列进等待态 |
humanWaitTools | ['ask_user_question'] | 0.6.1:等待类工具名(等用户回答),归类为 human |
jobWaitTools | ['job_output','job_wait'] | 0.6.3:等待类工具名(等后台任务)—— 第 4 类。**只有参数带 wait: true(真的在阻塞等待)**才归 job;不带 wait 的快照算真执行 |
slowMs | 60000 | 0.6.3:慢调用阈值(毫秒)。maxMs 达到它的工具在 wire 上带 slow: true,客户端加 ⚠ 并把超时命令指向后台任务插件 |
改配置后需要重启 DSH 生效。
手工验收(§6)
装好后在界面上按下面几条走一遍:
| # | 步骤 | 期望 |
|---|---|---|
| 1 | 让模型跑一条 ping -n 120 127.0.0.1(约 2 分钟) | 1 秒内出现状态条,阶段 = 执行工具(bash),耗时从 0 开始每秒跳 |
| 2 | 什么都不做,盯着看 2 分钟 | 耗时正常推进;日志没有任何新事件也不会冻住 |
| 3 | 等命令结束 | 阶段切回 请求模型,随后这一格切成空闲摘要「上回合 …」—— 它不会消失 |
| 4 | 关掉 statusBar 重启 | 状态条完全不出现(宿主半仍在折叠状态,成本极低)。但若字段 mountStatus 缺失(即老宿主半),这一格会显示红字「状态条开关缺失:配置未送达」;那个红格是故障信号,不是「关掉生效了」 |
| 5 | 让一次性工具输出超过 spillThresholdBytes(32 KiB) | 正文被瘦身,并带上含可读绝对路径(落盘 locator)的通知 |
| 6 | 让模型没跑测试却宣称"测试通过";或让它用 offset/limit 读一个文件 | 同一次工具返回里就出现校正提示 —— 不新增往返,会话步数不变 |
| 7 | 跑一条会产出 GBK 乱码的命令,或被噪声淹没的命令 | 同一次返回里出现「编码损坏」/「噪声」提示 |
| 8 | 造一个静默 2 分钟的步骤 | 到 90s(noProgressMs)时无进展那一格变黄并往上计数 |
| 9 | 会话中途注入记忆 / 触发压缩 | 5 分钟内出现 前缀已失效 胶囊,原因 = 压缩开始 / 压缩结束 / 大块结果注入 |
| 10 | 把 conversation.input.dock 的声明去掉(或换成不存在的槽位名)重启 | 界面不崩;控制台出现 [dsh-status-turbo] 状态条 3 秒内未能挂载到 conversation.input.dock… |
| 11 | 宿主半改成一个不存在的 key 版本(篡改 v) | 状态条位置出现红字「投影版本错配」,而不是静默消失 |
§6 的原始第 2 行(长命令不再阻塞对话,10 s 内返回后台 job id)本轮不适用:B2 未实现,理由与证据见
docs/plan-B2.md。这条是删掉并说明,不是留空洞 —— 所以上表重新编号成连续的 1–11。
开发
node --test "test/*.test.mjs" # 纯函数 + 宿主/客户端契约 + UI 契约 + 钩子
不用开界面也能验收 —— 拿真实的已落盘会话日志复算:
node tools/replay-log.mjs <session.v4.jsonl.zstd> [--peek] [--max N] [--stall-ms N]
它会对同一份日志跑完全相同的折叠与文案,逐行打印「哪个事件让状态条变成了什么」,最后给出最长的一次工具静默窗口 —— 那就是状态条在这段时间里一直在显示的东西。实测本会话:
# jsonl 行数 1182 # 可解析事件 1182(跳过坏行 0)
# 最长工具窗口: pwsh 静默 50.4s(发生在会话 +52m34s,
# 状态条此时应显示「执行工具(pwsh) 50.4s」)
(会话日志在 D:\deepseek-harness\sessions\<workspace-slug>\session-<id>\session.v4.jsonl.zstd;它是多帧 zstd,工具内部按 magic 28 B5 2F FD 切帧后逐帧解压。)
- 没有构建步骤:
lib/即源码即产物,改完直接重启 DSH 即可。 - 不依赖 zod:投影 schema 用直通 stub(依据见
docs/design.md§2.3)。 - 目录:
lib/state.js—— 纯折叠函数(不碰任何服务、不碰Date.now()、无第三方依赖;仅一处 Node 内建 import:node:crypto,用于 A2 的工具参数指纹)lib/index.js—— 宿主半(注册turbowire 投影 + B1 按步降档 + B3/C/D 的 post-execute 钩子)lib/gears.js—— B1 的纯判据(零 import,只吃工具轨迹)lib/spill.js—— B3:把超大结果交给宿主的 spill 后端,并渲染 locator 通知lib/notes.js—— C1/C2/C3 的提示文案与判据,外加详情面板要用的工具聚合lib/handoff.js—— 0.7.0 交接导出:阈值/节流/新鲜度判据、bili 命令探测(显式配置 → 包内 entry → profile 兜底)、导出 argvlib/rotation.js—— 到量轮换的纯判据shouldRotate(与交接导出同源阈值;handoffExport*键名下)lib/rotation-host.js—— 0.9.0 方案 B 的宿主动作:建空白新会话(0.21.1 起不再写消息)+ 按源标题补名 + 记结果,绝不抛lib/client.js—— 客户端半,往conversation.input.dock挂摘要格、往conversation.input.dock挂详情面板,并把「新会话」话术卡追加在设置页最下面(0.18.2 起左侧栏入口已撤)tools/replay-log.mjs—— 用真实日志离线复算状态条test/hooks.test.mjs+test/ui-contract.test.mjs—— 宿主钩子与 UI 契约的门禁test/panel.test.mjs—— 「新会话」话术卡的门禁:开场话术渲染、复制按钮与两种结果文案、三态兜底、设置页同时挂配置表单与话术卡docs/design.md—— 设计与实读依据(每个接口都有文件:行)docs/plan-B2.md—— 本轮为什么不实现 B2
设计约束(不可违反)
- 只读已 commit 的会话日志(投影)。不读宿主内存、不轮询、不注入 DOM、不爬 React fiber。
- 不发网络请求、不注册路由。「不落盘」要写准: 插件自己只在两处碰写,都不改会话日志、不改用户设置 ——
① B3 把超大结果委托宿主既有的 spill 后端落盘,插件只把拿回来的绝对路径印进正文;
② 0.7.0 交接导出前建目录、并写一个
.gitignore(lib/index.js)。 除此之外lib/不写盘。 - 不改
agent-default-model、不改 compaction 配置、不改任何用户设置。 - 不改写工具参数、不拦截工具。
- 任何失败都必须显示到界面上(或至少
console.error点名原因),不允许静默 return。
许可
MIT,见 LICENSE。第三方说明见 THIRD_PARTY_NOTICES.md。
Related plugins
dsh-web (dsh-task-board)
zhu1090093659/dsh-web
dsh-web (dsh-web-all)
zhu1090093659/dsh-web
dsh-web-ui (dsh-task-board)
zhu1090093659/dsh-web-ui
dsh-web-ui (dsh-web-ui-all)
zhu1090093659/dsh-web-ui