dsh-agent-board 1.7.3 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,14 +102,15 @@ dsh plugin --profile web remove dsh-agent-board
102
102
 
103
103
  ### 仪表盘(Token 消耗)
104
104
 
105
- - 仪表盘视图新增「Token 消耗」区:本看板累计**有效 / 缓存读 / 合计**三分量 + 输入 / 输出(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情,title 附「有效 N / 缓存读 M」拆分);大数字为**今日有效消耗**(本地日口径,无日账则显示 0,旁附「含缓存读共 X」小字对照,有效值不可知时以总量近似并标 `~`)、旁附「累计(本看板)」三分量小字,下方「近 7 天」迷你条形按日展示近 7 天**有效消耗**(今天高亮,title 补总量对照,7 天全为 0 时不渲染);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`);区底固定一行口径 caption:**仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话**
105
+ - 仪表盘视图新增「Token 消耗」区:本看板累计**有效 / 缓存读 / 合计**三分量 + 输入 / 输出(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情,title 附「有效 N / 缓存读 M」拆分);大数字为**今日有效消耗**(本地日口径,无日账则显示 0,旁附「含缓存读共 X」小字对照,旧口径日有效值不可知时显示「—」并在 title 说明,**不拿总量冒充有效值**)、旁附「累计(本看板)」三分量小字,下方「近 7 天」迷你条形按日展示近 7 天**有效消耗**(今天高亮,title 补总量对照,旧口径日画矮灰柱并标「旧口径数据(仅总量)」,7 天全为 0 时不渲染);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`);区底固定口径 caption:累计 / Top 8 仅看板派发的 run 消耗,主窗口行并列不混入
106
106
  - 口径要点:**有效消耗 = 输入 + 输出 + 缓存写(不含缓存读)**。实证本板累计 10.3M 里缓存读占 9.7M(94%)、有效消耗仅 605K——总量被缓存读撑高约 17 倍,因此大数字与「近 7 天」一律用有效值,缓存读单列可查(差额一眼可见),避免「仪表盘与真实消耗差太多」
107
107
  - **主数字口径统一**(`task-muwq9u04`):**按模型分布行与任务消耗 Top 8 行的主数字一律为有效消耗**(含缓存读的合计进悬浮 title),与「今日」「近 7 天」同口径——不再出现「今日有效 3.7M / 单任务 21M」这类同区并排的矛盾数字(模型有效分摊由 host 的 `byModelEff` 给出;老 host 缺该字段或老卡无五分量留账时,主数字退化用合计并在 title 标 `~`)
108
- - **统计范围接通 Token 区**(`task-muwc7hjd`):仪表盘顶部的「统计范围」(快捷:今天 / 近 7 天 / 近 30 天 / 全部 + 自定义起止)现在同时作用于 Token 区的**按模型分布 / 任务消耗 Top 8 / 累计三分量**——范围变化即用新范围重拉 `get-tasks`,由 host 按每个 run 的本地日落点(`t.runs[i].at` → `YYYY-MM-DD`,两端闭区间)过滤:范围外 run 不计入 total/byModel/top,`byDay` 同步裁到范围内;范围激活时 Token 区标题带「范围内: …」标记。**今日大数字与「近 7 天」柱子是自身固定口径,不随范围变化**(区底第二行 caption 与范围筛选浮层均写明)。无范围(两端皆空)时聚合结果与旧版逐字一致(parity);无 run 级留账(老卡 / runs 条目未落 usage)退化为按 `updatedAt` 本地日判定,无 `at` 的老 run 只在无范围时计入(宁可漏不错),run 未记模型名时按入选 run 占比摊任务级模型小计(合计与有效同法摊派,ΣbyModel 与入选总量、ΣbyModelEff 与入选有效各自对齐)。**数据源口径修正(`task-muwq9u04`)**:run 记录数组在**任务级 `t.runs[]`**(`t.usage.runs` 只是结算次数计数)——此前误把计数当数组读,范围过滤静默退化成 `updatedAt` 单日近似、Top 8 与无范围逐字相同;现已逐 run 精确裁切,且范围切换后客户端按 `usageSummary` 的 JSON 串比对补一次 notify(选范围即渲染,不等下一次任意刷新)
108
+ - **统计范围接通 Token 区**(`task-muwc7hjd`):仪表盘顶部的「统计范围」(快捷:今天 / 近 7 天 / 近 30 天 / 全部 + 自定义起止)现在同时作用于 Token 区的**按模型分布 / 任务消耗 Top 8 / 累计三分量**——范围变化即用新范围重拉 `get-tasks`,由 host 按每个 run 的本地日落点(`t.runs[i].at` → `YYYY-MM-DD`,两端闭区间)过滤:范围外 run 不计入 total/byModel/top,`byDay` 同步裁到范围内;范围激活时 Token 区标题带「范围内: …」标记。**今日大数字与「近 7 天」柱子是自身固定口径,不随范围变化**(区底第二行 caption 与范围筛选浮层均写明)。无范围(两端皆空)时聚合结果与旧版逐字一致(parity);无 run 级留账(老卡 / runs 条目未落 usage)退化为按 `updatedAt` 本地日判定,无 `at` 的老 run 只在无范围时计入(宁可漏不错),run 未记模型名时按入选 run 占比摊任务级模型小计(合计与有效同法摊派,ΣbyModel 与入选总量、ΣbyModelEff 与入选有效各自对齐)。**数据源口径修正(`task-muwq9u04`)**:run 记录数组在**任务级 `t.runs[]`**(`t.usage.runs` 只是结算次数计数)——此前误把计数当数组读,范围过滤静默退化成 `updatedAt` 单日近似、Top 8 与无范围逐字相同;现已逐 run 精确裁切,且范围切换后客户端按 `usageSummary` 的 JSON 串比对补一次 notify(选范围即渲染,不等下一次任意刷新)。**Token 口径三连修(`task-muwsnyqv`)**:① 范围内 `byDay` 按入选 run 的本地日落点逐日重建(`to` 端闭区间,`from=to=今天` 时今天不丢——修「范围含今天时 total 含今天、byDay 却吞今天」);② `usageSummary` 同时返回**未过滤全量 `byDayFull`** 与范围内 `byDay`,「今日」「近 7 天」改读 `byDayFull` 兑现固定口径承诺(老 host 缺字段退化读 `byDay`);③ **旧口径日(`e=null`:老 number 日账 / 存量兜底,只有含缓存读的总量)不拿总量冒充有效值**——今日大数字显示「—」+ title 说明,近 7 天画矮灰柱 + tooltip 标「旧口径数据(仅总量,含缓存读)」,柱高归一只按有效值(此前 154M 含 97% 缓存读的旧日账会被画成巨柱、把其余日子的有效柱压平)
109
109
  - 数据来源:每次 Worker/Verifier run 结算时读该 run 的 v4 会话日志(`~/.dsh/sessions/*/<runId>/session.v4.jsonl.zstd`),把 `assistant/message` 事件的 `usage`(`inputTokens` / `outputTokens` / `cacheReadTokens` / `cacheWriteTokens` / `totalTokens`,字段形状以真实日志为准)按 zstd 帧逐帧累加到任务 `usage`(含按模型小计、`runs` 计数与 `byDay` 日账——本地日 `YYYY-MM-DD`,一次 run 整笔记在结算日;多轮重跑/驳回重做自动累加);**run 级留账**:本次用量同时原样写回 `t.runs` 对应条目的 `usage`(五分量俱全),因此按天 / 按模型 / 按阶段任何维度都能从 runs 精确重建,口径若要再调整不必回头猜
110
- - `byDay` 为**双指标**形态 `{ t, e }`(`t` = 总量含缓存读,`e` = 有效消耗不含缓存读;一次 run 不跨日拆分)。聚合端兼容老数据:值是裸 number 的历史日账按 `{ t: n, e: null }` 处理——总量照常保留(「近 7 天」不会整段消失),有效值不可知就置 `null` 并由 UI 标 `~` 近似,**绝不把总量冒充有效值**
111
- - `get-tasks` 再现算 board 级 `usageSummary`(总量 / 有效合计 / 按模型(合计 `byModel` + 有效分摊 `byModelEff`,两者同源累加)/ Top8(带有效与缓存读拆分)/ 日账 `byDay`,不落盘额外表;老任务无 `byDay` 时整笔近似归到 `updatedAt` 的本地日,无 `updatedAt` 则不计入任何日)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」;第二参 `range`(可选 `{ from, to }` 本地日)给定即只聚合该范围内的 run(口径见上条)
112
- - 统计口径边界:本区只统计**看板派发的 Worker/Verifier run**,**不含主窗口对话本身**(不越界统计,UI caption 已明示)
110
+ - `byDay` 为**双指标**形态 `{ t, e }`(`t` = 总量含缓存读,`e` = 有效消耗不含缓存读;一次 run 不跨日拆分)。聚合端兼容老数据:值是裸 number 的历史日账按 `{ t: n, e: null }` 处理——总量照常保留(「近 7 天」不会整段消失),有效值不可知就置 `null`;**旧口径日(`e=null`)UI 不拿总量冒充有效值**(`task-muwsnyqv`):今日大数字显「—」、近 7 天画矮灰柱 + tooltip 标「旧口径数据(仅总量)」,总量仅在「含缓存读共 X」小字与悬浮 title 里单列对照
111
+ - `get-tasks` 再现算 board 级 `usageSummary`(总量 / 有效合计 / 按模型(合计 `byModel` + 有效分摊 `byModelEff`,两者同源累加)/ Top8(带有效与缓存读拆分)/ 日账双份(范围内 `byDay` + 未过滤全量 `byDayFull`,后者供「今日」「近 7 天」固定口径读取),不落盘额外表;老任务无 `byDay` 时整笔近似归到 `updatedAt` 的本地日,无 `updatedAt` 则不计入任何日)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」;第二参 `range`(可选 `{ from, to }` 本地日)给定即只聚合该范围内的 run(口径见上条)
112
+ - **主窗口(本会话对话)消耗单列**(`task-muwsol23`):累计行下方新增一行「**主窗口(本会话):今日有效 X · 累计 Y(缓存读 Z)**」,按模型分布尾部追加一条「**主窗口(对话)**」(琥珀色与 run 模型区分,有效为 0 不渲染,固定尾部不参与排序);数据源是该看板所属主会话自己的 v4 日志,host 侧**增量尾读**聚合(缓存键=会话 id:文件不变零读、变大只读新增字节段、变小/轮换全量重读一次,尾部半帧不结算下轮补读——3s 轮询稳态零全量重读),五分量与有效口径(e=输入+输出+缓存写)同 Worker 完全一致;**隔离红线:不进 Top 8、不进「本看板累计」、不进架构健康/学习飞轮基数**,只并列展示;随统计范围裁剪(按 `assistant/message` 事件的本地日落点);日志不存在/读取失败/无 usage 一律**静默降级**(不渲染该行,不炸轮询)
113
+ - 统计口径边界:本区累计 / Top 8 / 架构健康与学习飞轮基数只统计**看板派发的 Worker/Verifier run**,**不含主窗口对话**;主窗口对话消耗按上条单列一行 + 模型分布尾部一条并列展示(不越界混入,UI caption 已明示)
113
114
  - 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判
114
115
  - 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
115
116
  - 统计区新增「调研 ROI」对比行:resolved/archived 卡按有无调研注入分组现算卡数 / 平均执行时长 / 平均 token(双组总样本 ≥4 才渲染,无调研组明显更慢时数字 warn 色提示)
@@ -245,7 +246,7 @@ v1.6.0 起 host 端从单体 index.mjs(1487 行)拆为薄壳 + 7 个领域
245
246
  | 模块 | 域 | 内容 |
246
247
  |---|---|---|
247
248
  | `policy.mjs` | 策略层 | 粒度治理软闸门 + 学习飞轮候选教训(纯函数零状态) |
248
- | `usage.mjs` | 统计 | v4 会话日志定位 / zstd 分帧 / token usage 聚合(纯函数;有效消耗 `effectiveTokens` + `byModelEff` 模型有效分摊 + `byDay` 双指标 `{t,e}` 聚合,兼容老 number 日账;统计范围按任务级 `t.runs[].at` 逐 run 裁切;`sinceSeq` 水位线增量结算——同一持久子会话多次结算不重复计账) |
249
+ | `usage.mjs` | 统计 | v4 会话日志定位 / zstd 分帧 / token usage 聚合(纯函数;有效消耗 `effectiveTokens` + `byModelEff` 模型有效分摊 + `byDay` 双指标 `{t,e}` 聚合,兼容老 number 日账;统计范围按任务级 `t.runs[].at` 逐 run 裁切;`sinceSeq` 水位线增量结算——同一持久子会话多次结算不重复计账;`readMainWindowUsage` 主窗口会话**增量尾读**聚合——文件偏移水位缓存,追加只读增量、截断全量重读、尾部半帧不结算) |
249
250
  | `session.mjs` | 会话 | root 解析缓存 / 会话 id 归一 / workMode 派生 / runsFor |
250
251
  | `store.mjs` | 持久化 | boardPath / rt / wt 原子落盘 / 跨重启继承 / fileLocks 串行化 / mutateLocked |
251
252
  | `notify.mjs` | 通知 | makeMsg / 歧义 25s 去抖 / 回执聚合 + 空闲门控 / 投递前过滤 |
package/index.mjs CHANGED
@@ -15,7 +15,7 @@
15
15
  //
16
16
  // 兼容 re-export:对外导出契约不变(单测与历史引用直接 import 自本文件)。
17
17
  export { TASK_SIZE_CONTRACT, SUGGEST_SPLIT_TEXT, TEAM_SPLIT_RULE, suggestSplitOf, withSplitHint, splitRuleOf } from './lib/policy.mjs'
18
- export { findRunLog, readRunUsage, aggregateUsageSummary, effectiveTokens, taskEffectiveTokens } from './lib/usage.mjs'
18
+ export { findRunLog, readRunUsage, aggregateUsageSummary, effectiveTokens, taskEffectiveTokens, readMainWindowUsage } from './lib/usage.mjs'
19
19
  import { createSession } from './lib/session.mjs'
20
20
  import { createStore } from './lib/store.mjs'
21
21
  import { createNotify } from './lib/notify.mjs'
@@ -45,6 +45,8 @@ export function apply(ctx) {
45
45
  receiptBuf: {}, // 回执 45s/满 5 条聚合窗口
46
46
  receiptedKeys: {}, // 回执幂等去重(完成事件指纹,1h TTL)
47
47
  sysNotesBuf: {}, // 系统级异常通知队列(随回执冲刷)
48
+ mainWindowUsageCache: {}, // 主窗口消耗增量尾读缓存(task-muwsol23):sid → { size, mtimeMs, 聚合五分量+byDay },
49
+ // 文件不变零读 / 变大只读增量 / 变小全量重读一次;纯内存不落盘,重启自然全量一次
48
50
  }
49
51
  // ===== 模块接线(依赖顺序:session → store → notify → dispatch → rpc)=====
50
52
  var session = createSession(ctx, state)
@@ -237,10 +237,11 @@
237
237
  // usageSummary 由 host 从各任务 t.usage 现算(t.usage 来自 Worker/Verifier 会话 v4 日志的
238
238
  // assistant/message.usage 聚合)。这里只做展示、不做计费断言;日志读不到/还没有 run 结算时
239
239
  // usageSummary.total 为 0,一律显示「暂无数据」。
240
- // 日账(usageSummary.byDay:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
240
+ // 日账(usageSummary.byDayFull:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
241
241
  // 双指标口径:t = 总量(含缓存读)、e = 有效消耗(输入+输出+缓存写,不含缓存读)。
242
242
  // e === null 表示该日只有老形态 number 日账 / 存量兜底(没有逐 run 拆分,有效值不可知)——
243
- // 展示时退化为 t 并在文案/title 上标 ~ 近似,绝不把总量冒充有效值。
243
+ // 旧口径日不拿 t 冒充有效(task-muwsnyqv ③:本板 10-05 t=154M 含 97% 缓存读曾被画成巨柱):
244
+ // 今日大数字显示「—」+ title 说明,近 7 天画矮灰柱 + tooltip 标「旧口径数据(仅总量)」。
244
245
  // dayKey 口径与 host 记账完全一致——本地 getters 拼,绝不用 toISOString()(UTC 会让
245
246
  // 晚上 8 点后的消耗落到次日,今日消耗直接错位)。
246
247
  function localDayKey(d) {
@@ -281,7 +282,29 @@
281
282
  var tgOn = !!(tg.from || tg.to)
282
283
  var head = React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('bar-chart-3', 11), 'Token 消耗',
283
284
  tgOn ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, color: C.brand, border: '1px solid ' + C.brand, borderRadius: 8, padding: '0 6px' }, title: '模型分布 / 任务消耗 Top 8 / 累计已按统计范围过滤:' + rangeLabel() }, '范围内: ' + rangeLabel()) : null)
284
- if (!u || !u.total) return React.createElement('div', { style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据(Worker/Verifier 会话日志里还没有 usage 记录)'))
285
+ // ===== 主窗口(本会话对话)消耗(task-muwsol23):host 增量尾读主会话 v4 日志聚合 =====
286
+ // 隔离口径:只在 Token 区单列一行 + 模型分布尾部追加一条;不进累计 / Top 8 / 模型归属摊派。
287
+ // 缺字段(老 host)/ 读不到日志 / 无 usage → mw=null → 两处都不渲染(静默降级,不炸轮询)。
288
+ var mw = (u && u.mainWindow && typeof u.mainWindow === 'object' && typeof u.mainWindow.effective === 'number') ? u.mainWindow : null
289
+ var todayKey0 = localDayKey()
290
+ var mwToday = 0
291
+ if (mw && mw.byDay && typeof mw.byDay === 'object') {
292
+ var mwc0 = mw.byDay[todayKey0]
293
+ mwToday = (mwc0 && typeof mwc0.e === 'number') ? mwc0.e : 0
294
+ }
295
+ // 主窗口单列行(累计行下方):「主窗口(本会话):今日有效 X · 累计 Y(缓存读 Z)」
296
+ function mwLine() {
297
+ if (!mw) return null
298
+ return React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8 }, title: '主窗口(本会话对话)消耗:host 增量尾读本会话 v4 日志聚合,有效口径 = 输入+输出+缓存写(不含缓存读)' + (tgOn ? ';随统计范围裁剪(按助手消息的本地日落点)' : '') + '。与看板派发口径并列展示——不进累计 / Top 8 / 模型归属摊派' },
299
+ '主窗口(本会话):今日有效 ',
300
+ React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mwToday)),
301
+ ' · 累计 ',
302
+ React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mw.effective)),
303
+ '(缓存读 ',
304
+ React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mw.cacheRead)),
305
+ ')')
306
+ }
307
+ if (!u || !u.total) return React.createElement('div', { style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据(Worker/Verifier 会话日志里还没有 usage 记录)'), mwLine())
285
308
  // ===== 主数字口径统一(task-muwq9u04):模型分布行与 Top8 行的主数字一律显示**有效消耗**(e),
286
309
  // 含缓存读的合计(t)退到 title 悬浮;否则同一块里「今日有效 3.7M」与「单任务 21M」并排自相矛盾
287
310
  // (用户就是这么判成 bug 的)。数据源:host 的 byModelEff(模型有效分摊)与 topTasks[].effective;
@@ -293,52 +316,65 @@
293
316
  return { model: m, total: tt, eff: (ee === null ? tt : ee), approx: ee === null }
294
317
  }).sort(function (a, b) { return b.eff - a.eff })
295
318
  var maxM = models.length ? (models[0].eff || 1) : 1
319
+ // 主窗口行与 run 模型同一把尺(参与归一,条形长度才可比),但固定尾部追加、不参与排序(见渲染处)
320
+ if (mw && mw.effective > maxM) maxM = mw.effective
296
321
  var top = (u.topTasks || []).map(function (x) {
297
322
  var ee = (typeof x.effective === 'number') ? x.effective : null
298
323
  return { id: x.id, title: x.title, total: x.total, cacheRead: x.cacheRead, runs: x.runs, eff: (ee === null ? x.total : ee), approx: ee === null }
299
324
  }).sort(function (a, b) { return b.eff - a.eff })
300
325
  var maxT = top.length ? (top[0].eff || 1) : 1
301
- // 日账:今日数字取本地日 key,没有日账(byDay 缺字段/老 host)时退化为 0,不炸也不误报。
326
+ // 日账:今日数字取本地日 key,没有日账(缺字段/老 host)时退化为 0,不炸也不误报。
302
327
  // 读侧兼容两种单元形态:老 number(只有总量,有效值不可知)→ { t: n, e: null }。
303
- var byDay = (u.byDay && typeof u.byDay === 'object') ? u.byDay : {}
328
+ // 固定口径数据源(task-muwsnyqv ②):「今日」「近 7 天」读**未过滤**的 byDayFull——caption
329
+ // 承诺这两处不随统计范围变,读范围内 byDay 会让「今天被范围裁掉」时大数字凭空归零(实证)。
330
+ // 老 host 缺 byDayFull → 退化读 byDay(范围内口径,数值可能偏小但形态一致,不炸)。
331
+ var byDay = (u.byDayFull && typeof u.byDayFull === 'object') ? u.byDayFull : ((u.byDay && typeof u.byDay === 'object') ? u.byDay : {})
304
332
  function dayOf(v) {
305
333
  if (v && typeof v === 'object') return { t: Number(v.t) || 0, e: (v.e === 0 || v.e) ? Number(v.e) || 0 : null }
306
334
  return { t: Number(v) || 0, e: null }
307
335
  }
308
336
  var todayKey = localDayKey()
309
337
  var todayCell = dayOf(byDay[todayKey])
310
- // 有效消耗缺失(老日账/存量兜底)→ 大数字退化为总量并打 approx 标 ~,口径不伪造
311
- var todayApprox = todayCell.e === null
312
- var todayEff = todayApprox ? todayCell.t : todayCell.e
338
+ // 旧口径日(e===null 且有总量:老 number 日账/存量兜底,只有含缓存读的总量)不拿 t 冒充有效
339
+ // (task-muwsnyqv ③)——大数字显示「—」+ title 说明;t=0 且无日账 = 今日尚无结算,照常显示 0。
340
+ var todayLegacy = todayCell.e === null && todayCell.t > 0
341
+ var todayEff = todayCell.e === null ? 0 : todayCell.e
313
342
  var days = lastNDays(7)
314
343
  var maxDay = 1
315
344
  var hasDayData = false
316
- for (var di = 0; di < days.length; di++) { var dc = dayOf(byDay[days[di]]); var dv = (dc.e === null ? dc.t : dc.e); if (dv > maxDay) maxDay = dv; if (dc.t > 0 || dc.e > 0) hasDayData = true }
345
+ var hasLegacy = false
346
+ // maxDay 只按有效消耗归一:旧口径日(e===null)的 t 含缓存读(本板实测占 97%),参与归一会
347
+ // 把其余日子的有效柱压成平地(task-muwsnyqv ③:10-05 t=154M 巨柱就是这么来的)。
348
+ for (var di = 0; di < days.length; di++) { var dc = dayOf(byDay[days[di]]); if (dc.e !== null && dc.e > maxDay) maxDay = dc.e; if (dc.e === null && dc.t > 0) hasLegacy = true; if (dc.t > 0 || dc.e > 0) hasDayData = true }
317
349
  function mini(label, value) { return React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, label + ' ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(value))) }
318
350
  // 累计三分量:有效(真实成本)+ 缓存读(占了 total 的大头,必须单列才看得出虚高来源)+ 合计
319
351
  var effTotal = (typeof u.effective === 'number') ? u.effective : ((u.input || 0) + (u.output || 0) + (u.cacheWrite || 0))
320
352
  return React.createElement('div', { style: box },
321
353
  head,
322
354
  React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap', marginBottom: 8 } },
323
- React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand }, title: '今日有效消耗(本地日 ' + todayKey + ')= 输入+输出+缓存写,不含缓存读;一次 run 的消耗整笔记在结算日' + (todayApprox ? '。本条日账来自老形态/存量兜底数据,有效值不可知,此处以总量近似(标 ~)' : '') }, (todayApprox ? '~' : '') + fmtTokens(todayEff)),
324
- React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(今日有效' + (todayApprox ? ' · 近似' : '') + ')'),
325
- todayCell.t > todayEff ? React.createElement('span', { style: { fontSize: 10, color: C.text2 }, title: '今日总量(含缓存读)——与有效消耗的差额就是缓存读' }, '含缓存读共 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(todayCell.t))) : null,
355
+ React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: todayLegacy ? C.text2 : C.brand }, title: todayLegacy ? ('今日有效消耗不可知:该日账为旧口径数据(仅总量 ' + fmtTokens(todayCell.t) + ' tok,含缓存读),总量不冒充有效值') : ('今日有效消耗(本地日 ' + todayKey + ')= 输入+输出+缓存写,不含缓存读;一次 run 的消耗整笔记在结算日') }, todayLegacy ? '—' : fmtTokens(todayEff)),
356
+ React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(今日有效' + (todayLegacy ? ' · 旧口径不可知' : '') + ')'),
357
+ todayCell.t > todayEff ? React.createElement('span', { style: { fontSize: 10, color: C.text2 }, title: '今日总量(含缓存读)——与有效消耗的差额就是缓存读' + (todayLegacy ? ';该日为旧口径数据,有效消耗不可知' : '') }, '含缓存读共 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(todayCell.t))) : null,
326
358
  React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '累计(本看板) 有效 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(effTotal)), ' · 缓存读 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(u.cacheRead)), ' · 合计 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(u.total))),
327
359
  mini('输入', u.input), mini('输出', u.output), u.cacheWrite ? mini('缓存写', u.cacheWrite) : null),
360
+ // 主窗口(本会话对话)单列行:累计行下方,与看板派发口径并列不混入(mw=null 静默不渲染)
361
+ mwLine(),
328
362
  // 近 7 天迷你条形:高按区间 max 归一(今天高亮 brand,其余浅底 + 边框),
329
- // 柱高一律取**有效消耗**;e 不可知的日退化为总量(title 标 ~ 近似)。
330
- // 7 天全为 0 时整块不渲染(零残留,不占版面)
363
+ // 柱高一律取**有效消耗**;e 不可知的旧口径日不拿总量冒充——画固定矮灰柱(tooltip 标明
364
+ // 「旧口径数据(仅总量,含缓存读)」),有效柱高不被缓存读撑歪。7 天全为 0 时整块不渲染。
331
365
  hasDayData ? React.createElement('div', { style: { marginBottom: 8 } },
332
- React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(有效消耗)'),
366
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(有效消耗)', hasLegacy ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, marginLeft: 6 } }, '灰柱 = 旧口径数据(仅总量)') : null),
333
367
  React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 4 } },
334
368
  days.map(function (k) {
335
369
  var c = dayOf(byDay[k])
336
- var approx = c.e === null
337
- var v = approx ? c.t : c.e
370
+ // 旧口径日(e===null 且有总量):不拿 t 冒充有效画柱(task-muwsnyqv ③)——
371
+ // 画固定矮灰柱 + tooltip 标明;有效日柱高照常按有效消耗归一。
372
+ var legacy = c.e === null && c.t > 0
373
+ var v = legacy ? 0 : (c.e || 0)
338
374
  var isToday = k === todayKey
339
- var h = v > 0 ? Math.max(3, Math.round(v / maxDay * 32)) : 3
340
- return React.createElement('div', { key: k, title: k.slice(5) + ':有效 ' + String(v) + (approx ? '(近似:老日账只有总量)' : '') + ' tok' + (c.t > v ? ' / 含缓存读共 ' + String(c.t) + ' tok' : ''), style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
341
- React.createElement('div', { style: { width: '100%', height: h, background: isToday ? C.brand : C.nested, border: '1px solid ' + (isToday ? C.brand : C.border), borderRadius: 2 } }),
375
+ var h = legacy ? 6 : (v > 0 ? Math.max(3, Math.round(v / maxDay * 32)) : 3)
376
+ return React.createElement('div', { key: k, title: legacy ? (k.slice(5) + ':旧口径数据(仅总量 ' + String(c.t) + ' tok,含缓存读;有效消耗不可知,不拿总量画柱)') : (k.slice(5) + ':有效 ' + String(v) + ' tok' + (c.t > v ? ' / 含缓存读共 ' + String(c.t) + ' tok' : '')), style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
377
+ React.createElement('div', { style: { width: '100%', height: h, background: legacy ? C.border : (isToday ? C.brand : C.nested), border: '1px solid ' + (legacy ? C.border2 : (isToday ? C.brand : C.border)), borderRadius: 2 } }),
342
378
  React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2, whiteSpace: 'nowrap' } }, k.slice(5)))
343
379
  }))) : null,
344
380
  React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } },
@@ -346,18 +382,22 @@
346
382
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按模型分布(有效消耗)'),
347
383
  models.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : models.map(function (m) {
348
384
  return React.createElement(UsageRow, { key: m.model, label: m.model, value: m.eff, max: maxM, color: C.brand, title: m.model + ':有效 ' + (m.approx ? '~' : '') + String(m.eff) + ' tokens(不含缓存读)' + (m.approx ? '——本模型无有效分量留账,以合计近似' : '') + ' · 含缓存读合计 ' + String(m.total) + ' tokens' })
349
- })),
385
+ }),
386
+ // 模型分布尾部追加「主窗口(对话)」一条(task-muwsol23):有效为 0 不渲染;
387
+ // 琥珀色与 run 模型(brand)区分,固定尾部不参与排序,与派发口径并列不混入
388
+ (mw && mw.effective > 0) ? React.createElement(UsageRow, { key: 'main-window', label: '主窗口(对话)', value: mw.effective, max: maxM, color: C.warn, title: '主窗口(对话):有效 ' + String(mw.effective) + ' tokens(不含缓存读)· 缓存读 ' + String(mw.cacheRead) + ' tokens——本会话对话消耗,与看板派发口径并列不混入(不进累计 / Top 8)' }) : null),
350
389
  React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
351
390
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8(有效消耗)'),
352
391
  top.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : top.map(function (x) {
353
392
  var t = getTask(x.id)
354
393
  return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.eff, max: maxT, color: C.ok, labelColor: t ? C.brand : C.text2, title: x.title + '(有效 ' + (x.approx ? '~' : '') + String(x.eff) + ' tokens(不含缓存读) · 含缓存读合计 ' + String(x.total) + ' · 其中缓存读 ' + String(x.cacheRead || 0) + ' · ' + (x.runs || 0) + ' 次 run)' + (x.approx ? '(老卡无五分量留账,有效值以合计近似)' : '') + (t ? '——点击查看详情' : ''), onClick: t ? function () { state.detailId = x.id; notify() } : undefined })
355
394
  }))),
356
- // 口径边界:本区只统计看板派发的 Worker/Verifier run,主窗口对话自身不越界纳入
395
+ // 口径边界:累计 / Top 8 只统计看板派发的 Worker/Verifier run;主窗口对话消耗自 task-muwsol23 起
396
+ // 单列一行 + 模型分布尾部一条并列展示(不越界混入累计/Top8/归属摊派),caption 必须讲清并列关系。
357
397
  // 范围说明(task-muwc7hjd):用户常把 RangeFilter 当成「整页过滤」,但今日大数字与近 7 天柱子
358
398
  // 是自身固定口径(今日=本地今天、近 7 天=最近 7 个本地日)——不随范围变,必须在文案里讲清,
359
399
  // 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
360
- React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话;主数字(模型分布 / Top 8)与「今日」「近 7 天」均为有效消耗口径(输入+输出+缓存写,不含缓存读),含缓存读的合计在悬浮 title 里单列对照'),
400
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run 消耗;主窗口行=本会话对话消耗,与看板派发口径并列不混入(单列一行 + 模型分布尾部一条,不进累计 / Top 8);主数字(模型分布 / Top 8)与「今日」「近 7 天」均为有效消耗口径(输入+输出+缓存写,不含缓存读),含缓存读的合计在悬浮 title 里单列对照'),
361
401
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'))
362
402
  }
363
403
 
package/lib/client.js CHANGED
@@ -1569,10 +1569,11 @@ function apply(ctx) {
1569
1569
  // usageSummary 由 host 从各任务 t.usage 现算(t.usage 来自 Worker/Verifier 会话 v4 日志的
1570
1570
  // assistant/message.usage 聚合)。这里只做展示、不做计费断言;日志读不到/还没有 run 结算时
1571
1571
  // usageSummary.total 为 0,一律显示「暂无数据」。
1572
- // 日账(usageSummary.byDay:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
1572
+ // 日账(usageSummary.byDayFull:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
1573
1573
  // 双指标口径:t = 总量(含缓存读)、e = 有效消耗(输入+输出+缓存写,不含缓存读)。
1574
1574
  // e === null 表示该日只有老形态 number 日账 / 存量兜底(没有逐 run 拆分,有效值不可知)——
1575
- // 展示时退化为 t 并在文案/title 上标 ~ 近似,绝不把总量冒充有效值。
1575
+ // 旧口径日不拿 t 冒充有效(task-muwsnyqv ③:本板 10-05 t=154M 含 97% 缓存读曾被画成巨柱):
1576
+ // 今日大数字显示「—」+ title 说明,近 7 天画矮灰柱 + tooltip 标「旧口径数据(仅总量)」。
1576
1577
  // dayKey 口径与 host 记账完全一致——本地 getters 拼,绝不用 toISOString()(UTC 会让
1577
1578
  // 晚上 8 点后的消耗落到次日,今日消耗直接错位)。
1578
1579
  function localDayKey(d) {
@@ -1613,7 +1614,29 @@ function apply(ctx) {
1613
1614
  var tgOn = !!(tg.from || tg.to)
1614
1615
  var head = React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('bar-chart-3', 11), 'Token 消耗',
1615
1616
  tgOn ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, color: C.brand, border: '1px solid ' + C.brand, borderRadius: 8, padding: '0 6px' }, title: '模型分布 / 任务消耗 Top 8 / 累计已按统计范围过滤:' + rangeLabel() }, '范围内: ' + rangeLabel()) : null)
1616
- if (!u || !u.total) return React.createElement('div', { style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据(Worker/Verifier 会话日志里还没有 usage 记录)'))
1617
+ // ===== 主窗口(本会话对话)消耗(task-muwsol23):host 增量尾读主会话 v4 日志聚合 =====
1618
+ // 隔离口径:只在 Token 区单列一行 + 模型分布尾部追加一条;不进累计 / Top 8 / 模型归属摊派。
1619
+ // 缺字段(老 host)/ 读不到日志 / 无 usage → mw=null → 两处都不渲染(静默降级,不炸轮询)。
1620
+ var mw = (u && u.mainWindow && typeof u.mainWindow === 'object' && typeof u.mainWindow.effective === 'number') ? u.mainWindow : null
1621
+ var todayKey0 = localDayKey()
1622
+ var mwToday = 0
1623
+ if (mw && mw.byDay && typeof mw.byDay === 'object') {
1624
+ var mwc0 = mw.byDay[todayKey0]
1625
+ mwToday = (mwc0 && typeof mwc0.e === 'number') ? mwc0.e : 0
1626
+ }
1627
+ // 主窗口单列行(累计行下方):「主窗口(本会话):今日有效 X · 累计 Y(缓存读 Z)」
1628
+ function mwLine() {
1629
+ if (!mw) return null
1630
+ return React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8 }, title: '主窗口(本会话对话)消耗:host 增量尾读本会话 v4 日志聚合,有效口径 = 输入+输出+缓存写(不含缓存读)' + (tgOn ? ';随统计范围裁剪(按助手消息的本地日落点)' : '') + '。与看板派发口径并列展示——不进累计 / Top 8 / 模型归属摊派' },
1631
+ '主窗口(本会话):今日有效 ',
1632
+ React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mwToday)),
1633
+ ' · 累计 ',
1634
+ React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mw.effective)),
1635
+ '(缓存读 ',
1636
+ React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mw.cacheRead)),
1637
+ ')')
1638
+ }
1639
+ if (!u || !u.total) return React.createElement('div', { style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据(Worker/Verifier 会话日志里还没有 usage 记录)'), mwLine())
1617
1640
  // ===== 主数字口径统一(task-muwq9u04):模型分布行与 Top8 行的主数字一律显示**有效消耗**(e),
1618
1641
  // 含缓存读的合计(t)退到 title 悬浮;否则同一块里「今日有效 3.7M」与「单任务 21M」并排自相矛盾
1619
1642
  // (用户就是这么判成 bug 的)。数据源:host 的 byModelEff(模型有效分摊)与 topTasks[].effective;
@@ -1625,52 +1648,65 @@ function apply(ctx) {
1625
1648
  return { model: m, total: tt, eff: (ee === null ? tt : ee), approx: ee === null }
1626
1649
  }).sort(function (a, b) { return b.eff - a.eff })
1627
1650
  var maxM = models.length ? (models[0].eff || 1) : 1
1651
+ // 主窗口行与 run 模型同一把尺(参与归一,条形长度才可比),但固定尾部追加、不参与排序(见渲染处)
1652
+ if (mw && mw.effective > maxM) maxM = mw.effective
1628
1653
  var top = (u.topTasks || []).map(function (x) {
1629
1654
  var ee = (typeof x.effective === 'number') ? x.effective : null
1630
1655
  return { id: x.id, title: x.title, total: x.total, cacheRead: x.cacheRead, runs: x.runs, eff: (ee === null ? x.total : ee), approx: ee === null }
1631
1656
  }).sort(function (a, b) { return b.eff - a.eff })
1632
1657
  var maxT = top.length ? (top[0].eff || 1) : 1
1633
- // 日账:今日数字取本地日 key,没有日账(byDay 缺字段/老 host)时退化为 0,不炸也不误报。
1658
+ // 日账:今日数字取本地日 key,没有日账(缺字段/老 host)时退化为 0,不炸也不误报。
1634
1659
  // 读侧兼容两种单元形态:老 number(只有总量,有效值不可知)→ { t: n, e: null }。
1635
- var byDay = (u.byDay && typeof u.byDay === 'object') ? u.byDay : {}
1660
+ // 固定口径数据源(task-muwsnyqv ②):「今日」「近 7 天」读**未过滤**的 byDayFull——caption
1661
+ // 承诺这两处不随统计范围变,读范围内 byDay 会让「今天被范围裁掉」时大数字凭空归零(实证)。
1662
+ // 老 host 缺 byDayFull → 退化读 byDay(范围内口径,数值可能偏小但形态一致,不炸)。
1663
+ var byDay = (u.byDayFull && typeof u.byDayFull === 'object') ? u.byDayFull : ((u.byDay && typeof u.byDay === 'object') ? u.byDay : {})
1636
1664
  function dayOf(v) {
1637
1665
  if (v && typeof v === 'object') return { t: Number(v.t) || 0, e: (v.e === 0 || v.e) ? Number(v.e) || 0 : null }
1638
1666
  return { t: Number(v) || 0, e: null }
1639
1667
  }
1640
1668
  var todayKey = localDayKey()
1641
1669
  var todayCell = dayOf(byDay[todayKey])
1642
- // 有效消耗缺失(老日账/存量兜底)→ 大数字退化为总量并打 approx 标 ~,口径不伪造
1643
- var todayApprox = todayCell.e === null
1644
- var todayEff = todayApprox ? todayCell.t : todayCell.e
1670
+ // 旧口径日(e===null 且有总量:老 number 日账/存量兜底,只有含缓存读的总量)不拿 t 冒充有效
1671
+ // (task-muwsnyqv ③)——大数字显示「—」+ title 说明;t=0 且无日账 = 今日尚无结算,照常显示 0。
1672
+ var todayLegacy = todayCell.e === null && todayCell.t > 0
1673
+ var todayEff = todayCell.e === null ? 0 : todayCell.e
1645
1674
  var days = lastNDays(7)
1646
1675
  var maxDay = 1
1647
1676
  var hasDayData = false
1648
- for (var di = 0; di < days.length; di++) { var dc = dayOf(byDay[days[di]]); var dv = (dc.e === null ? dc.t : dc.e); if (dv > maxDay) maxDay = dv; if (dc.t > 0 || dc.e > 0) hasDayData = true }
1677
+ var hasLegacy = false
1678
+ // maxDay 只按有效消耗归一:旧口径日(e===null)的 t 含缓存读(本板实测占 97%),参与归一会
1679
+ // 把其余日子的有效柱压成平地(task-muwsnyqv ③:10-05 t=154M 巨柱就是这么来的)。
1680
+ for (var di = 0; di < days.length; di++) { var dc = dayOf(byDay[days[di]]); if (dc.e !== null && dc.e > maxDay) maxDay = dc.e; if (dc.e === null && dc.t > 0) hasLegacy = true; if (dc.t > 0 || dc.e > 0) hasDayData = true }
1649
1681
  function mini(label, value) { return React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, label + ' ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(value))) }
1650
1682
  // 累计三分量:有效(真实成本)+ 缓存读(占了 total 的大头,必须单列才看得出虚高来源)+ 合计
1651
1683
  var effTotal = (typeof u.effective === 'number') ? u.effective : ((u.input || 0) + (u.output || 0) + (u.cacheWrite || 0))
1652
1684
  return React.createElement('div', { style: box },
1653
1685
  head,
1654
1686
  React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap', marginBottom: 8 } },
1655
- React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand }, title: '今日有效消耗(本地日 ' + todayKey + ')= 输入+输出+缓存写,不含缓存读;一次 run 的消耗整笔记在结算日' + (todayApprox ? '。本条日账来自老形态/存量兜底数据,有效值不可知,此处以总量近似(标 ~)' : '') }, (todayApprox ? '~' : '') + fmtTokens(todayEff)),
1656
- React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(今日有效' + (todayApprox ? ' · 近似' : '') + ')'),
1657
- todayCell.t > todayEff ? React.createElement('span', { style: { fontSize: 10, color: C.text2 }, title: '今日总量(含缓存读)——与有效消耗的差额就是缓存读' }, '含缓存读共 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(todayCell.t))) : null,
1687
+ React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: todayLegacy ? C.text2 : C.brand }, title: todayLegacy ? ('今日有效消耗不可知:该日账为旧口径数据(仅总量 ' + fmtTokens(todayCell.t) + ' tok,含缓存读),总量不冒充有效值') : ('今日有效消耗(本地日 ' + todayKey + ')= 输入+输出+缓存写,不含缓存读;一次 run 的消耗整笔记在结算日') }, todayLegacy ? '—' : fmtTokens(todayEff)),
1688
+ React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(今日有效' + (todayLegacy ? ' · 旧口径不可知' : '') + ')'),
1689
+ todayCell.t > todayEff ? React.createElement('span', { style: { fontSize: 10, color: C.text2 }, title: '今日总量(含缓存读)——与有效消耗的差额就是缓存读' + (todayLegacy ? ';该日为旧口径数据,有效消耗不可知' : '') }, '含缓存读共 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(todayCell.t))) : null,
1658
1690
  React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '累计(本看板) 有效 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(effTotal)), ' · 缓存读 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(u.cacheRead)), ' · 合计 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(u.total))),
1659
1691
  mini('输入', u.input), mini('输出', u.output), u.cacheWrite ? mini('缓存写', u.cacheWrite) : null),
1692
+ // 主窗口(本会话对话)单列行:累计行下方,与看板派发口径并列不混入(mw=null 静默不渲染)
1693
+ mwLine(),
1660
1694
  // 近 7 天迷你条形:高按区间 max 归一(今天高亮 brand,其余浅底 + 边框),
1661
- // 柱高一律取**有效消耗**;e 不可知的日退化为总量(title 标 ~ 近似)。
1662
- // 7 天全为 0 时整块不渲染(零残留,不占版面)
1695
+ // 柱高一律取**有效消耗**;e 不可知的旧口径日不拿总量冒充——画固定矮灰柱(tooltip 标明
1696
+ // 「旧口径数据(仅总量,含缓存读)」),有效柱高不被缓存读撑歪。7 天全为 0 时整块不渲染。
1663
1697
  hasDayData ? React.createElement('div', { style: { marginBottom: 8 } },
1664
- React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(有效消耗)'),
1698
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(有效消耗)', hasLegacy ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, marginLeft: 6 } }, '灰柱 = 旧口径数据(仅总量)') : null),
1665
1699
  React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 4 } },
1666
1700
  days.map(function (k) {
1667
1701
  var c = dayOf(byDay[k])
1668
- var approx = c.e === null
1669
- var v = approx ? c.t : c.e
1702
+ // 旧口径日(e===null 且有总量):不拿 t 冒充有效画柱(task-muwsnyqv ③)——
1703
+ // 画固定矮灰柱 + tooltip 标明;有效日柱高照常按有效消耗归一。
1704
+ var legacy = c.e === null && c.t > 0
1705
+ var v = legacy ? 0 : (c.e || 0)
1670
1706
  var isToday = k === todayKey
1671
- var h = v > 0 ? Math.max(3, Math.round(v / maxDay * 32)) : 3
1672
- return React.createElement('div', { key: k, title: k.slice(5) + ':有效 ' + String(v) + (approx ? '(近似:老日账只有总量)' : '') + ' tok' + (c.t > v ? ' / 含缓存读共 ' + String(c.t) + ' tok' : ''), style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
1673
- React.createElement('div', { style: { width: '100%', height: h, background: isToday ? C.brand : C.nested, border: '1px solid ' + (isToday ? C.brand : C.border), borderRadius: 2 } }),
1707
+ var h = legacy ? 6 : (v > 0 ? Math.max(3, Math.round(v / maxDay * 32)) : 3)
1708
+ return React.createElement('div', { key: k, title: legacy ? (k.slice(5) + ':旧口径数据(仅总量 ' + String(c.t) + ' tok,含缓存读;有效消耗不可知,不拿总量画柱)') : (k.slice(5) + ':有效 ' + String(v) + ' tok' + (c.t > v ? ' / 含缓存读共 ' + String(c.t) + ' tok' : '')), style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
1709
+ React.createElement('div', { style: { width: '100%', height: h, background: legacy ? C.border : (isToday ? C.brand : C.nested), border: '1px solid ' + (legacy ? C.border2 : (isToday ? C.brand : C.border)), borderRadius: 2 } }),
1674
1710
  React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2, whiteSpace: 'nowrap' } }, k.slice(5)))
1675
1711
  }))) : null,
1676
1712
  React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } },
@@ -1678,18 +1714,22 @@ function apply(ctx) {
1678
1714
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按模型分布(有效消耗)'),
1679
1715
  models.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : models.map(function (m) {
1680
1716
  return React.createElement(UsageRow, { key: m.model, label: m.model, value: m.eff, max: maxM, color: C.brand, title: m.model + ':有效 ' + (m.approx ? '~' : '') + String(m.eff) + ' tokens(不含缓存读)' + (m.approx ? '——本模型无有效分量留账,以合计近似' : '') + ' · 含缓存读合计 ' + String(m.total) + ' tokens' })
1681
- })),
1717
+ }),
1718
+ // 模型分布尾部追加「主窗口(对话)」一条(task-muwsol23):有效为 0 不渲染;
1719
+ // 琥珀色与 run 模型(brand)区分,固定尾部不参与排序,与派发口径并列不混入
1720
+ (mw && mw.effective > 0) ? React.createElement(UsageRow, { key: 'main-window', label: '主窗口(对话)', value: mw.effective, max: maxM, color: C.warn, title: '主窗口(对话):有效 ' + String(mw.effective) + ' tokens(不含缓存读)· 缓存读 ' + String(mw.cacheRead) + ' tokens——本会话对话消耗,与看板派发口径并列不混入(不进累计 / Top 8)' }) : null),
1682
1721
  React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
1683
1722
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8(有效消耗)'),
1684
1723
  top.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : top.map(function (x) {
1685
1724
  var t = getTask(x.id)
1686
1725
  return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.eff, max: maxT, color: C.ok, labelColor: t ? C.brand : C.text2, title: x.title + '(有效 ' + (x.approx ? '~' : '') + String(x.eff) + ' tokens(不含缓存读) · 含缓存读合计 ' + String(x.total) + ' · 其中缓存读 ' + String(x.cacheRead || 0) + ' · ' + (x.runs || 0) + ' 次 run)' + (x.approx ? '(老卡无五分量留账,有效值以合计近似)' : '') + (t ? '——点击查看详情' : ''), onClick: t ? function () { state.detailId = x.id; notify() } : undefined })
1687
1726
  }))),
1688
- // 口径边界:本区只统计看板派发的 Worker/Verifier run,主窗口对话自身不越界纳入
1727
+ // 口径边界:累计 / Top 8 只统计看板派发的 Worker/Verifier run;主窗口对话消耗自 task-muwsol23 起
1728
+ // 单列一行 + 模型分布尾部一条并列展示(不越界混入累计/Top8/归属摊派),caption 必须讲清并列关系。
1689
1729
  // 范围说明(task-muwc7hjd):用户常把 RangeFilter 当成「整页过滤」,但今日大数字与近 7 天柱子
1690
1730
  // 是自身固定口径(今日=本地今天、近 7 天=最近 7 个本地日)——不随范围变,必须在文案里讲清,
1691
1731
  // 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
1692
- React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话;主数字(模型分布 / Top 8)与「今日」「近 7 天」均为有效消耗口径(输入+输出+缓存写,不含缓存读),含缓存读的合计在悬浮 title 里单列对照'),
1732
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run 消耗;主窗口行=本会话对话消耗,与看板派发口径并列不混入(单列一行 + 模型分布尾部一条,不进累计 / Top 8);主数字(模型分布 / Top 8)与「今日」「近 7 天」均为有效消耗口径(输入+输出+缓存写,不含缓存读),含缓存读的合计在悬浮 title 里单列对照'),
1693
1733
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'))
1694
1734
  }
1695
1735
 
package/lib/rpc.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  import * as core from './core.mjs'
9
9
  import path from 'node:path'
10
10
  import fsNode from 'node:fs'
11
- import { findRunLog, readLogBytes, readLogFrames, aggregateUsageSummary } from './usage.mjs'
11
+ import { findRunLog, readLogBytes, readLogFrames, aggregateUsageSummary, readMainWindowUsage } from './usage.mjs'
12
12
  import { TASK_SIZE_CONTRACT, withSplitHint, pushRejectLesson, pushArbitrationLesson } from './policy.mjs'
13
13
  import { makeMsg } from './notify.mjs'
14
14
  import { computeHealthHints } from './health.mjs'
@@ -40,6 +40,8 @@ export function createRpc(ctx, state, deps) {
40
40
  var getActorId = deps.getActorId, resolveRoot = deps.resolveRoot, toolSessionId = deps.toolSessionId, rpcSessionId = deps.rpcSessionId
41
41
  var rootForSession = deps.rootForSession, deriveWorkMode = deps.deriveWorkMode, runsFor = deps.runsFor
42
42
  var rt = deps.rt, mutateLocked = deps.mutateLocked
43
+ // 主窗口消耗(task-muwsol23):测试可注入临时日志根;生产 undefined → findRunLog 回退 ~/.dsh/sessions
44
+ var sessionsRoot = (typeof deps.sessionsRoot === 'string' && deps.sessionsRoot) ? deps.sessionsRoot : undefined
43
45
  var maybeNotify = deps.maybeNotify, notifyTaskDone = deps.notifyTaskDone
44
46
  var spawnOneShot = deps.spawnOneShot, accumulateRunUsage = deps.accumulateRunUsage, readContextPack = deps.readContextPack
45
47
  // 上报通道的 run 收尾(task-muwkhqf8):board_report/board_verdict 推进任务落定后,对当前 continuable
@@ -73,6 +75,8 @@ export function createRpc(ctx, state, deps) {
73
75
  var teamModeCache = state.teamModeCache
74
76
  var feedbackCache = state.feedbackCache
75
77
  var epicSplitCache = state.epicSplitCache
78
+ // 主窗口消耗增量尾读缓存(task-muwsol23):本体在 index.mjs state 构建;测试桩 state 缺字段时就地补
79
+ if (!state.mainWindowUsageCache) state.mainWindowUsageCache = {}
76
80
  // RPC handlers 表必须在最前面初始化:后面的 handle(...) 调用依赖它(var 只提升声明不提升赋值)
77
81
  var handlers = state.handlers
78
82
  function handle(method, fn) { handlers[method] = fn }
@@ -148,6 +152,15 @@ export function createRpc(ctx, state, deps) {
148
152
  // 模型分布 / Top8 / 累计生效——run 级数据只在 host,所以过滤必须在 host 做(客户端只拿聚合,没法自己裁)。
149
153
  // 缺省/空范围 = 现状逐字不变(aggregateUsageSummary 内部判定,空串不算范围)。
150
154
  d.usageSummary = aggregateUsageSummary(d.tasks, args && args.range)
155
+ // ===== 主窗口(本会话对话)消耗单列(task-muwsol23)=====
156
+ // 数据源 = 本看板所属主会话自己的 v4 日志,readMainWindowUsage 增量尾读聚合
157
+ // (缓存 state.mainWindowUsageCache,键=会话 id:文件不变零读、变大只读增量、变小/轮换全量重读一次)。
158
+ // 隔离红线:只挂 usageSummary.mainWindow——**不进 Top8 / 「本看板累计」/ 架构健康与学习飞轮基数**
159
+ // (那些仍只读 d.tasks 的 run 留账,本行挂账发生在聚合完成之后,互不影响)。
160
+ // 随统计范围裁剪(assistant/message 事件本地日落点);读不到/无 usage/任何失败 → null 不挂字段
161
+ // (客户端静默不渲染该行),函数本体全 try/catch 绝不抛进 3s 轮询。
162
+ var __mw = readMainWindowUsage(sid, sessionsRoot, args && args.range, state.mainWindowUsageCache)
163
+ if (__mw) d.usageSummary.mainWindow = __mw
151
164
  // 架构自省 L1:healthHints 现算(纯函数零存储零 IO,近 50 卡窗口),客户端「架构健康」区超阈值才显示
152
165
  d.healthHints = computeHealthHints(d.tasks)
153
166
  // 史诗父卡语义层:childStats 现算(零存储)——{ <parentId>: { total, settled, resolved, active, activeTitle } },
package/lib/usage.mjs CHANGED
@@ -186,7 +186,8 @@ function dayCell(v) {
186
186
  // 为什么在 host 侧过滤:run 级数据(runs[i].usage 五分量 + runs[i].at 时间戳)只在 host,
187
187
  // 客户端轮询只拿聚合——范围筛选只能在这一层做,客户端没法自己裁。
188
188
  // 口径:run 的本地日落点 localDayKey(run.at) 判 in/out;范围外 run 不计入 total/byModel/top,
189
- // byDay 也同步裁到范围内(范围外日不入 byDay)。
189
+ // byDay 也同步裁到范围内(范围外日不入 byDay;run 留账任务按入选 run 逐日重建,to 端闭区间)。
190
+ // byDayFull 与范围无关恒为全量(task-muwsnyqv ②)——客户端「今日」「近 7 天」固定口径读它。
190
191
  // 缺省(无 range / from 与 to 皆空)= 现状逐字不变(parity):不引入任何新的过滤分支语义,
191
192
  // 老任务(无 runs 留账)与无 at 的老 run 照旧全量计入。
192
193
  function normDayKey(v) {
@@ -231,12 +232,37 @@ function runUsageOf(r) {
231
232
  // 「按任务 updatedAt 单日近似」,于是切范围后 topTasks 与无范围逐字相同(用户活体实证:选 2026-10-05
232
233
  // 后 total/byDay 收窄、Top8 第一名仍是全量 48M 的那张卡)。现在改为逐 run 精确裁切。
233
234
  export function aggregateUsageSummary(tasks, range) {
234
- var s = { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, effective: 0, byModel: {}, byModelEff: {}, byDay: {}, topTasks: [] }
235
+ var s = { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, effective: 0, byModel: {}, byModelEff: {}, byDay: {}, byDayFull: {}, topTasks: [] }
235
236
  var list = Array.isArray(tasks) ? tasks : []
236
237
  var hasRange = rangeOn(range)
237
238
  for (var i = 0; i < list.length; i++) {
238
239
  var t = list[i]; var uRaw = t && t.usage
239
240
  if (!uRaw) continue
241
+ // ===== byDayFull:未过滤全量日账(task-muwsnyqv ②)=====
242
+ // 「今日」「近 7 天」是固定口径(客户端 caption 承诺不随统计范围变),数据源必须是全量日账——
243
+ // 只读范围内裁剪的 byDay 时,「今天被范围裁掉」会让今日大数字凭空归零(活体实证:范围
244
+ // {10-05,10-06} 下今日条目整丢)。口径与无范围 byDay 逐字同源:任务级 uRaw.byDay 逐日合并
245
+ // (老 number → e=null 不猜),无 byDay 的存量任务整笔归 updatedAt 本地日;与下方范围判定
246
+ // 完全解耦(范围外任务照样计入 byDayFull),无范围时 byDayFull 与 byDay 内容一致(parity)。
247
+ var byF = (uRaw.byDay && typeof uRaw.byDay === 'object') ? uRaw.byDay : null
248
+ var hasByF = false
249
+ for (var bk0 in byF) { if (Object.prototype.hasOwnProperty.call(byF, bk0)) { hasByF = true; break } }
250
+ if (hasByF) {
251
+ for (var dk0 in byF) {
252
+ if (!Object.prototype.hasOwnProperty.call(byF, dk0)) continue
253
+ var c0 = dayCell(byF[dk0])
254
+ // 合并规则与 byDay 相同:任一来源 e 不可知 → 该日 e=null(不假装算得出);全部已知才累加。
255
+ var cur0 = s.byDayFull[dk0] || { t: 0, e: undefined }
256
+ cur0.t += c0.t
257
+ if (c0.e === null) cur0.e = null
258
+ else if (cur0.e !== null) cur0.e = (cur0.e || 0) + c0.e
259
+ if (cur0.e === undefined) cur0.e = null
260
+ s.byDayFull[dk0] = cur0
261
+ }
262
+ } else if (numOr0(uRaw.total)) {
263
+ var lkF = dayKeyOf(uRaw.updatedAt)
264
+ if (lkF) { var lcF = s.byDayFull[lkF] || { t: 0, e: undefined }; lcF.t += numOr0(uRaw.total); lcF.e = null; s.byDayFull[lkF] = lcF }
265
+ }
240
266
  // 过滤层只决定「这次任务贡献哪些用量」,下游累加口径(模型/Top/byDay)逐字不动。
241
267
  var u = uRaw
242
268
  if (hasRange) {
@@ -248,6 +274,7 @@ export function aggregateUsageSummary(tasks, range) {
248
274
  var covered = 0 // 落了 usage 的 run 数(判「有没有 run 级留账可裁」——与 at 无关)
249
275
  var modeledTot = 0 // 入选 run 里**记了模型名**的那部分总量(剩下的按任务级占比摊,见下)
250
276
  var modeledEff = 0
277
+ var dayB = null // 入选 run 的逐日归桶(重建范围内 byDay,见下)
251
278
  for (var ri = 0; ri < trs.length; ri++) {
252
279
  var r = trs[ri]
253
280
  var ru = runUsageOf(r)
@@ -257,6 +284,17 @@ export function aggregateUsageSummary(tasks, range) {
257
284
  picked++
258
285
  uSum.total += ru.total; uSum.input += ru.input; uSum.output += ru.output
259
286
  uSum.cacheRead += ru.cacheRead; uSum.cacheWrite += ru.cacheWrite
287
+ // byDay 同步重建(task-muwsnyqv ①):旧版合成的 u 丢 byDay/updatedAt → 范围内日账整块消失
288
+ // (「今天被吞」实证:range 含今天时 total/effective 含今天、byDay 却无今天)。按入选 run 的
289
+ // at 本地日逐日归桶——与范围过滤同一落点口径(dayInRange 两端闭区间:d > to 才丢),
290
+ // from=to=今天 时今天必在 byDay。run 级五分量俱全 → e 恒为已知有效值(不是 null)。
291
+ var rd = dayKeyOf(r && r.at)
292
+ if (rd) {
293
+ if (!dayB) dayB = {}
294
+ var dc1 = dayB[rd] || { t: 0, e: 0 }
295
+ dc1.t += ru.total; dc1.e += effectiveTokens(ru)
296
+ dayB[rd] = dc1
297
+ }
260
298
  // 按模型分布同步按范围收窄:模型 key 与结算同源(派发覆盖 > 日志记录),run 留账里没记模型名
261
299
  // → 退化为任务级模型小计,但只在本次有 run 入选时摊(否则等于把范围外消耗算进来)。
262
300
  // 合计与有效(byModel / byModelEff)同一轮同源累加,两条口径不各算一遍。
@@ -289,7 +327,7 @@ export function aggregateUsageSummary(tasks, range) {
289
327
  modelsE[tmKeys[ti]] = (modelsE[tmKeys[ti]] || 0) + Math.round(numOr0(taskModels[tmKeys[ti]]) * ratioE)
290
328
  }
291
329
  }
292
- u = { total: uSum.total, input: uSum.input, output: uSum.output, cacheRead: uSum.cacheRead, cacheWrite: uSum.cacheWrite, models: models || {}, modelEff: modelsE || {}, runs: picked }
330
+ u = { total: uSum.total, input: uSum.input, output: uSum.output, cacheRead: uSum.cacheRead, cacheWrite: uSum.cacheWrite, models: models || {}, modelEff: modelsE || {}, runs: picked, byDay: dayB || {} }
293
331
  } else if (!covered) {
294
332
  // 无 run 级留账可裁(本功能上线前的存量卡;或 runs 条目全都没落 usage):没有 run 级时间戳,
295
333
  // 整笔退化为「updatedAt 的本地日」这一近似口径(与 byDay 存量兜底同源);无 updatedAt / 解析失败 →
@@ -353,3 +391,128 @@ export function aggregateUsageSummary(tasks, range) {
353
391
  s.topTasks = s.topTasks.slice(0, 8)
354
392
  return s
355
393
  }
394
+
395
+ // ===== 主窗口(本会话对话)Token 消耗:增量尾读主会话自己的 v4 日志(task-muwsol23)=====
396
+ // 与 readRunUsage 的差异:主会话日志持续增长(可达数万事件),3s 轮询下每轮全量读会把
397
+ // 同步 IO 摊进轮询热路径——这里按「文件偏移水位」增量尾读,缓存条目由调用方持有(键=会话 id):
398
+ // { size, mtimeMs, total, input, output, cacheRead, cacheWrite, effective, byDay, hasUsage }
399
+ // - size = 已结算到的文件偏移;只推进到「最后一个完整解压帧」的末尾——尾部半帧(写入中)
400
+ // 不结算,下轮从该偏移重读(绝不丢事件、绝不重复计账);
401
+ // - 文件变大 → 只读 [size, 新size) 的新增字节段(3s 轮询稳态零全量重读);
402
+ // - 文件变小(日志轮换/截断)→ 缓存作废,全量重读一次(封顶 64MB,与 readRunUsage 同一护栏);
403
+ // - 文件不变 → 零读直接用缓存聚合值。
404
+ // 聚合口径与 Worker 完全同源:五分量(input/output/cacheRead/cacheWrite/total)+ e=input+output+cacheWrite,
405
+ // 日落点取 assistant/message 事件的 time 字段(epoch ms)的本地日(与 run 口径同一 localDayKey)。
406
+ // 范围裁剪:缓存里存的是**未裁剪**全量聚合;range({from,to} 本地日)在出参时按 byDay 逐日裁
407
+ // (与 run 口径同一 dayInRange,两端闭区间),范围内 total/effective/cacheRead 由入选日重算——
408
+ // 无日可判(无 time/坏 time)的事件只进平账不进任何日,范围下自然被裁(宁可漏不错)。
409
+ // 隔离红线:返回值只挂 usageSummary.mainWindow——不进 Top8 / 「本看板累计」/ 架构健康与学习飞轮基数。
410
+ // 失败静默降级:日志不存在/不可读/解不出任何 usage → null(客户端不渲染该行),绝不抛进轮询。
411
+ var MAINWIN_LOG_CAP = 64 * 1024 * 1024
412
+ // 聚合器初始形态(也是缓存条目形态)
413
+ function mainWinAcc() {
414
+ return { size: 0, mtimeMs: 0, total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, effective: 0, byDay: {}, hasUsage: false }
415
+ }
416
+ // 事件时间戳(v4 事件的 time 字段,epoch ms)→ 本地日 key;缺失/非法返回 ''(不进任何日账)
417
+ function dayKeyOfMs(ms) {
418
+ var t = Number(ms)
419
+ if (!isFinite(t) || t <= 0) return ''
420
+ var d = new Date(t)
421
+ if (isNaN(d.getTime())) return ''
422
+ return localDayKey(d)
423
+ }
424
+ // 把 buf 内全部完整帧的 assistant/message usage 累加进 acc;返回「结算到」的相对偏移。
425
+ // 帧序旧→新扫(追加写):中间坏帧跳过但照样结算掉(坏帧永远读不回来,别让水位卡住);
426
+ // 最后一段解压失败 = 尾部半帧(写入进行中)→ 停在它的起点,下轮写完整后重读。
427
+ function mainWinAggregateFrames(acc, buf) {
428
+ var offs = []
429
+ var at = buf.indexOf(ZSTD_MAGIC)
430
+ while (at >= 0) { offs.push(at); at = buf.indexOf(ZSTD_MAGIC, at + 1) }
431
+ var consumed = 0
432
+ for (var k = 0; k < offs.length; k++) {
433
+ var end = k + 1 < offs.length ? offs[k + 1] : buf.length
434
+ var lines = null
435
+ try {
436
+ lines = zstdDecompressSync(buf.subarray(offs[k], end)).toString('utf8').split('\n')
437
+ } catch (_) {
438
+ if (k === offs.length - 1) break // 尾部半帧:不结算,下轮从它的起点重读
439
+ consumed = end; continue
440
+ }
441
+ for (var i = 0; i < lines.length; i++) {
442
+ if (!lines[i]) continue
443
+ var e
444
+ try { e = JSON.parse(lines[i]) } catch (_) { continue }
445
+ if (!e || e.type !== 'assistant/message') continue
446
+ var u = e.data && e.data.usage
447
+ if (!u) continue
448
+ var inp = numOr0(u.inputTokens), outp = numOr0(u.outputTokens)
449
+ var cr = numOr0(u.cacheReadTokens), cw = numOr0(u.cacheWriteTokens)
450
+ var tot = numOr0(u.totalTokens) || (inp + outp + cr + cw)
451
+ var eff = inp + outp + cw
452
+ acc.total += tot; acc.input += inp; acc.output += outp
453
+ acc.cacheRead += cr; acc.cacheWrite += cw; acc.effective += eff
454
+ acc.hasUsage = true
455
+ var dk = dayKeyOfMs(e.time)
456
+ if (dk) {
457
+ var cell = acc.byDay[dk] || { t: 0, e: 0, cr: 0 }
458
+ cell.t += tot; cell.e += eff; cell.cr += cr
459
+ acc.byDay[dk] = cell
460
+ }
461
+ }
462
+ consumed = end
463
+ }
464
+ return consumed
465
+ }
466
+ // 主窗口消耗读取入口。cacheStore 缺省(undefined/非对象)→ 不缓存每次全量读(测试/老宿主兜底)。
467
+ // 出参 byDay 单元为 { t, e, cr }(t=总量含缓存读,e=有效,cr=缓存读)——范围裁剪要按日重算
468
+ // 三个口径,故比 run 日账 {t,e} 多带一个缓存读分量;出参是副本,改出参不污染缓存。
469
+ export function readMainWindowUsage(sessionId, sessionsRoot, range, cacheStore) {
470
+ try {
471
+ if (!sessionId) return null
472
+ var log = findRunLog(sessionId, sessionsRoot)
473
+ if (!log) return null
474
+ var st = null
475
+ try { st = fsNode.statSync(log) } catch (_) { return null }
476
+ var store = (cacheStore && typeof cacheStore === 'object') ? cacheStore : null
477
+ var key = String(sessionId)
478
+ var acc = store ? store[key] : null
479
+ if (acc && st.size < acc.size) acc = null // 日志轮换/截断(文件变小)→ 缓存作废全量重读
480
+ if (!acc) {
481
+ var buf0 = readLogBytes(log, MAINWIN_LOG_CAP)
482
+ if (!buf0) return null
483
+ var base0 = st.size - buf0.length // 封顶截断时只丢最老帧(buf 起点对应绝对偏移 base0)
484
+ acc = mainWinAcc()
485
+ acc.size = base0 + mainWinAggregateFrames(acc, buf0)
486
+ acc.mtimeMs = st.mtimeMs
487
+ if (store) store[key] = acc
488
+ } else if (st.size > acc.size) {
489
+ // 增量尾读:只读新增字节段 [acc.size, st.size);读取失败保留旧聚合值出数(降级不闪断,下轮再试)
490
+ var buf1 = readLogBytes(log, st.size - acc.size)
491
+ if (buf1) { acc.size += mainWinAggregateFrames(acc, buf1); acc.mtimeMs = st.mtimeMs }
492
+ }
493
+ // st.size === acc.size → 零读直接用缓存
494
+ if (!acc.hasUsage) return null // 还没有任何 usage 事件 → 静默(客户端不渲染该行)
495
+ var out = { total: 0, effective: 0, cacheRead: 0, byDay: {} }
496
+ if (rangeOn(range)) {
497
+ for (var dk in acc.byDay) {
498
+ if (!Object.prototype.hasOwnProperty.call(acc.byDay, dk)) continue
499
+ if (!dayInRange(dk, range)) continue
500
+ var c = acc.byDay[dk]
501
+ out.byDay[dk] = { t: c.t, e: c.e, cr: c.cr }
502
+ out.total += c.t; out.effective += c.e; out.cacheRead += c.cr
503
+ }
504
+ } else {
505
+ out.total = acc.total; out.effective = acc.effective; out.cacheRead = acc.cacheRead
506
+ for (var dk2 in acc.byDay) {
507
+ if (!Object.prototype.hasOwnProperty.call(acc.byDay, dk2)) continue
508
+ var c2 = acc.byDay[dk2]
509
+ out.byDay[dk2] = { t: c2.t, e: c2.e, cr: c2.cr }
510
+ }
511
+ }
512
+ return out
513
+ } catch (e) {
514
+ // 展示统计绝不影响轮询:任何意外一律记日志降级为「无该行」
515
+ console.error('[task-board] readMainWindowUsage failed (' + sessionId + '):', String(e))
516
+ return null
517
+ }
518
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-agent-board",
3
3
  "description": "DeepSeek Harness 智能看板:任务卡片 + 一次性 Worker/Verifier 派发 + 依赖调度 + 管线分档 + Team 模式(歧义上报主窗口裁决)",
4
- "version": "1.7.3",
4
+ "version": "1.7.4",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
7
7
  "scripts": {
@@ -1417,7 +1417,7 @@ test('aggregateUsageSummary: 空任务/无 usage 任务 → 全零 + 空 Top', (
1417
1417
  assert.equal(s.total, 0); assert.equal(s.input, 0); assert.equal(s.output, 0); assert.equal(s.cacheRead, 0)
1418
1418
  assert.deepEqual(s.byModel, {}); assert.deepEqual(s.topTasks, [])
1419
1419
  assert.deepEqual(s.byDay, {}) // 日账缺省空对象(老看板/无 usage 不炸)
1420
- assert.deepEqual(aggregateUsageSummary(undefined), { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, effective: 0, byModel: {}, byModelEff: {}, byDay: {}, topTasks: [] })
1420
+ assert.deepEqual(aggregateUsageSummary(undefined), { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, effective: 0, byModel: {}, byModelEff: {}, byDay: {}, byDayFull: {}, topTasks: [] })
1421
1421
  })
1422
1422
 
1423
1423
  test('aggregateUsageSummary: 总量/输入输出缓存拆分累加 + 按模型小计合并', () => {
@@ -1865,12 +1865,95 @@ test('Token 日账接线:dispatch 记 byDay 双指标(本地日)+ 仪表
1865
1865
  assert.match(cli, /'近 7 天(有效消耗)'/)
1866
1866
  assert.match(cli, /k\.slice\(5\) \+ ':有效 ' \+ String\(v\)/) // 条形 title:MM-DD:有效 N tok
1867
1867
  assert.match(cli, /含缓存读共 ' \+ String\(c\.t\) \+ ' tok'/) // 条形 title 补总量对照
1868
- assert.match(cli, /口径:仅看板派发的 Worker\/Verifier run 消耗,不含主窗口对话/) // 口径边界明示(不含主窗口)
1868
+ assert.match(cli, /口径:累计与 Top 8 仅看板派发的 Worker\/Verifier run 消耗/) // 口径边界明示(累计/Top8 仅 run 口径)
1869
+ assert.match(cli, /主窗口行=本会话对话消耗,与看板派发口径并列不混入/) // 主窗口行并列不混入(task-muwsol23 起单列展示)
1869
1870
  assert.match(cli, /主数字(模型分布 \/ Top 8)与「今日」「近 7 天」均为有效消耗口径/) // 主数字口径统一(task-muwq9u04)
1870
1871
  assert.match(cli, /其中缓存读 ' \+ String\(x\.cacheRead \|\| 0\)/) // Top8 title 补 缓存读 拆分(合计/有效进 title)
1871
- assert.match(cli, /background: isToday \? C\.brand : C\.nested/) // 今天高亮 brand、其余浅底
1872
+ assert.match(cli, /background: legacy \? C\.border : \(isToday \? C\.brand : C\.nested\)/) // 旧口径日灰柱、今天高亮 brand、其余浅底
1872
1873
  assert.match(cli, /hasDayData \? React\.createElement/) // 7 天全空不渲染该区
1873
- assert.match(cli, /'(近似:老日账只有总量)'/) // e 不可知 → 标 ~ 近似,不冒充有效值
1874
+ // e 不可知的旧口径日:不拿总量冒充有效——今日「—」/ 柱形矮灰柱 + tooltip 标明(task-muwsnyqv ③,
1875
+ // 旧「标 ~ 近似」分支已除,见三连修测试的反向断言)
1876
+ assert.match(cli, /旧口径数据(仅总量/)
1877
+ })
1878
+
1879
+ // ===== Token 口径三连修(task-muwsnyqv):byDay 闭区间 / byDayFull 固定口径 / 旧口径不冒充 =====
1880
+ test('Token 三连修①:byDay 范围 to 闭区间 + 入选 run 重建日账(from=to=今天 时 byDay 有今天)', () => {
1881
+ // 活体实证形状:range{from:'2026-10-05',to:'2026-10-06'} 时 total/effective 含今天而 byDay 整丢——
1882
+ // 病根是 picked 路径合成的 u 丢 byDay/updatedAt。修复后按入选 run 的 at 本地日逐日重建。
1883
+ const t1 = mkTask({
1884
+ id: 'a', title: 'A',
1885
+ runs: [
1886
+ mkRun(localIso(2026, 10, 5, 8, 0), 'm-x', U(3, 4, 100, 5)), // 10-05:t=112 e=12
1887
+ mkRun(localIso(2026, 10, 6, 9, 0), 'm-x', U(6, 15, 152, 5)) // 10-06(今天):t=178 e=26
1888
+ ],
1889
+ usage: { total: 290, input: 9, output: 19, cacheRead: 252, cacheWrite: 10, runs: 2, models: { 'm-x': 290 }, updatedAt: localIso(2026, 10, 6, 10, 0), byDay: { '2026-10-05': { t: 112, e: 12 }, '2026-10-06': { t: 178, e: 26 } } }
1890
+ })
1891
+ const s = aggregateUsageSummary([t1], { from: '2026-10-05', to: '2026-10-06' })
1892
+ assert.equal(s.total, 290) // run 级过滤:两天都计入
1893
+ assert.deepEqual(s.byDay, { '2026-10-05': { t: 112, e: 12 }, '2026-10-06': { t: 178, e: 26 } }) // to 端闭区间:10-06 不丢
1894
+ // 两端边界各一条:from=to=边界日 → 当日必在 byDay(run 级重建,日 key 与范围过滤同口径 r.at)
1895
+ assert.deepEqual(aggregateUsageSummary([t1], { from: '2026-10-06', to: '2026-10-06' }).byDay, { '2026-10-06': { t: 178, e: 26 } })
1896
+ assert.deepEqual(aggregateUsageSummary([t1], { from: '2026-10-05', to: '2026-10-05' }).byDay, { '2026-10-05': { t: 112, e: 12 } })
1897
+ // 范围内日账与入选 total 自洽(Σ byDay.t === s.total,run 级重建不丢账)
1898
+ const sum = Object.keys(s.byDay).reduce((n, k) => n + s.byDay[k].t, 0)
1899
+ assert.equal(sum, s.total)
1900
+ // run 级重建的 e 恒为已知有效值(五分量俱全),不出现 null
1901
+ assert.equal(s.byDay['2026-10-06'].e, 26)
1902
+ })
1903
+
1904
+ test('Token 三连修②:byDayFull 未过滤全量与 byDay 并存(今日/近 7 天固定口径数据源)+ 客户端接线', () => {
1905
+ const t1 = mkTask({
1906
+ id: 'a', title: 'A',
1907
+ runs: [
1908
+ mkRun(localIso(2026, 10, 5, 8, 0), 'm-x', U(3, 4, 100, 5)),
1909
+ mkRun(localIso(2026, 10, 6, 9, 0), 'm-x', U(6, 15, 152, 5))
1910
+ ],
1911
+ usage: { total: 290, input: 9, output: 19, cacheRead: 252, cacheWrite: 10, runs: 2, models: { 'm-x': 290 }, updatedAt: localIso(2026, 10, 6, 10, 0), byDay: { '2026-10-05': { t: 112, e: 12 }, '2026-10-06': { t: 178, e: 26 } } }
1912
+ })
1913
+ const s = aggregateUsageSummary([t1], { from: '2026-10-06', to: '2026-10-06' })
1914
+ assert.deepEqual(s.byDay, { '2026-10-06': { t: 178, e: 26 } }) // byDay:范围内
1915
+ assert.deepEqual(s.byDayFull, { '2026-10-05': { t: 112, e: 12 }, '2026-10-06': { t: 178, e: 26 } }) // byDayFull:全量不受裁
1916
+ // 范围外任务也进 byDayFull(固定口径不随范围),但不进 total/byDay
1917
+ const tOut = mkTask({ id: 'out', usage: { total: 50, input: 1, output: 1, cacheRead: 48, cacheWrite: 0, runs: 1, models: {}, updatedAt: localIso(2026, 10, 1, 9, 0) } })
1918
+ const s2 = aggregateUsageSummary([t1, tOut], { from: '2026-10-06', to: '2026-10-06' })
1919
+ assert.equal(s2.total, 178) // 范围外任务不进 total
1920
+ assert.deepEqual(s2.byDay, { '2026-10-06': { t: 178, e: 26 } }) // 也不进范围内 byDay
1921
+ assert.deepEqual(s2.byDayFull['2026-10-01'], { t: 50, e: null }) // 但进 byDayFull(存量兜底归 updatedAt,e 不可知=null)
1922
+ // 无范围:byDayFull 与 byDay 内容一致(parity,老 host 行为逐字不变)
1923
+ const s3 = aggregateUsageSummary([t1])
1924
+ assert.deepEqual(s3.byDayFull, s3.byDay)
1925
+ // 老 number 形态日账:byDayFull 同样 e=null 不猜(不冒充有效值)
1926
+ const legacy = mkTask({ id: 'old', usage: { total: 600, models: {}, byDay: { '2026-10-01': 600 } } })
1927
+ assert.deepEqual(aggregateUsageSummary([legacy]).byDayFull, { '2026-10-01': { t: 600, e: null } })
1928
+ // 纯函数:入参对象未被就地改写
1929
+ assert.deepEqual(t1.usage.byDay, { '2026-10-05': { t: 112, e: 12 }, '2026-10-06': { t: 178, e: 26 } })
1930
+ // 接线(源码级):host 聚合骨架带 byDayFull;客户端今日/近 7 天读 byDayFull,老 host 缺字段退化 byDay
1931
+ const usageSrc = readFileSync(new URL('../lib/usage.mjs', import.meta.url), 'utf8')
1932
+ assert.match(usageSrc, /byDayFull: \{\}/)
1933
+ const cli = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
1934
+ assert.match(cli, /var byDay = \(u\.byDayFull && typeof u\.byDayFull === 'object'\) \? u\.byDayFull : \(\(u\.byDay && typeof u\.byDay === 'object'\) \? u\.byDay : \{\}\)/)
1935
+ })
1936
+
1937
+ test('Token 三连修③:e=null 旧口径日不拿 t 冒充有效(今日「—」/ 灰柱 + tooltip,源码级断言)', () => {
1938
+ const cli = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
1939
+ // 柱形:e===null 的日子走 legacy 灰柱分支,柱高不再吃 t 兜底
1940
+ assert.match(cli, /var legacy = c\.e === null && c\.t > 0/)
1941
+ assert.match(cli, /var v = legacy \? 0 : \(c\.e \|\| 0\)/)
1942
+ assert.match(cli, /':旧口径数据(仅总量 '/) // 柱形 tooltip 标旧口径
1943
+ assert.match(cli, /灰柱 = 旧口径数据(仅总量)/) // 有近 7 天旧口径日时表头图例
1944
+ // maxDay 只按有效值归一(t 不参与,含缓存读的巨柱压不平其他天)
1945
+ assert.match(cli, /if \(dc\.e !== null && dc\.e > maxDay\) maxDay = dc\.e/)
1946
+ // 今日大数字:旧口径日显示「—」+ title 说明,不再 ~t 冒充
1947
+ assert.match(cli, /var todayLegacy = todayCell\.e === null && todayCell\.t > 0/)
1948
+ assert.match(cli, /todayLegacy \? '—' : fmtTokens\(todayEff\)/)
1949
+ assert.match(cli, /今日有效消耗不可知:该日账为旧口径数据(仅总量/)
1950
+ // 反向断言:旧的 t 兜底冒充分支已除(柱形与今日两处都不再有 e===null → 用 t 的路径)
1951
+ assert.equal(/c\.e === null \? c\.t : c\.e/.test(cli), false)
1952
+ assert.equal(/todayApprox \? todayCell\.t : todayCell\.e/.test(cli), false)
1953
+ assert.equal(/(近似:老日账只有总量)/.test(cli), false)
1954
+ // README 双份记录老数据处理口径
1955
+ const pkg = readFileSync(new URL('../README.md', import.meta.url), 'utf8')
1956
+ assert.ok(pkg.includes('旧口径'), 'README 应记录旧口径日(e=null)不拿总量冒充有效的处理')
1874
1957
  })
1875
1958
 
1876
1959
  // ===== 调研 ROI 行 token 有效口径(task-muupnnq5)=====
@@ -0,0 +1,230 @@
1
+ // dsh-agent-board — 主窗口 Token 消耗展示项单测(test/usage-mainwindow.test.mjs,task-muwsol23)
2
+ // 覆盖(对应任务断言①~⑥ + README):
3
+ // ① 增量尾读——同文件追加后只读增量字节段(readSync 字节计数);无变化零读;截断全量重读
4
+ // ② 聚合五分量 + e=input+output+cacheWrite 口径 + byDay 本地日落点 + 尾部半帧不结算(下轮补读不丢不重)
5
+ // ③ 范围裁剪——byDay/累计三分量随 range(本地日闭区间);空范围=无范围(parity);出参副本不污染缓存
6
+ // ④ 隔离红线——mainWindow 不进 Top8/累计/模型归属(挂上后逐字不变);聚合本体永不产出 mainWindow 键
7
+ // ⑤ 失败静默降级——日志不存在/无 usage/垃圾字节/无缓存槽 → null 或旧值,绝不抛
8
+ // ⑥ client/host 接线源码断言——单列行 / 模型分布尾部条(0 不渲染)/ caption / get-tasks 挂载
9
+ import { test } from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+ import fs from 'node:fs'
12
+ import os from 'node:os'
13
+ import path from 'node:path'
14
+ import zlib from 'node:zlib'
15
+ import { readFileSync } from 'node:fs'
16
+ import { readMainWindowUsage, aggregateUsageSummary } from '../index.mjs'
17
+ import { localDayKey } from '../lib/usage.mjs'
18
+
19
+ const zstdOk = typeof zlib.zstdCompressSync === 'function'
20
+ // 一帧追加写:与 v4 日志同形(多帧 zstd 顺序拼接,帧内 jsonl)
21
+ function frameOf(events) { return zlib.zstdCompressSync(Buffer.from(events.map(function (e) { return JSON.stringify(e) }).join('\n') + '\n', 'utf8')) }
22
+ // 主会话日志根:布局与真实一致 <root>/<bucket>/<sid>/session.v4.jsonl.zstd(findRunLog 按桶探测)
23
+ function mkMainLog(sid, chunks) {
24
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-mw-'))
25
+ const dir = path.join(root, 'bucket', sid)
26
+ fs.mkdirSync(dir, { recursive: true })
27
+ const logPath = path.join(dir, 'session.v4.jsonl.zstd')
28
+ if (chunks && chunks.length) fs.writeFileSync(logPath, Buffer.concat(chunks))
29
+ return { root: root, logPath: logPath, append: function (b) { fs.appendFileSync(logPath, b) } }
30
+ }
31
+ // 事件时间戳(epoch ms):本地构造保证落点与 localDayKey 口径一致(与机器时区无关)
32
+ function msOf(y, m, d, h) { return new Date(y, m - 1, d, h === undefined ? 12 : h, 0, 0).getTime() }
33
+ function dayOf(y, m, d, h) { return localDayKey(new Date(msOf(y, m, d, h))) }
34
+ // 一条 assistant/message usage 事件(time 是 v4 事件自带的全序时间戳字段)
35
+ function amEvent(ms, usage) { return { type: 'assistant/message', time: ms, data: { usage: usage } } }
36
+ function U(inp, outp, cr, cw) { return { inputTokens: inp, outputTokens: outp, cacheReadTokens: cr, cacheWriteTokens: cw, totalTokens: inp + outp + cr + cw } }
37
+
38
+ // ===== ① 增量尾读(字节计数)=====
39
+ test('主窗口①:增量尾读——追加后只读新增字节段、无变化零读、截断全量重读(readSync 字节计数)', (t) => {
40
+ if (!zstdOk) { t.skip('zstdCompressSync unavailable'); return }
41
+ const L = mkMainLog('sid-mw1', [frameOf([amEvent(msOf(2026, 10, 5, 10), U(100, 7, 1000, 0))])])
42
+ try {
43
+ const cache = {}
44
+ // 字节计数:node:fs 的默认导出是共享 CJS 对象,包装 readSync 即覆盖 usage.mjs 的 fsNode.readSync
45
+ const origRead = fs.readSync
46
+ let bytes = 0
47
+ fs.readSync = function (fd, buf, off, len, pos) { bytes += len; return origRead.apply(fs, arguments) }
48
+ try {
49
+ const sz1 = fs.statSync(L.logPath).size
50
+ const r1 = readMainWindowUsage('sid-mw1', L.root, null, cache)
51
+ assert.equal(bytes, sz1) // 首轮全量读
52
+ assert.equal(r1.total, 1107); assert.equal(r1.effective, 107); assert.equal(r1.cacheRead, 1000)
53
+ // 第二轮无变化 → 零读直接用缓存(3s 轮询稳态零全量重读的硬保证)
54
+ bytes = 0
55
+ const r1b = readMainWindowUsage('sid-mw1', L.root, null, cache)
56
+ assert.equal(bytes, 0)
57
+ assert.deepEqual(r1b, r1)
58
+ // 追加一帧 → 只读新增字节段(增量尾读),聚合 = 旧账 + 增量(不重复计账)
59
+ const add = frameOf([amEvent(msOf(2026, 10, 6, 9), U(3, 4, 0, 5))])
60
+ L.append(add)
61
+ bytes = 0
62
+ const r2 = readMainWindowUsage('sid-mw1', L.root, null, cache)
63
+ assert.equal(bytes, add.length) // 增量段字节数,不是全文件
64
+ assert.equal(r2.total, 1119) // 1107 + 12(若全量重读重复计账会变 2226)
65
+ assert.equal(r2.effective, 119) // 107 + (3+4+5)
66
+ assert.equal(r2.cacheRead, 1000)
67
+ // 截断(日志轮换/文件变小)→ 缓存作废,全量重读一次
68
+ const fresh = frameOf([amEvent(msOf(2026, 10, 7, 8), U(1, 1, 1, 1))])
69
+ fs.writeFileSync(L.logPath, fresh)
70
+ bytes = 0
71
+ const r3 = readMainWindowUsage('sid-mw1', L.root, null, cache)
72
+ assert.equal(bytes, fresh.length)
73
+ assert.equal(r3.total, 4) // 旧账随轮换清零重算(不是 1119+4)
74
+ assert.equal(r3.effective, 3)
75
+ } finally { fs.readSync = origRead }
76
+ } finally { fs.rmSync(L.root, { recursive: true, force: true }) }
77
+ })
78
+
79
+ // ===== ② 聚合口径 + byDay 日落点 + 尾部半帧 =====
80
+ test('主窗口②:五分量 + e=输入+输出+缓存写 + byDay 本地日落点;尾部半帧不结算(补全后整帧只记一次)', (t) => {
81
+ if (!zstdOk) { t.skip('zstdCompressSync unavailable'); return }
82
+ const d1 = dayOf(2026, 10, 5, 10), d2 = dayOf(2026, 10, 6, 23)
83
+ const f1 = frameOf([
84
+ { type: 'request/context', time: msOf(2026, 10, 5, 9), data: { model: 'm-x' } }, // 非 assistant/message:不计
85
+ amEvent(msOf(2026, 10, 5, 10), U(10, 5, 100, 5)), // d1:t=120 e=20 cr=100
86
+ amEvent(msOf(2026, 10, 6, 23), U(1, 2, 3, 4)) // d2:t=10 e=7 cr=3
87
+ ])
88
+ const L = mkMainLog('sid-mw2', [f1])
89
+ try {
90
+ const cache = {}
91
+ const r = readMainWindowUsage('sid-mw2', L.root, null, cache)
92
+ assert.equal(r.total, 130) // 120 + 10(总量含缓存读)
93
+ assert.equal(r.effective, 27) // (10+5+5) + (1+2+4):缓存读不进有效口径
94
+ assert.equal(r.cacheRead, 103)
95
+ assert.deepEqual(r.byDay, { [d1]: { t: 120, e: 20, cr: 100 }, [d2]: { t: 10, e: 7, cr: 3 } })
96
+ // 尾部半帧(写入进行中):解压必失败 → 不结算,聚合值原地不动
97
+ const good = frameOf([amEvent(msOf(2026, 10, 6, 23, 30), U(2, 2, 0, 1))]) // t=5 e=5
98
+ L.append(good.subarray(0, good.length - 6))
99
+ const r2 = readMainWindowUsage('sid-mw2', L.root, null, cache)
100
+ assert.equal(r2.total, 130)
101
+ assert.equal(r2.effective, 27)
102
+ // 补全该帧 → 从半帧起点重读,整帧恰好记一次(不丢不重)
103
+ L.append(good.subarray(good.length - 6))
104
+ const r3 = readMainWindowUsage('sid-mw2', L.root, null, cache)
105
+ assert.equal(r3.total, 135)
106
+ assert.equal(r3.effective, 32)
107
+ assert.deepEqual(r3.byDay[d2], { t: 15, e: 12, cr: 3 })
108
+ } finally { fs.rmSync(L.root, { recursive: true, force: true }) }
109
+ })
110
+
111
+ // ===== ③ 范围裁剪 =====
112
+ test('主窗口③:范围裁剪——byDay/累计三分量随 range(本地日闭区间);空范围=无范围(parity);出参副本不污染缓存', (t) => {
113
+ if (!zstdOk) { t.skip('zstdCompressSync unavailable'); return }
114
+ const d1 = dayOf(2026, 10, 5, 10), d2 = dayOf(2026, 10, 6, 23)
115
+ const L = mkMainLog('sid-mw3', [frameOf([
116
+ amEvent(msOf(2026, 10, 5, 10), U(10, 5, 100, 5)),
117
+ amEvent(msOf(2026, 10, 6, 23), U(1, 2, 3, 4))
118
+ ])])
119
+ try {
120
+ const cache = {}
121
+ const all = readMainWindowUsage('sid-mw3', L.root, null, cache)
122
+ assert.equal(all.total, 130)
123
+ // 范围只留 d2:三分量由入选日重算,byDay 只剩 d2(与 run 口径同一 dayInRange 两端闭区间)
124
+ const cropped = readMainWindowUsage('sid-mw3', L.root, { from: d2, to: d2 }, cache)
125
+ assert.equal(cropped.total, 10)
126
+ assert.equal(cropped.effective, 7)
127
+ assert.equal(cropped.cacheRead, 3)
128
+ assert.deepEqual(cropped.byDay, { [d2]: { t: 10, e: 7, cr: 3 } })
129
+ // 范围全落空:0 值对象(不是 null——会话确有消耗,只是都在范围外)
130
+ const miss = readMainWindowUsage('sid-mw3', L.root, { from: '2020-01-01', to: '2020-01-02' }, cache)
131
+ assert.deepEqual(miss, { total: 0, effective: 0, cacheRead: 0, byDay: {} })
132
+ // 空范围与不传等价(parity,与 aggregateUsageSummary 同一约定)
133
+ assert.deepEqual(readMainWindowUsage('sid-mw3', L.root, { from: '', to: '' }, cache), all)
134
+ // 缓存里存的是未裁剪全量:范围请求之后再读全量仍正确(裁剪发生在出参,不毁缓存)
135
+ const again = readMainWindowUsage('sid-mw3', L.root, null, cache)
136
+ assert.equal(again.total, 130)
137
+ // 出参是副本:改出参 byDay 不污染缓存聚合
138
+ cropped.byDay[d2].t = 999999
139
+ assert.equal(readMainWindowUsage('sid-mw3', L.root, null, cache).total, 130)
140
+ } finally { fs.rmSync(L.root, { recursive: true, force: true }) }
141
+ })
142
+
143
+ // ===== ④ 隔离红线(parity)=====
144
+ test('主窗口④:隔离红线——挂上 mainWindow 后 Top8/累计/模型归属逐字不变;聚合本体永不产出 mainWindow 键', () => {
145
+ const tasks = [
146
+ { id: 'a', title: 'A', usage: { total: 290, input: 9, output: 19, cacheRead: 252, cacheWrite: 10, runs: 2, models: { 'm-x': 290 }, updatedAt: '2026-10-06T02:00:00.000Z', byDay: { '2026-10-05': { t: 112, e: 12 }, '2026-10-06': { t: 178, e: 26 } } } },
147
+ { id: 'b', title: 'B', usage: { total: 30, input: 1, output: 1, cacheRead: 28, cacheWrite: 0, runs: 1, models: { 'm-y': 30 }, updatedAt: '2026-10-06T03:00:00.000Z', byDay: { '2026-10-06': { t: 30, e: 2 } } } }
148
+ ]
149
+ const before = aggregateUsageSummary(tasks)
150
+ // host 接线形态(rpc.mjs get-tasks):聚合完成后才挂 mainWindow
151
+ const after = aggregateUsageSummary(tasks)
152
+ after.mainWindow = { total: 999999, effective: 8888, cacheRead: 7777, byDay: { '2026-10-06': { t: 999999, e: 8888, cr: 7777 } } }
153
+ // Top8 / 本看板累计 / 模型归属逐字不变(隔离红线)
154
+ assert.deepEqual(after.topTasks, before.topTasks)
155
+ assert.equal(after.total, before.total)
156
+ assert.equal(after.effective, before.effective)
157
+ assert.equal(after.cacheRead, before.cacheRead)
158
+ assert.deepEqual(after.byModel, before.byModel)
159
+ assert.deepEqual(after.byModelEff, before.byModelEff)
160
+ assert.deepEqual(after.byDay, before.byDay)
161
+ assert.deepEqual(after.byDayFull, before.byDayFull)
162
+ // aggregateUsageSummary 本体永不产出 mainWindow 键(隔离在聚合层就成立,范围路径同样)
163
+ assert.equal('mainWindow' in before, false)
164
+ assert.equal('mainWindow' in aggregateUsageSummary(tasks, { from: '2026-10-06', to: '2026-10-06' }), false)
165
+ // 源码级:mainWindow 挂载必须发生在 run 口径聚合之后
166
+ const rpc = readFileSync(new URL('../lib/rpc.mjs', import.meta.url), 'utf8')
167
+ const iAgg = rpc.indexOf('d.usageSummary = aggregateUsageSummary(d.tasks, args && args.range)')
168
+ const iMw = rpc.indexOf('d.usageSummary.mainWindow =')
169
+ assert.ok(iAgg >= 0 && iMw > iAgg, 'mainWindow 必须在 aggregateUsageSummary 之后挂载(隔离顺序)')
170
+ })
171
+
172
+ // ===== ⑤ 失败静默降级 =====
173
+ test('主窗口⑤:失败静默降级——日志不存在/无 usage/垃圾字节/无缓存槽 → null 或旧值,绝不抛', (t) => {
174
+ if (!zstdOk) { t.skip('zstdCompressSync unavailable'); return }
175
+ // 日志不存在 / 空会话 id → null(客户端不渲染该行)
176
+ assert.equal(readMainWindowUsage('no-such-session-' + Date.now(), undefined, null, {}), null)
177
+ assert.equal(readMainWindowUsage('', undefined, null, {}), null)
178
+ // 只有非 usage 事件 → null(不渲染,与 readRunUsage 的 hasUsage 同哲学)
179
+ const L1 = mkMainLog('sid-nouse', [frameOf([{ type: 'step/end', time: Date.now(), data: {} }])])
180
+ try { assert.equal(readMainWindowUsage('sid-nouse', L1.root, null, {}), null) } finally { fs.rmSync(L1.root, { recursive: true, force: true }) }
181
+ // 垃圾字节(非 zstd 帧)→ null,不抛
182
+ const L2 = mkMainLog('sid-garbage', null)
183
+ fs.writeFileSync(L2.logPath, Buffer.from('not a zstd stream at all'))
184
+ try { assert.equal(readMainWindowUsage('sid-garbage', L2.root, null, {}), null) } finally { fs.rmSync(L2.root, { recursive: true, force: true }) }
185
+ // 无 cacheStore(undefined)也能工作:每次全量读,不炸(老宿主/直接调用兜底)
186
+ const L3 = mkMainLog('sid-nocache', [frameOf([amEvent(msOf(2026, 10, 5, 10), U(1, 1, 0, 0))])])
187
+ try {
188
+ const r = readMainWindowUsage('sid-nocache', L3.root, null, undefined)
189
+ assert.equal(r.total, 2)
190
+ assert.equal(r.effective, 2)
191
+ } finally { fs.rmSync(L3.root, { recursive: true, force: true }) }
192
+ })
193
+
194
+ // ===== ⑥ client/host 接线(源码级断言)=====
195
+ test('主窗口⑥:接线(源码级)——get-tasks 挂载 / 增量尾读三态 / 单列行 + 模型分布尾部条 + caption', () => {
196
+ const rpc = readFileSync(new URL('../lib/rpc.mjs', import.meta.url), 'utf8')
197
+ const usage = readFileSync(new URL('../lib/usage.mjs', import.meta.url), 'utf8')
198
+ const idx = readFileSync(new URL('../index.mjs', import.meta.url), 'utf8')
199
+ const cli = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8') // 组装产物(pretest 已重建)
200
+ // host:get-tasks 在 run 口径聚合之后挂载 mainWindow(带统计范围 + 增量尾读缓存)
201
+ assert.match(rpc, /readMainWindowUsage\(sid, sessionsRoot, args && args\.range, state\.mainWindowUsageCache\)/)
202
+ assert.match(rpc, /if \(__mw\) d\.usageSummary\.mainWindow = __mw/) // null 不挂字段(客户端静默不渲染)
203
+ assert.match(idx, /mainWindowUsageCache: \{\}/) // 缓存本体在共享 state
204
+ // usage.mjs:增量尾读三态 + 半帧不结算
205
+ assert.match(usage, /export function readMainWindowUsage\(sessionId, sessionsRoot, range, cacheStore\)/)
206
+ assert.match(usage, /if \(acc && st\.size < acc\.size\) acc = null/) // 变小/轮换 → 全量重读
207
+ assert.match(usage, /st\.size > acc\.size/) // 变大 → 增量臂
208
+ assert.match(usage, /readLogBytes\(log, st\.size - acc\.size\)/) // 只读新增字节段
209
+ assert.match(usage, /if \(k === offs\.length - 1\) break/) // 尾部半帧不结算
210
+ // client:缺字段静默降级 + 累计行下方单列行
211
+ assert.match(cli, /u\.mainWindow && typeof u\.mainWindow === 'object'/)
212
+ assert.match(cli, /'主窗口(本会话):今日有效 '/)
213
+ assert.match(cli, /' · 累计 '/)
214
+ assert.match(cli, /'(缓存读 '/)
215
+ // client:模型分布尾部追加「主窗口(对话)」(0 不渲染,固定尾部不参与排序)
216
+ assert.match(cli, /label: '主窗口(对话)'/)
217
+ assert.match(cli, /mw && mw\.effective > 0/)
218
+ // caption:并列不混入的口径说明
219
+ assert.match(cli, /主窗口行=本会话对话消耗,与看板派发口径并列不混入/)
220
+ })
221
+
222
+ // ===== README 双份记录 =====
223
+ test('主窗口⑦:README 双份记录主窗口行口径(增量尾读 / 隔离红线 / 范围裁剪 / 静默降级)', () => {
224
+ const pkg = readFileSync(new URL('../README.md', import.meta.url), 'utf8')
225
+ const root = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8')
226
+ assert.equal(pkg, root) // 两份 README 必须字节一致(npm run sync-readme 的约束)
227
+ for (const s of ['主窗口(本会话)', '主窗口(对话)', '增量尾读', '不进 Top 8', '不含主窗口对话', '静默降级']) {
228
+ assert.ok(pkg.includes(s), 'README 应记录主窗口行口径:' + s)
229
+ }
230
+ })