dsh-eta-forecast

uitested: passnpmMIT

Adds an ETA widget to the DSH web and desktop UI that estimates when the current turn will finish from the todo list and step pace, and shows the remaining time, a progress bar and a confidence badge.

下载量 / Downloads
1,065
依赖体积 / node_modules
0.23 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

Install

dsh plugin --profile web add dsh-eta-forecast

Sandbox test results

StageResult
1 install✓
2 list✓
3 mount✓
4 boot—

Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).

README

dsh-eta-forecast

给 DeepSeek Harness 的 Web / 桌面界面加一个「这一轮大概几点做完」的小挂件。

它不是按字数、按步数拍脑袋外推的。真正被当成进度分母的是 todo 清单——

模型被要求「开工前先列清单、做完就勾」;步数只用来估「一条 todo 平均要几步」,

是节奏修正项,不是进度本身。

它显示什么

位置 · 内容

左上胶囊 · 多会话时的会话切换(只有一条会话时不显示)

大字 · 预计完成时刻(本地时钟,如 14:32)

副行 · 还剩多久 + 已经跑了多久

进度条 · 清单完成度(in_progress 按半条计)

左下 · 3/7 项,没有清单时退化为「第 N 步」

右下徽章 · 置信度:样本不足 / 粗略 / 中等 / 较准

第三行 · 正在做的那一条(正在做:…)

超时时 · 大字换成已跑时长,副行「已超时 N 分」—— 不再演倒计时

超过预测之后(超时追踪)

预测总有失手的时候。之前跑过预测点之后,倒计时只会逼近 0 然后卡住 ——

「还有 1 秒」挂好几分钟,用户不知道这轮已经超时了。现在:

  • 跑过预测终点(+ 90 秒宽容期)还在跑 → 大字换成已跑时长,副行「已超时 N 分」。
  • 但只有实时估计也认同「就快完了」时才喊。 如果越过截止点之后实时估计算出的

总时长仍然是截止点的两倍以上,说明这轮只是比 6 样本时的预测大、活儿还在正常推进 ——

这时不喊超时,让倒计时继续站岗。判据与实测见下方「第三轮改造」。

  • 超时后仍然给重估时间:「重估还要 M 分(HH:MM)」—— 按现有样本重新算的。

这一条是实测补上的:真实一轮跑了 97 分钟,旧版从第 11 分钟起就只喊「已超时」,

连续 88 分钟没给任何时间信息。现在同一轮全程都有可读的重估。

  • 超时锚点用「成熟预测」(样本 ≥6 时给出的第一次预测),不用最早那次。

首预测只有 2~3 个样本,最乐观也最不准;拿它当锚点会在第 11 分钟就误报超时,

而同一时刻细化后的估计还说「还有 838 秒」。改用成熟锚点后,同一轮的超时

首次出现从 11 分钟推迟到 32 分钟。

  • 超时会学进下一轮。 每次完整跑完的轮次若超出预测,记下「超出比例」;

之后这个会话的估计按最近超时记录的中位比例放大(封顶 1.6×)。

被中断/出错的轮次不算(锚点不可信),准时完成的也不记(不污染)。

估算准确性

第一轮改造(真机上那轮 97.2 分钟、22 步、没有 todo 清单的回放)

指标 · 改前 · 改后

超时首次出现 · 第 11.3 分钟 · 第 31.9 分钟

超时后「无任何时间信息」的时刻 · 11 / 15 · 0 / 15

末期误差(第 1 轮) · +23% · +15%

早期误差(第 2 轮 1.7 分钟处) · −86% · −60%

平均绝对误差(第 2 轮) · 62% · 42%

1. 整轮时长先验不再排在最后。 没有 todo 清单时,「这类一轮通常跑多久」

比「还剩几步 × 中位步时」稳得多 —— 步时受模型思考时长影响,实测一轮内中位步时

从 22 秒漂到 277 秒。以前它只在其它估计全空时兜底,等于把最好的先验排在最后。

现在它与步数证据一起加权(≥2 轮给 0.45,只有 1 轮给 0.25)。

单样本只用于修正已有估计,绝不由它单独给出数字 —— 那不是估计,是把上一轮硬套过来。

2. 平滑改为不对称:估短时跟得快(α=0.55),估长时跟得慢(α=0.22)。

用户对「说还要 20 分结果 5 分就好了」的容忍度,远高于「说还要 3 分又等了 20 分」。

3. 精度可度量。 每次完整跑完的轮次记一条「预测 vs 实际」,快照里给出:

  • accuracy.mape:最近几轮的中位绝对相对误差(0.18 = 典型偏 18%)
  • accuracy.bias:中位带符号误差(正 = 习惯估长,负 = 习惯估短)

卡片上表现为副行末尾的「近几轮偏差约 18%」,置信度徽章的 tooltip 里还带偏向。

样本不足 2 轮时一律不显示 —— 不编数字。

第二轮改造:早期上界(把「刚跑 40 秒就报还有 6 分钟」压回去)

第一轮改造之后,把本机全部真实会话日志都回放了一遍,才发现坏名声几乎全来自

一个地方:轮次早期的高估。一小时以上的轮次其实早就偏低了,问题只在开头。

分桶(第一轮改造后) · MAE / 偏置

1–2 分钟 · 112% / +111%

2–5 分钟 · 53% / +31%

5–15 分钟 · 57% / +20%

15–30 分钟 · 44% / −26%

30–60 分钟 · 50% / −18%

60 分钟以上 · 103% / +12%

长轮早就偏低了,所以修法不是「统统砍掉」,而是

「已经跑了 T 秒,这一轮最多还能再跑 k(T)·T + 120 秒」,其中
k(T) = 2.42 + 1.98·log10(1 + T/120) —— 随 T 对数放松
(40 秒时 k≈2.7、10 分钟时 k≈4.0、一小时时 k≈5.4、两小时时 k≈6.0,
折算成「这一轮最多总长」分别是 3.8 / 42 / 324 / 717 分钟,长轮基本不再受约束)。

关键在于这条上界只作用于显示层。如果把它夹在平滑之前,超时锚点也会跟着变小,

实测会发现超时率明显升高 —— 更多轮次退化成「已超时、不给任何时间」,

那正是第一轮改造刚刚修好的、让挂件最没用的形态。放在 forecastSession 返回之前时,

超时判定与精度记录全都看未夹的估计,只有用户看到的那个数字被约束。

因此超时率一位小数都没变(同一批观测点上逐点完全一致)。

回放结果(同一批日志、同一批观测点,逐点对比):

分组 · MAE · 偏置 · 灾难率

全部 · 61% → 52% · +12% → +8% · 5% → 0%

2 分钟以上 · 56% → 47% · +2% → −1% · 5% → 0%

5–30 分钟 · 51% → 45% · −1% → −3% · 6% → 0%

30 分钟以上 · 64% → 50% · −10% → −17% · 4% → 0%

60 分钟以上 · 103% → 76% · +12% → −8% · 3% → 0%

「灾难率」= 非超时态下「显示还剩 ≤30 秒、但实际还剩 2 分钟以上」的观测点占比。

超时态不计入,因为那时卡片显示的是「已超时 M 分」,倒计时数字根本不在屏幕上 ——

把它算进去会得到一个漂亮但没意义的数字。这个指标比 MAE 更贴近体感:

它就是「这破倒计时到底准不准」和「嗯,差不多就是这么回事」之间的区别。

顺带一个反直觉的结论:夹太紧反而更差。上界前面那个 ×1.15 不是随手写的,

把这条缓冲扫了一遍(同一批 2118 个观测点):

缓冲倍数 · MAE · 偏置 · 灾难率

1.00(直接把上界当结果) · 49% · −3% · 5%

1.15(采用) · 52% · +8% · 0%

1.30 · 56% · +18% · 0%

1.60 · 67% · +36% · 0%

1.00 的 MAE 确实更低,但它把误差整体压到偏低一侧,并且灾难率又回到 5% ——

也就是每二十个观测点就有一个「说还剩不到半分钟、实际还有好几分钟」。

这正是第一轮改造花力气消掉的那个形态,所以宁可多 3 个百分点 MAE 换回 0%。

再往右则纯亏:1.30 起 MAE 单调变差,灾难率也不会更低。

口径、分桶、灾难率的定义全部写死在回放脚本里,任何人都能在自己机器上复现:

node test/replay-all.mjs                        # 跑本机 ~/.dsh/sessions 的全部日志
node test/replay-all.mjs --before HEAD          # 与任意 git 版本逐点对比
node test/replay-all.mjs --variant path/to/x.mjs # 并排第三组(调参时必须并排看)

--variant 存在的意义:调超时/夹取这类参数时,改文件→跑→改回来这套流程里

任何一次忘记还原都会污染结论(本轮就被坑过一次,得拿 git show HEAD: 当基线重跑)。

给它一个实验副本路径,脚本会把它复制到临时目录再 import,不动工作树。

test/replay-accuracy.mjs 保留着,但它只回放单独一轮,适合讲故事、

不适合支撑「准确率提高了几个百分点」这种结论 —— 换一组参数就能翻盘。

第三轮改造:超时抑制(把「明明还在干活却一直喊已超时」压回去)

前两轮把全部 MAE 从 100% 压到五成上下,但用户的实际抱怨换了个方向:「经常超时」。

把所有真实日志按「超时态占多少墙钟时间」重新量一遍,问题很具体 ——

超时态占了 60% 的墙钟,最长的一轮连续喊了 195 分钟「已超时」。

根因是超时锚点(maturePredictedMs)的语义:它在样本刚够 6 个时冻结,

之后永不更新 —— 不冻结就永远判不出超时,所以这个冻结本身是对的。错的是

拿它当无条件判据:一轮跑到第 195 分钟,早就不该再拿「第 6 个样本时的预测」说事。

修法不是改锚点,而是加一条克制:

越过承诺截止点之后,如果实时估计的总时长仍然远超截止点(> 截止点 × 2),
说明活儿还在正常推进、只是比 6 样本时的预测大 —— 这时不喊超时,
让倒计时继续站岗;只有实时估计也认同「就快做完了」时才切到超时态。

试过的错误路线先记一笔:把锚点做成棘轮(估计总时长超过锚点 1.5 倍就把锚点

抬到新估计)能把超时率压到 14%,但真超时也几乎不再触发 —— 棘轮每次把锚点

追到当前总时长,等于永远差一点、永远判不出超时。过度校正,放弃。

实测(同一批 28 会话 / 196 轮 / 2301 个观测点):

指标 · 改前 · 改后

超时态占墙钟 · 60% · 8%

最长连续超时 · 195 分钟 · 22 分钟

有超时段的轮次 · 45% · 31%

真超时覆盖(实际还剩 >5 分时被标超时) · 63% · 8%

「离谱」占比(超时里卡片自己还说剩余 >2×真实且 >5 分) · 13% · 4%

MAE / 偏置 / ±2 倍 / 灾难率 · 51% / +5% / 92% / 0% · 一位都没动

这个判据扫过一遍候选值(同一批观测点):

抑制倍数 · 超时率 · 最长连续 · 真超时覆盖 · 离谱占比

1.5 · 2% · 10.6 分钟 · 2% · 6%

2.0(采用) · 8% · 22.1 分钟 · 8% · 4%

3.0 · 18% · 45.7 分钟 · 19% · 4%

不抑制(1e9) · 60% · 195.2 分钟 · 63% · 13%

1.5 会把真超时也一起压没了(覆盖率只剩 2%,等于这个功能名存实亡);3.0 又回到

「动不动就喊超时」。2.0 是唯一同时把「最长连续」压到二十分钟级、又保住 8% 覆盖的档位。

代价写在明面上:抑制是有意识的少报。真正跑飞的轮次,卡片可能到很晚才承认

超时,期间一直显示一个偏大的倒计时。这个取舍是照着用户的真实抱怨选的 ——

「说还有 20 分结果 5 分钟就好了」的容忍度,远高于「一直在喊超时、可它明明还在干活」。

外观(缩放 / 壁纸)点标题行的 ⚙ 调,见下方「外观设置」。

版本号

设置面板标题旁有一枚版本徽章(如 1.0.0)。它由宿主在下发客户端脚本时

从 package.json 注入(__DSH_ETA_VERSION__ 占位符替换)—— 浏览器里没有

文件系统,客户端自己读不到版本。state.json 也带 version 字段,

排查问题时和面板显示对得上。发新版本只改 package.json 的 version,

其余自动跟随。

三个刻意的设计选择

1. 等你的时间不算工期。 一轮里 ask_user_question 与 approval/asked

期间整个回合是挂起的。如果把这段算进「已用时长」,一次授权等待就能把 ETA

推到几小时之后,而且再也收不回来。所以挂起时长从 elapsed 里扣掉,挂起时

挂件上会显示 ⏸ 等你操作,这段时间不计入工期。

2. 没样本就说没样本。 一轮刚开始、清单一条都没勾时,任何数字都是编的。

这时大字显示 `

…(truncated)

Rendered from the README published on npm. Formatted only, not edited.

Versions

Published on npm::5,最后更新 2026-10-28

Links

GitHub · Homepage · npm

Same category