Skip to main content
D

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-turbo

README

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 起状态条不再重复它)。

键默认说明
downgradetrue总开关。只有显式写 false 才关闭(关掉后连 llm 服务都不依赖)
downgradeFloorlow最多降到哪一档。按该模型自己公布的有序档位表取下标比较;模型没公布这一档就退化为一档一档降
minQuietSteps2连续几个"干净只读步"才降档。设 1 更激进,也更容易误降
readOnlyTools10 个工具名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 逐字镜像)

键默认含义
handoffExporttrue总开关。默认开(装上就工作);显式写 false 时它真的什么都不做(不探测、不起进程、不建目录)
handoffExportTokens240000本步输入 token 达到它才考虑导出(0.8.1 从 400000 降 —— 本机实测体感卡点是 341421)
handoffExportMinDeltaTokens60000距上次导出又长了这么多才再导(0.8.1 从 100000 降)
handoffExportDir'handoff'导出目录(相对会话 cwd)
handoffExportFullfalse是否带 --full(附每个块的原始消息;文档会大很多)
handoffExportBiliBin''bili 可执行路径;空串表示走上面的两级探测
handoffExportTimeoutMs1200000.9.2(P1-4):单次 bili export(以及车道列表读取)的硬超时(毫秒)。超时路径先 kill,两个管道 destroy 之后宽限期到了再升级 SIGKILL(lib/handoff.js · runBili);两个调用点(runExport / listLanes)共用这一个键,非法值回落 120000
handoffExportWindowPercent25阈值 = 窗口的百分之多少(窗口已知时)。token 与字节两个口径共用这一个旋钮(0.8.1 从 40 降:旧值 40%×1M 恰好等于旧 token 旋钮 ⇒ 不 binding)
handoffExportMaxPerProcess64本进程最多起多少次导出;用满会记 failed + 一条 warn
handoffExportSubLanesfalse是否连该会话的子车道(|<sub:…>)一起导。0.7.8 默认关(一次越桶会按车道数乘一份)
handoffExportBytes1800000字节口径:模型可见体量达到它就触发(与 token 口径取或);0.8.1 从 3000000 降 = 240000 × 7.5
handoffExportMinDeltaBytes450000字节口径的节流(0.8.1 从 8000000 降 —— 旧值大于阈值,字节口径的第二次导出是条死路)
handoffExportFreshMs300000只对新鲜事件触发:折到的事件距现在超过这么多毫秒就不动作(挡冷启动重放的批量触发)
handoffExportRefreshBytes0同一车道是否允许重复导出。0 = 每车道最多 1 份(0.7.8 默认);设成字节增量才恢复「随水位桶刷新」
rotation'auto'到量轮换模式:off / manual(面板一行 · 可点)/ auto。默认 auto(0.9.1 起)
rotationCooldownMs600000两次轮换的最小间隔(进程级生效,防连环轮换)
rotationOpenChildtrue0.9.0:宿主建好新会话后是否自动切过去(auto 模式下即自动切)
rotationFreshMs3000000.8.1:只对新鲜事件触发轮换判据(挡冷启动重放,同 handoffExportFreshMs)
rotationRequireIdlefalse0.9.2:是否要求「当前没有开放轮次」才轮换。默认 false —— 建一个空白新会话不打断正在跑的回合;设 true 回到 0.8.0 的老行为(实测后果:从不空闲的会话一个也轮换不了)
rotationArchiveOldfalse切换成功后是否归档旧会话(0.8.1 定为 false:归档后侧边栏与 @ 引用读同一份列表投影 ⇒ @{old} 指引会失效;界面里没有取消归档入口)
rotationIncreaseTitletrue轮换建出的子会话是否按旧会话标题做 (N) 递增改名(0.20.0 起复活:0.9.0 起是空白新建,子会话没有标题事件 —— 0.20.0 之前回退规则只能拿首条种子模板截断 ⇒ 每一代同名,0.21.1 起连那条首条消息也没有了,自动命名更加认不出亲;宿主现在自己成了消费者,命名与官方 sessions.fork({ increaseTitle: true }) 同一套约定)
rotationCreatefalse到量时是否真的建新会话(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):

  1. 宿主建一个空白会话(新会话的 header 带 cwd 与 parentSession,但不继承旧历史)。0.19.0 起走 ctx.agents.create({ sessionId, meta }) —— 在代理生命周期里发布,持久化层才会给它挂写句柄,会话因此落盘、能在会话列表里看到(拿不到 agents 服务时退回 ctx.sessions.create 的老行为:会话只活在内存里,列表里看不到);0.19.3 起建会话时一并带上模型选择(沿用旧会话正在用的 provider / model,取不到用宿主默认选择)与旧会话的 agentPreset(setup 里挂同一套组合)—— 不带模型的话,子会话一跑就是「本轮运行失败」(系统提示里的 {{model}} 渲染时是空的);

  2. 0.21.1 起不再往新会话写任何消息(用户 2026-10-16:「这个应该改成手动复制,请你把这个开启新会话 自动填充这个的去掉」):建出来的就是空白会话。原先那一步(append 首条 user/message,文字来自 rotationSeedMessage 模板,也曾是「把旧会话 id 送进上下文」的唯一落点)已删除;那段开场话术现在只从状态条「复制话术」按钮或设置页话术卡手动复制;

  3. 把 childId / message 写进旧会话的轮换记录(wire v10;0.21.1 起记录里不再有 seedOk),再由客户端切过去;

  4. 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 用法。


配置

键默认说明
statusBartrue状态条挂载开关。宿主把它投影成 wire 字段 mountStatus('enabled' / 'disabled');客户端有三态:enabled → 正常显示;disabled → 完全不渲染;字段缺失(老宿主不下发这个字段)→ 显示红格「状态条开关缺失:配置未送达」,而不是静默什么都不做。关掉的含义是「不显示状态条」,不是一个悄悄失效的开关
noProgressMs90000无进展判定阈值(毫秒)。超过则状态条转黄并显示卡了多久
downgradetrue见上一节
downgradeFloorlow见上一节
minQuietSteps2见上一节
spillThresholdBytes32768B3:结果正文达到这么多 UTF-8 字节才考虑落盘(与宿主 tool-result-pruner 是 AND 关系)
localTrimHeadChars40960.6.5:拿不到宿主 pruner 时本地裁剪保留的头部字符数(外层 spillThresholdBytes 决定「要不要落盘」,这三个键决定「给模型看多少」)
localTrimTailChars10240.6.5:本地裁剪保留的尾部字符数;被省掉的中间段替换成一行「此处省略 n 个字符」
localTrimMinChars81920.6.5:最长文本块短于它就不裁(与 spillThresholdBytes 无关,单位是字符);裁剪结果不短于原文时同样不裁
bulkInjectionBytes32768B4:单条 tool/result 载荷达到这个字节数即算一次「大块注入」,会作废缓存前缀
evidenceWhitelist3 条正则源C1:3 条完整正则字面量(含定界斜杠与 flag)的字符串形式;命中任一条即视为有可核对证据
stuckActionwarnD:warn(默认,零副作用)/ nudge / stop
stuckRounds3D:连续多少个 turn/end 没出现新工具名。目前没有消费者(已知缺口)
stuckRepeats3D:同一聚合键重复多少次算「卡住」
mojibakeMinRuns2C3-a:连续 U+FFFD 的最长连段达到它才提醒编码损坏
activityLimit240.6.0:活动流环缓冲最多保留几条(超过从头部丢)
activityBytes81920.6.0:活动流环缓冲总字节上限(UTF-8,超过从头部丢)
reasoningChars4000.6.0:单条模型思考截断到的字符数
resultHeadLines60.6.0:工具结果 / 报错保留的首行数
redactKeys11 键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 的快照算真执行
slowMs600000.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 —— 宿主半(注册 turbo wire 投影 + 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 兜底)、导出 argv
    • lib/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