dsh-agent-board 1.7.1 → 1.7.3

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,8 +102,14 @@ dsh plugin --profile web remove dsh-agent-board
102
102
 
103
103
  ### 仪表盘(Token 消耗)
104
104
 
105
- - 仪表盘视图新增「Token 消耗」区:本看板累计总量 + 输入 / 输出 / 缓存读(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`)
106
- - 数据来源:每次 Worker/Verifier run 结算时读该 run 的 v4 会话日志(`~/.dsh/sessions/*/<runId>/session.v4.jsonl.zstd`),把 `assistant/message` 事件的 `usage`(`inputTokens` / `outputTokens` / `cacheReadTokens` / `cacheWriteTokens` / `totalTokens`,字段形状以真实日志为准)按 zstd 帧逐帧累加到任务 `usage`(含按模型小计与 `runs` 计数,多轮重跑/驳回重做自动累加),`get-tasks` 再现算 board 级 `usageSummary`(总量 / 按模型 / Top8,不落盘额外表)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」
105
+ - 仪表盘视图新增「Token 消耗」区:本看板累计**有效 / 缓存读 / 合计**三分量 + 输入 / 输出(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情,title 附「有效 N / 缓存读 M」拆分);大数字为**今日有效消耗**(本地日口径,无日账则显示 0,旁附「含缓存读共 X」小字对照,有效值不可知时以总量近似并标 `~`)、旁附「累计(本看板)」三分量小字,下方「近 7 天」迷你条形按日展示近 7 天**有效消耗**(今天高亮,title 补总量对照,7 天全为 0 时不渲染);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`);区底固定一行口径 caption:**仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话**
106
+ - 口径要点:**有效消耗 = 输入 + 输出 + 缓存写(不含缓存读)**。实证本板累计 10.3M 里缓存读占 9.7M(94%)、有效消耗仅 605K——总量被缓存读撑高约 17 倍,因此大数字与「近 7 天」一律用有效值,缓存读单列可查(差额一眼可见),避免「仪表盘与真实消耗差太多」
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(选范围即渲染,不等下一次任意刷新)
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 已明示)
107
113
  - 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判
108
114
  - 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
109
115
  - 统计区新增「调研 ROI」对比行:resolved/archived 卡按有无调研注入分组现算卡数 / 平均执行时长 / 平均 token(双组总样本 ≥4 才渲染,无调研组明显更慢时数字 warn 色提示)
@@ -129,32 +135,51 @@ draft → pending → in-progress → verifying → resolved → archived
129
135
  - **依赖调度**:`dependsOn` 声明依赖(DFS 环检测),依赖全部完成后才会被派发,串行链路自动编排
130
136
  - **管线分档**:`full`(执行+验证)/ `work`(只做不验)/ `direct`(不进池,主窗口直接处理),创建时按规则自动分类、可手动覆盖
131
137
  - **硬性验收**:`acceptance` 字段写验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑
132
- - **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]`);进行中的任务持有文件锁,派发器发现候选与活动任务 touches 重叠就跳过本轮(卡片显示 `🔒 等文件释放`,详情页列出在等谁),锁在提交验收/完成后自动释放——避免并行 Worker 改同一批文件互踩。手动「派发」遇到冲突会列出冲突任务,确认后才以 `force` 越权派发
138
+ - **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]`);持有文件锁的任务(`in-progress`/`verifying`)与候选 touches 重叠就跳过本轮(卡片显示 `🔒 等文件释放`,详情页列出在等谁),**锁随工作态**:状态流转到已完成(`resolved`)即放锁,`cancelled`/归档同样不再持锁(归档回归纯收纳动作、不再是释放点)——锁只护「正在写」的阶段,「验收后-提交前」的窗口期由主窗口「回执到即提交」纪律 + 史诗 post-hook 承接,不用长持锁把整批串行化。避免并行 Worker 改同一批文件互踩;手动「派发」遇到冲突会列出冲突任务,确认后才以 `force` 越权派发
133
139
  - **里程碑进展通道**:Worker 每完成一个可验证的里程碑,可调用 `board_report`(`kind: "progress"`,`question` 写一行进展摘要 ≤200 字符)上报——进行中的卡片显示「📈 最近进展 · 相对时间」(覆盖式只留最新一条),详情页消息流保留全部 progress 条目
134
140
  - **防表演式汇报**:进展契约只写在 Worker prompt 里、且要求「有实际产物/结论才报」(禁止定时汇报);progress **静默不通知主窗口**(不进回执聚合),也不写 `history` 流转记录,避免刷屏
135
- - **子任务**:父子层级 + 上下文继承 + 父任务自动流转 + 级联归档(僵尸态出清:`archive-task` 对「无活跃 run 的 in-progress」——典型如被 parentKick 推进后子任务已全部归档的史诗——直接放行,有活跃 run 的仍拒)
141
+ - **子任务**:父子层级 + 上下文继承 + 父任务自动流转 + 级联归档(僵尸态出清:`archive-task` 对「无活跃 run 的 in-progress」——典型如被 parentKick 推进后子任务已全部归档的史诗——直接放行,有活跃 run 的仍拒);**归档子任务仍计入史诗进度并在详情留档可见**——进度分子口径 `settled = resolved | cancelled | archived`、分母也含归档,归档一张子卡不会再让史诗进度从 `0/10` 退化成 `0/9`(进度只增不减),全归档的父卡也照常显示徽章;详情子任务清单不排归档行(灰化 + 行尾「已归档」徽章 + 沉底排序,点击仍可进子卡看留档)
136
142
  - **死会话零打扰**:15s 派发心跳对无活 root 的会话板直接跳过(不读盘/不写盘/零日志,会话重开后自动恢复派发);`create-task` 拒绝不完整 sessionId(防裸短 id 建出幻影板)
137
143
  - **面板轮询渲染短路**:`get-tasks` 响应附 `tasksHash`,任务列表无变化时前端跳过重渲染(usage/池状态等轻量字段照常刷新)
138
- - 卡片识别层:父卡显示「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条(有在跑子任务时附「▸ 在跑:标题」行);子卡标题下显示「↳ 父任务标题」;依赖未满足的待办卡底部灰字「⛓ 等待「依赖标题」」(childStats 由 host 现算,缺字段一律不渲染)
139
- - 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,点击直达子卡详情,标题行汇总 resolved/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
144
+ - **看板抽屉稳定性(isRoot 蝶变防抖)**:生成开始/结束瞬间 agents 树重建会让 `isRoot` 出现单次瞬态 `false`——现在需**连续 3 次(≈9s)false** 才收起抽屉(会话生命周期内「曾确认 true」粘滞),看板不再在生成状态切换时突然消失;从未确认 root 的子代理会话仍即时隐藏入口,保护语义不变
145
+ - 卡片识别层:父卡显示「📦 史诗 · settled/total」徽章 + 3px 迷你进度条(`settled = resolved | cancelled | archived`,total 同口径含归档;有在跑子任务时附「▸ 在跑:标题」行);子卡标题下显示「↳ 父任务标题」;依赖未满足的待办卡底部灰字「⛓ 等待「依赖标题」」(childStats 由 host 现算,缺字段一律不渲染)
146
+ - 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,**含已归档留档**——归档行灰化 + 行尾「已归档」徽章并沉底,点击直达子卡详情,标题行汇总 settled/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
147
+ - **可选 hooks(史诗可选点位:前置/后置各是一次真实 agent 运行)**:`epic.hooks = { pre: { enabled, prompt, state, runId }, post: 同构 }`——**前置**让子任务具备开跑条件(串行闸门:未完成前该史诗的子任务一张都不派),**后置**把这批活收口(全部子任务了结后跑一遍,完成后史诗才转验证中);prompt 只给薄框架契约,具体动作由 hook agent **自行决策**,**吃不准就走歧义上报**(失败即转阻塞等人裁决:重试/跳过/放弃,不自动重跑)。hook run 记在 `epic.runs[]`(role `hook-pre`/`hook-post`),卡片带相位徽章(`⏳ 前置准备中` / `🧪 收尾中` / `⚠️ hook 失败待裁决`,无 hooks 的老卡零渲染),详情页 hooks 区可编辑 enabled+prompt、查看 state/耗时并跳转 run 会话、失败一键跳裁决区;**hooks 仅主窗口可设**(工具与 RPC 双通道门禁),commit/push 不入任何默认形态
140
148
  - **删除通道(真删,无 undo)**:`delete-task` RPC(卡片 hover 垃圾桶按钮 / 详情页「删除」按钮,均先 `confirm('删除不可恢复,确认删除「标题」?')`)+ `batch-op op='delete'`(多选模式底部「批量删除」,同样 confirm)。状态门禁:**草稿/待办/阻塞可删**;进行中/验证中拒绝并提示先用 `terminate-agent` 终止(避免在跑的 run 变孤儿);已完成/取消引导改用归档(`archive-task`,留档可检索);有**未归档子任务**时拒删(防 `parentId` 悬空破坏父任务自动流转);已归档任务幂等返回 ok。是真删(从 `tasks` 数组移除),因此**不产生 `batch-undo` 撤销快照**(批量条对 delete 不显示「↩️ 撤销」),删除操作在 host 端 `console.error` 留一行日志便于溯源
141
149
  - **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约
142
150
  - **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
151
+ - **开关 `epicSplit`(史诗拆分总开关)**(⚙️ 入池配置弹层「**功能**」小节,默认**开**):关掉只停**引导**——Team 提示词第 6 条拆分条款整条不注入、`create-task`/`task_create` 响应不再附 `suggestSplit` 软提示;**机制不禁**(显式传 `parentId` 建子卡、史诗自动收口与 hooks 状态机照常工作,明确要拆时不受阻);老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)。工具描述里的粒度契约是静态工具定义(随 request header 快照),不在开关范围
143
152
 
144
153
  ### 一次性派发(v74 去池化)
145
154
 
146
- - 每个任务 spawn 一个**一次性子代理**(Worker/Verifier),上下文全量注入 prompt,做完即销毁——无常驻池、无池化状态残留
147
- - **预研上下文注入(contextFiles/contextNotes)**:主窗口调研时读过的文件与笔记,由 host 在派发时读盘取最新内容,经「上下文注入」区块提供给 Worker/Verifier(不混进 user prompt);预算口径单文件 8KB、总包 40KB;UI 侧调研门禁——full/work 且声明了 touches 却未附调研的卡片亮「⚠️ 无调研」徽章,详情页「调研注入」区列 files 清单 + notes 字数(无则明示)
148
- - **锚点行段**:`contextFiles` 支持 `path:L2350-L2420` / `path:L2350` 行段语法(只认尾部 `:L<行号>`,兼容 Windows 盘符),只注入该段(段长上限 400 行,超出截断并标注);锚点无效(越界/写法错)自动回退头部注入并标注「锚点无效,已回退头部」
149
- - **截断结构索引**:头部注入被预算截断时,标注升级为「截断:共 N 行,已注入 1–M 行」,并附结构索引块(JS/TS 顶层函数/类/箭头赋值、Markdown 标题及行号,上限 40 条)——Worker 照索引用锚点语法补读目标段即可,不用全文盘点
150
- - **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时读包失败落任务「最近失败」行,不再静默
155
+ - 每个任务 spawn 一个**一次性子代理**(Worker/Verifier),上下文全量注入 prompt,做完即销毁——无常驻池、无池化状态残留(Worker 的可续跑形态见上一节「可续跑 Worker」,Verifier 与 hook run 恒为一次性)
156
+ - **预研上下文注入(contextFiles/contextNotes,瘦身分离形态)**:主窗口调研时读过的文件与笔记随子代理的**首条 prompt 一次性注入**——调研笔记全文(notes,≤8000 字符)+ **文件清单**(每行「`路径:L起-L止` — 一句用途」);**文件内容本体不进 prompt**,由子代理用 `read` 工具按行号范围按需自取(执行时盘面更新鲜;旧形态「host 读盘取正文注入」既受单文件 8KB/总包 40KB 截断,又随 runtime 快照每轮刷新重发——自治 run 实测 6×48.8K 字符≈白烧 75–100K token);UI 侧调研门禁——full/work 且声明了 touches 却未附调研的卡片亮「⚠️ 无调研」徽章,详情页「调研注入」区列 files 清单 + notes 字数(无则明示)
157
+ - **锚点行段与用途**:`contextFiles` 条目写法「`path:L2350-L2420` / `path:L2350`」+ 可选「` — 一句用途`」(em dash 两侧空格分隔;缺省只给路径行号)——锚点只认尾部 `:L<行号>`(兼容 Windows 盘符),清单里原样带上行号供子代理直接按行段 read;锚点写错(`:L0` / `:L5-L2`)自动剥掉,不误导子代理去读空段
158
+ - **按需自取代替 host 预切段**:派发侧零读盘(不再切行段、不再附结构索引块)——子代理自己 `read(path, offset, limit)` 取需要的那段,清单里给出的行号就是起点;`task_preview_context` / `preview-context` 返回的也是这份瘦身清单(不是文件正文)
159
+ - **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时清单组装失败落任务「最近失败」行,不再静默
151
160
  - **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
152
161
  - 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
153
162
  - 看门狗:运行超时且事件流停滞 → 标记"疑似卡死"(不自动杀,裁决权交主窗口/用户)
154
163
  - 歧义上报:Worker 遇到歧义不猜测,上报等主窗口裁决(任何模式下都通知);裁决后新 Worker 携带裁决答案接手
164
+ - 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
155
165
  - 手动派发:详情页「派发 / 派发验收」按钮可随时手动触发单任务派发(auto 模式补派、manual 模式主通道)
156
166
  - 会话隔离:看板按会话分桶,多会话互不干扰
157
167
 
168
+ ### 可续跑 Worker(continuable,卡1~卡3 已落地,默认开)
169
+
170
+ **一句话**:Worker 从「一次性 run」变成**持久子会话**——超时不再丢现场,重派时原会话**冷复活**接着干;Verifier 与 hook run 仍是一次性。
171
+
172
+ - **continuable 化**:Worker 走 `subagents.startContinuable`(rec 持 `childId`),turn 结束改由 host 事件 `agent/status` 的 `running→idle` 观测(一次性路径本就用 `run.result` 结算,未受影响);`claimedBy` / `t.runs[].id` / 详情页会话跳转的 id 语义不变(仍是子会话 id)
173
+ - **硬超时 interrupt 留存(不销毁)**:失败/硬超时结算时只对子会话发取消信号打断当前 turn——Activation、未认领收件箱、已发布后代全部保留,子会话 idle 后仍可被唤醒;`interrupt` 失败(会话已死/无权限)绝不阻断结算:任务照常回待办,只给该 run 落 `noResume` 标记(重派直接起新 Worker,不空唤醒)
174
+ - **重派冷复活续跑**:命中「上次 continuable Worker 结局=超时/失败」的待办卡时,不 spawn 新会话,改 `subagents.sendMessage(活父 Agent, childId, 断点续跑指令)`——子会话带着上一轮全部上下文复活,先盘点工作树再从断点继续;`t.runs[]` 追加一条 `resume: true` 记录(详情页历史会话按钮带 **↻** 标记),续跑同样计入三连败计数;续跑不可用(`NOT_RESUMABLE` 等)→ 回落全新 Worker,并把原因写进任务消息随首条 prompt 注入
175
+ - **续跑基线(不拿旧文本冒充交付物)**:续跑轮结算只认**基线字节之后**新写的助手文本——一个字没产出就走「空文本按失败」重排,绝不把上一轮(被中断那次)的残留文本当成本轮交付物推进验收
176
+ - **重启 reconcile(找回活跃续跑 Worker)**:host 重启会清空内存里的活跃 run 表,但持久子会话还活着。首轮派发周期对「进行中且无活跃 run」的卡查一次 `listChildren(root)`:仍在列 → 重建 rec 观测(监听器本就在)并重挂两级超时臂、基线取当前日志字节数;不在列 → 视为会话已死,走硬超时等价物(回待办重排 + 留一行流转记录),不占 Worker 并发位
177
+ - **usage 增量计账(按 seq 水位线)**:注意「一个持久子会话被结算多次」是新形态——整份日志全量累加会把前几轮的 token 反复记账(实测同一 childId 结算两次=双倍)。现按 v4 日志事件自带的 `seq` 记水位线(落在 `t.runs[].usageSeq`):每次结算只累加水位线之后的 `assistant/message` 增量,本轮没新增量就一行都不记;one-shot 路径(每 run 独立日志)行为逐字不变
178
+ - **开关 `workerContinuable`(默认开)**:关掉即逐字回退旧的一次性路径(零 `startContinuable`/零 `sendMessage`/零 `interrupt`,结算仍走 `run.result`+`dispose`);Verifier 与 hooks(`hook-pre`/`hook-post`)**保持一次性**,不受该开关影响
179
+ - **结算双通道 + 池韧性(2026-10-06 事故修复)**:continuable 结算有事件通道(`agent/status` 的 running→idle,身份取 `agent.session.id`)与上报通道(`board_report` 落定即收尾)两条入口,任一到达即关账(结局/usage/超时臂三件套,幂等);派发周期自带**幽灵活跃表项 GC**(卡面证据核对回收残留 rec,防残留把派发容量顶到 0 拖死全池)+ 整轮 try/catch 与逐卡隔离(单点异常只作废该卡该轮)+ 去抖 latch 时间戳兜底复位
180
+ - **续跑指令优先级**:断点续跑指令会带上卡上 messages 原文(仲裁/干预/驳回理由),并声明**最新裁决/干预优先于历史原始契约**(冲突以最新为准)——冷复活子会话的历史里没有仲裁答案,不带原文它无从知晓
181
+ - **高优干预实时送达 continuable Worker**:`task_intervene` 对 continuable rec 走宿主投递通道(保留插件 source,下一个 step 边界消费),会话不可用降级 `sendMessage` 冷复活投递,再不行回退「记录注入随重派送达」并在 history 注明
182
+
158
183
  ### 工作模式(三档)
159
184
 
160
185
  看板上一个选择器切换三档工作模式(RPC 单入口 `set-work-mode`,`mode` = `list` / `auto` / `team`):
@@ -169,14 +194,14 @@ draft → pending → in-progress → verifying → resolved → archived
169
194
 
170
195
  - **歧义裁决**:Worker 遇歧义不猜测,一律上报;裁决后新 Worker 携带答案接手(Team 托管档附带 system prompt 派发引导 + 默认草稿护栏)
171
196
  - **Verifier 验收**:`acceptance` 硬性验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑;跨档一致
172
- - **touches 排他**:`touches` 文件级排他锁在活动任务间生效,冲突任务跳过本轮派发,提交验收/完成后释放;跨档一致
197
+ - **touches 排他**:`touches` 文件级排他锁在活动任务间生效,冲突任务跳过本轮派发,**锁随工作态**(`in-progress`/`verifying` 持有;状态流转到 `resolved` 即释放,`cancelled`/归档同样释放——归档不再承担解锁职责);跨档一致
173
198
  - 孤儿回收、看门狗、级联归档、会话隔离同样三档一致
174
199
 
175
200
  Team 托管档独有(调度员体验):
176
201
  - 主窗口 system prompt 注入派发引导(提示词层面建议实质性改动走看板,不硬拦截);引导含**上下文书写提示**——子代理是全新会话、无会话记忆,description 写不够会自行调研跑偏
177
202
  - **默认草稿护栏**:`task_create` / `create-task` 缺省建为草稿(草稿不派发),先把所有任务的 dependsOn、contextNotes/contextFiles 补齐,再逐个 `task_update publish=true` 统一发布;确实要立即派发的单个任务才显式传 `draft:false`
178
203
  - **歧义通知 25s 去抖**:通知延迟 25s 投递,投递前重读看板——歧义已被裁决、或任务已 resolved/archived 就静默跳过(消除主窗口 turn 排队导致的过期回声);同一任务连续多次上报只投最新一条
179
- - 任务完成/阻塞时主窗口收到**批量聚合回执**(45s 窗口或满 5 条聚合,等主窗口空闲再发,不打断对话)
204
+ - 任务完成/阻塞时主窗口收到**批量聚合回执**(45s 窗口或满 5 条聚合,等主窗口空闲再发,不打断对话);⚙️ 设置区「通知」小节有两个回执开关——`notifyDispatch`(⚡ 派发回执:任务被 Worker/Verifier 领走时播报)与 `notifyDone`(✅ 完成回执:完成/阻塞时聚合播报),缺省均**开**(老看板文件缺字段自动补 `true`);**歧义裁决通知不受开关影响**(裁决通道不是回执,任务等人裁决必须提醒)
180
205
 
181
206
  > 兼容:旧的 `set-board-mode` / `set-team-mode` 两个 RPC 原样保留(旧客户端与脚本不受影响),
182
207
  > 内部仍以 `boardMode` + `teamMode` 两个字段落盘,老看板文件无损;`get-tasks` 额外返回派生字段 `workMode` 供 UI 单点读取。
@@ -220,11 +245,11 @@ v1.6.0 起 host 端从单体 index.mjs(1487 行)拆为薄壳 + 7 个领域
220
245
  | 模块 | 域 | 内容 |
221
246
  |---|---|---|
222
247
  | `policy.mjs` | 策略层 | 粒度治理软闸门 + 学习飞轮候选教训(纯函数零状态) |
223
- | `usage.mjs` | 统计 | v4 会话日志定位 / zstd 分帧 / token usage 聚合(纯函数) |
248
+ | `usage.mjs` | 统计 | v4 会话日志定位 / zstd 分帧 / token usage 聚合(纯函数;有效消耗 `effectiveTokens` + `byModelEff` 模型有效分摊 + `byDay` 双指标 `{t,e}` 聚合,兼容老 number 日账;统计范围按任务级 `t.runs[].at` 逐 run 裁切;`sinceSeq` 水位线增量结算——同一持久子会话多次结算不重复计账) |
224
249
  | `session.mjs` | 会话 | root 解析缓存 / 会话 id 归一 / workMode 派生 / runsFor |
225
250
  | `store.mjs` | 持久化 | boardPath / rt / wt 原子落盘 / 跨重启继承 / fileLocks 串行化 / mutateLocked |
226
251
  | `notify.mjs` | 通知 | makeMsg / 歧义 25s 去抖 / 回执聚合 + 空闲门控 / 投递前过滤 |
227
- | `dispatch.mjs` | 派发引擎 | poolCycle / spawnOneShot / settleRun / 两级超时 / 孤儿回收 |
252
+ | `dispatch.mjs` | 派发引擎 | poolCycle / spawnOneShot / settleRun / 两级超时 / 孤儿回收 / 可续跑 Worker(continuable + 重启 reconcile + usage 水位线) |
228
253
  | `rpc.mjs` | 接口层 | RPC 路由 + 13 个 Agent 工具注册 |
229
254
 
230
255
  (store→dispatch 的循环依赖由 `deps.poolCycle` 晚绑定解开;index.mjs 对外 re-export 契约不变。)
package/index.mjs CHANGED
@@ -14,8 +14,8 @@
14
14
  // 共享闭包状态全部收进 apply 内的显式 state 对象,逐模块经参数注入(禁止跨模块隐式引用)。
15
15
  //
16
16
  // 兼容 re-export:对外导出契约不变(单测与历史引用直接 import 自本文件)。
17
- export { TASK_SIZE_CONTRACT, SUGGEST_SPLIT_TEXT, TEAM_SPLIT_RULE, suggestSplitOf, withSplitHint } from './lib/policy.mjs'
18
- export { findRunLog, readRunUsage, aggregateUsageSummary } from './lib/usage.mjs'
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'
19
19
  import { createSession } from './lib/session.mjs'
20
20
  import { createStore } from './lib/store.mjs'
21
21
  import { createNotify } from './lib/notify.mjs'
@@ -32,13 +32,15 @@ export function apply(ctx) {
32
32
  knownSessions: {}, // 心跳驱动的会话集合(>30 分钟无活跃淘汰)
33
33
  teamModeCache: {}, // rt()/set-*-mode 同步,systemPrompt 引导段读取
34
34
  feedbackCache: {}, // 学习飞轮开关缓存(rt()/set-board-config 同步)
35
+ epicSplitCache: {}, // 史诗拆分总开关缓存(rt()/set-board-config 同步,Team 引导段读取)
35
36
  fileLocks: {}, // 每会话一条 promise 链,串行化所有 读-改-写
36
37
  cyclePending: {}, // kickCycle 50ms 去抖
37
- activeRuns: {}, // sid -> { taskId: { run, role, taskId, startedAt, model } }
38
+ activeRuns: {}, // sid -> { taskId: { id, run, role, taskId, startedAt, model, settled, continuable?, childId?, ran? } }
39
+ // id = 该次 run 的子会话 id(一次性=run.id,continuable=childId,语义统一);
40
+ // continuable Worker 无 run(持久子会话),turn 结算靠 agent/status 事件见 dispatch.mjs
38
41
  dispatchedEver: {}, // sid -> { runId: true }(回执判定:区分派发执行 vs 主窗口手动)
42
+ reconcileDone: {}, // sid -> true(可续跑 Worker 重启 reconcile 的 per-host 一次性标记,见 dispatch.mjs)
39
43
  badModels: {}, // 模型熔断坏名单(sid|model → true)
40
- packByChild: {}, // 预研文件注入缓存(按子代理会话 id)
41
- pendingPacks: [], // 首轮竞速认领队列(start() 返回前的首次 prompt 组装)
42
44
  escNotifyTimers: {}, // 歧义通知 25s 去抖(同任务新调度顶替旧调度)
43
45
  receiptBuf: {}, // 回执 45s/满 5 条聚合窗口
44
46
  receiptedKeys: {}, // 回执幂等去重(完成事件指纹,1h TTL)
@@ -53,9 +55,11 @@ export function apply(ctx) {
53
55
  var notify = createNotify(ctx, state, { rt: store.rt, rootForSession: session.rootForSession, withTimeout: session.withTimeout, isDispatched: session.isDispatched })
54
56
  var dispatch = createDispatch(ctx, state, {
55
57
  rt: store.rt, wt: store.wt, mutateLocked: store.mutateLocked, kickCycle: store.kickCycle,
56
- rootForSession: session.rootForSession, sessionCwd: session.sessionCwd,
57
- withTimeout: session.withTimeout, runsFor: session.runsFor, feedbackOn: session.feedbackOn,
58
+ rootForSession: session.rootForSession,
59
+ // sessionCwd 不再注入 dispatch(预研清单瘦身后派发侧不读盘);rpc 侧仍需要(epic 预检路径存在性)
60
+ withTimeout: session.withTimeout, runsFor: session.runsFor, feedbackOn: session.feedbackOn, epicSplitOn: session.epicSplitOn,
58
61
  pushSysNote: notify.pushSysNote, maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
62
+ notifyDispatched: notify.notifyDispatched,
59
63
  })
60
64
  storeDeps.poolCycle = dispatch.poolCycle // 晚绑定收口
61
65
  // 僵尸 epic 出清配套(反馈 n-mutma3mmwceq):archive-task 门禁的活性判定。
@@ -69,6 +73,10 @@ export function apply(ctx) {
69
73
  maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
70
74
  pushSysNote: notify.pushSysNote, sessionCwd: session.sessionCwd, // 调研门禁③:epic 发布预检汇总投递 + 预研路径相对解析根
71
75
  spawnOneShot: dispatch.spawnOneShot, accumulateRunUsage: dispatch.accumulateRunUsage, readContextPack: dispatch.readContextPack,
76
+ // 上报通道结算入口(task-muwkhqf8):board_report/board_verdict 把任务推进到落定态时,
77
+ // 由 rpc 侧顺手对「该任务当前的 continuable Worker rec」补做收尾三件套
78
+ // (关 run 结局 / usage 落账 / 摘超时臂)——治「工具上报完成后 idle 事件没到 → run 永远 running」。
79
+ reportRunSettled: dispatch.settleReportedRun,
72
80
  hasActiveRun: hasActiveRun,
73
81
  })
74
82
 
@@ -103,7 +103,9 @@
103
103
  }
104
104
 
105
105
  // 史诗/依赖/父子识别层(childStats 缺省兼容:host 未返回该字段时一律不渲染相关元素):
106
- // 父卡 = 元信息行首位「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
106
+ // 父卡 = 元信息行首位「📦 史诗 · settled/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
107
+ // settled = resolved | cancelled | archived(含已归档子任务、total 也含),口径同宿主 aggregateChildStats;
108
+ // 老宿主只下发 resolved 时回退读它(缺省兼容)——所以归档子卡不再让徽章从 0/10 退化成 0/9;
107
109
  // 子卡 = 标题下一行小字「↳ 父任务标题」(从 state.tasks 找父卡,找不到回退 shortId);
108
110
  // 依赖未满足的待办卡 = 卡片底部灰字「⛓ 等待「第一个未满足依赖标题」」(多个时附「 等 N 个」)。
109
111
  // ===== 调研门禁·UI:「⚠️ 无调研」徽章判定(纯 client 侧现算,host 不下发该字段)=====
@@ -119,13 +121,43 @@
119
121
  var notes = typeof cx.notes === 'string' ? cx.notes.trim() : ''
120
122
  return files.length === 0 && notes.length === 0
121
123
  }
122
- function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); var cs = (state.childStats && state.childStats[t.id]) || null; var epic = !!(cs && cs.total > 0); var parentT = t.parentId ? getTask(t.parentId) : null; var depWaitTitle = ''; var depWaitN = 0; if (depBlock) { t.dependsOn.forEach(function (id) { var d = getTask(id); if (!d || (d.status !== 'resolved' && d.status !== 'archived')) { depWaitN++; if (!depWaitTitle) depWaitTitle = d && d.title ? d.title : id } }) } var durB = cardDur(t);
124
+ // ===== epic hook 相位徽章(hooks=agent run 的卡片侧可见性,task-muuw56yf)=====
125
+ // 与 host core.normalizeHooks 契约同源:epic.hooks = { pre: { enabled, prompt, state, runId, pending? }, post: 同构 },
126
+ // state ∈ idle | running | done | failed(host 状态机推进,UI 只读展示,不在这里改状态)。
127
+ // 缺省兼容硬约束:卡上无 hooks / 该点位未启用 → 返回 null,老卡渲染逐字不变(零额外 DOM)。
128
+ // 优先级与文案(同一时刻只可能有一个相位有话说):
129
+ // pre|post failed → 「⚠️ hook 失败待裁决」(err 色):史诗已被 host 转 blocked 挂歧义,过程态文案都让位;
130
+ // pre running → 「⏳ 前置准备中」:串行闸门期内,该 epic 的子任务一张都不派;
131
+ // post running → 「🧪 收尾中」:子任务已全部了结、epic 仍 in-progress,等收口 run 归还;
132
+ // idle / done → 不渲染(普通进行中维持「📦 史诗 · x/y」原样,稳定态不加噪音)。
133
+ function hookBadgeOf(t) {
134
+ var h = t && t.hooks
135
+ if (!h) return null
136
+ var pre = (h.pre && h.pre.enabled) ? h.pre : null
137
+ var post = (h.post && h.post.enabled) ? h.post : null
138
+ if ((pre && pre.state === 'failed') || (post && post.state === 'failed')) {
139
+ var ph = (pre && pre.state === 'failed') ? 'pre' : 'post'
140
+ return { txt: '⚠️ hook 失败待裁决', color: C.err, tip: 'hooks.' + ph + '(' + (ph === 'pre' ? '前置准备' : '收口') + ')运行失败:史诗已转阻塞等人裁决(重试/跳过/放弃),详情页可跳裁决区' }
141
+ }
142
+ if (pre && pre.state === 'running') return { txt: '⏳ 前置准备中', color: C.brand, tip: '前置 hook run 正在跑:完成前该史诗的子任务一张都不派(串行闸门)' }
143
+ if (post && post.state === 'running') return { txt: '🧪 收尾中', color: C.brand, tip: '收口 hook run 正在跑:子任务已全部了结,史诗仍 in-progress,收口完成后才转验证中' }
144
+ return null
145
+ }
146
+ function hookPhaseBadge(t) {
147
+ var b = hookBadgeOf(t)
148
+ if (!b) return null
149
+ return React.createElement('span', { title: b.tip, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + b.color + ' 14%, transparent)', color: b.color, fontWeight: 600, whiteSpace: 'nowrap' } }, b.txt)
150
+ }
151
+
152
+ function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); var cs = (state.childStats && state.childStats[t.id]) || null; var csDone = cs ? (typeof cs.settled === 'number' ? cs.settled : cs.resolved) : 0; var epic = !!(cs && cs.total > 0); var parentT = t.parentId ? getTask(t.parentId) : null; var depWaitTitle = ''; var depWaitN = 0; if (depBlock) { t.dependsOn.forEach(function (id) { var d = getTask(id); if (!d || (d.status !== 'resolved' && d.status !== 'archived')) { depWaitN++; if (!depWaitTitle) depWaitTitle = d && d.title ? d.title : id } }) } var durB = cardDur(t);
123
153
  // 无障碍(反馈 n-mut9rzpyc7p1):卡片根以 button 角色进 Tab 序(aria-label=标题+状态),
124
154
  // Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
125
155
  // focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
126
156
  function activate() { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }
127
157
  return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
128
- epic ? React.createElement('span', { title: '史诗父卡:' + cs.resolved + '/' + cs.total + ' 个子任务已完成', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + cs.resolved + '/' + cs.total) : null,
158
+ epic ? React.createElement('span', { title: '史诗父卡:' + csDone + '/' + cs.total + ' 个子任务已了结(完成/取消/归档)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + csDone + '/' + cs.total) : null,
159
+ // hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
160
+ epic ? hookPhaseBadge(t) : null,
129
161
  t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
130
162
  (Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
131
163
  React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
@@ -140,8 +172,8 @@
140
172
  title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)',
141
173
  style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', cursor: 'pointer', color: C.err, opacity: isCardHover(t.id, 'del') ? 1 : 0, transition: 'opacity .15s' }
142
174
  }, ic('trash-2', 11)) : null),
143
- // 史诗迷你进度条(3px 高,resolved/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
144
- epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' + cs.resolved + '/' + cs.total }, React.createElement('div', { style: { height: '100%', width: Math.max(0, Math.min(100, Math.round((cs.resolved / cs.total) * 100))) + '%', background: C.ok, borderRadius: 2, transition: 'width .3s' } })) : null,
175
+ // 史诗迷你进度条(3px 高,settled/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
176
+ epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' + csDone + '/' + cs.total }, React.createElement('div', { style: { height: '100%', width: Math.max(0, Math.min(100, Math.round((csDone / cs.total) * 100))) + '%', background: C.ok, borderRadius: 2, transition: 'width .3s' } })) : null,
145
177
  epic && cs.activeTitle ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '在跑子任务:' + cs.activeTitle }, '▸ 在跑:' + cs.activeTitle) : null,
146
178
  // 当前动作行:展示摘要(工具名+关键参数),悬停 title 仍是 host 原文
147
179
  (t.status === 'in-progress' || t.status === 'verifying') && state.activity[t.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: state.activity[t.id] }, '👁 ' + activitySummary(state.activity[t.id])) : null,
@@ -26,15 +26,20 @@
26
26
  if (!(execMs >= 0)) return // 时间戳异常(NaN/负值)跳过
27
27
  var cx = t.context || {}
28
28
  var hasRes = (Array.isArray(cx.files) && cx.files.length > 0) || (typeof cx.notes === 'string' && cx.notes.trim().length > 0)
29
- var tok = (t.usage && typeof t.usage.total === 'number' && t.usage.total > 0) ? t.usage.total : null // 无 usage 记录的卡不拉低 token 均值(只计有结算样本)
30
- ;(hasRes ? roiYes : roiNo).push({ execMs: execMs, tok: tok })
29
+ // token 口径与「Token 消耗区」一致化(task-muupnnq5):取**有效消耗**(输入+输出+缓存写,不含缓存读),
30
+ // 不再拿含缓存读的 total 冒充——本板实测缓存读占总量 ~94%,用 total 会让 ROI 行的 token 均值虚高十几倍,
31
+ // 与 Token 区大数字口径互相打架。分量字段缺失的老/存量卡兜底退化为 total 并打 tokFallback 标记(title 标注),
32
+ // 绝不把总量伪装成有效值;无任何结算记录的卡 tok=null,不参与均值(不拉低样本)。
33
+ var tk = roiTokenOf(t.usage)
34
+ ;(hasRes ? roiYes : roiNo).push({ execMs: execMs, tok: tk.tok, tokFallback: tk.fallback })
31
35
  })
32
36
  var roi = null
33
37
  if (roiYes.length > 0 && roiNo.length > 0 && roiYes.length + roiNo.length >= 4) {
34
38
  var roiAgg = function (arr) {
35
39
  var execMs = Math.round(arr.reduce(function (a, b) { return a + b.execMs }, 0) / arr.length)
36
- var toks = []; arr.forEach(function (x) { if (x.tok !== null) toks.push(x.tok) })
37
- return { n: arr.length, execMs: execMs, tok: toks.length ? Math.round(toks.reduce(function (a, b) { return a + b }, 0) / toks.length) : null }
40
+ var toks = [], fb = 0
41
+ arr.forEach(function (x) { if (x.tok !== null) { toks.push(x.tok); if (x.tokFallback) fb++ } })
42
+ return { n: arr.length, execMs: execMs, tok: toks.length ? Math.round(toks.reduce(function (a, b) { return a + b }, 0) / toks.length) : null, tokN: toks.length, tokFallbackN: fb }
38
43
  }
39
44
  roi = { yes: roiAgg(roiYes), no: roiAgg(roiNo) }
40
45
  }
@@ -42,6 +47,19 @@
42
47
  return { total: total, byStatus: byStatus, byPriority: byPriority, byAgent: byAgent, avgQueue: fmtMs(avgMs(queueTimes)), avgExec: fmtMs(avgMs(execTimes)), avgVerify: fmtMs(avgMs(verifyTimes)), todayDone: todayDone, dailyDone: dailyDone, recentActivity: recentActivity, roi: roi }
43
48
  }
44
49
 
50
+ // 单卡 token 口径(task-muupnnq5):ROI 行与「Token 消耗区」口径同源,避免同一张卡两处两个数。
51
+ // 有分量(input/output/cacheWrite 任一 >0)→ 有效消耗(不含缓存读);只有 total 的老/存量卡 → 兜底 total 并标 fallback;
52
+ // 两者皆无 → tok=null(无结算样本,跳过而不是记 0,避免拉低均值)。
53
+ // 注:客户端 bundle 只注入 React(无模块系统),故此处内联,host 侧同款判定在 lib/usage.mjs taskEffectiveTokens。
54
+ function roiTokenOf(u) {
55
+ if (!u) return { tok: null, fallback: false }
56
+ function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
57
+ var inp = pos(u.input), outp = pos(u.output), cw = pos(u.cacheWrite), tot = pos(u.total)
58
+ if (inp > 0 || outp > 0 || cw > 0) return { tok: inp + outp + cw, fallback: false }
59
+ if (tot > 0) return { tok: tot, fallback: true }
60
+ return { tok: null, fallback: false }
61
+ }
62
+
45
63
  function TrendChart(props) {
46
64
  var data = props.data || []
47
65
  var max = 1; data.forEach(function (d) { if (d.count > max) max = d.count })
@@ -65,17 +83,23 @@
65
83
  // 调研 ROI 对比行(task-mutnjesa):让数据替道理说话——「有调研 vs 无调研」分组账单直接摆给主窗口看,
66
84
  // 比任何引导文案都管用。数据源 computeStats 的 roi(null = 任一组为空或总样本 <4,整块不渲染);
67
85
  // 无调研组明显更慢(平均执行 > 有调研组 1.2 倍,阈值写清避免把随机波动误读成结论)时其数字用 warn 色。
86
+ // token 口径(task-muupnnq5):**有效消耗**(输入+输出+缓存写,不含缓存读),与「Token 消耗区」大数字同源;
87
+ // 组内只要有一张卡走了 total 兜底(无分量字段的老/存量卡),该数字后加 '~' 并在 title 里写明张数。
68
88
  function ResearchRoiRow(props) {
69
89
  var roi = props.roi
70
90
  if (!roi) return null
71
91
  function fmtM(ms) { var m = Math.round(ms / 60000); if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
72
92
  var noSlower = roi.no.execMs > roi.yes.execMs * 1.2
73
93
  function grp(icon, label, g, warnNums) {
74
- return React.createElement('span', null,
94
+ var approx = (g.tokFallbackN || 0) > 0
95
+ var tip = g.tok === null ? label + ':无带 usage 结算记录的卡,token 均值不可计'
96
+ : g.n + ' 卡中 ' + g.tokN + ' 张有 usage 结算记录;均值口径 = 有效消耗(输入+输出+缓存写,不含缓存读)'
97
+ + (approx ? ';其中 ' + g.tokFallbackN + ' 张无分量字段(老/存量结算),退化为含缓存读的合计值参与均值(故标 ~)' : '')
98
+ return React.createElement('span', { title: tip },
75
99
  icon + ' ' + label + ' ',
76
- React.createElement('span', { style: { fontWeight: 600, color: warnNums ? C.warn : C.text } }, g.n + ' 卡 平均 ' + fmtM(g.execMs) + '/约 ' + (g.tok === null ? '-' : fmtTokens(g.tok)) + ' tok'))
100
+ React.createElement('span', { style: { fontWeight: 600, color: warnNums ? C.warn : C.text } }, g.n + ' 卡 平均 ' + fmtM(g.execMs) + '/约 ' + (g.tok === null ? '-' : (approx ? '~' : '') + fmtTokens(g.tok)) + ' tok'))
77
101
  }
78
- return React.createElement('div', { style: { padding: '6px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12, fontSize: 10, color: C.text2, display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }, title: 'resolved/archived 且领取过的卡按有无调研注入分组现算(执行 = 被领取→完成;token 均值只计有 usage 结算记录的卡)' },
102
+ return React.createElement('div', { style: { padding: '6px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12, fontSize: 10, color: C.text2, display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }, title: 'resolved/archived 且领取过的卡按有无调研注入分组现算(执行 = 被领取→完成;token 均值只计有 usage 结算记录的卡,口径 = 有效消耗:输入+输出+缓存写,不含缓存读;老卡无分量字段时退化为合计并标 ~)' },
79
103
  React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('scale', 11), '调研 ROI'),
80
104
  grp('📎', '有调研', roi.yes, false),
81
105
  React.createElement('span', null, '·'),
@@ -107,7 +131,14 @@
107
131
  var _R = React; var useState = _R.useState
108
132
  var _a = useState(state.rfOpen || false), open = _a[0], setOpen = _a[1]
109
133
  var rg = activeRange()
110
- function setRange(from, to) { state.dateRange = { from: from, to: to }; notify() }
134
+ function setRange(from, to) {
135
+ state.dateRange = { from: from, to: to }
136
+ // 报告/总览是本地现算,notify 即可;Token 区的模型分布/Top8/累计要按范围重算,
137
+ // 而过滤在 host(run 级数据只在 host)→ 必须重拉一次 get-tasks(fetchTasks 会带上新范围)。
138
+ // fetchTasks 在 kernel 域定义,同处 apply 函数体 → 函数声明提升,此处可用。
139
+ if (typeof fetchTasks === 'function') fetchTasks()
140
+ notify()
141
+ }
111
142
  function preset(days) {
112
143
  if (days === 0) { setRange('', ''); return }
113
144
  var to = new Date(); var from = new Date(Date.now() - (days - 1) * 86400000)
@@ -135,8 +166,8 @@
135
166
  React.createElement('input', { type: 'date', value: rg.from, onChange: function (e) { setRange(e.target.value, rg.to) }, style: dateInput })),
136
167
  React.createElement('label', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, '截至',
137
168
  React.createElement('input', { type: 'date', value: rg.to, onChange: function (e) { setRange(rg.from, e.target.value) }, style: dateInput })),
138
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '作用于报告与全局总览')),
139
- React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 5 } }, '报告统计「时间范围内有活动」的任务;总览只显示范围内有活跃的会话。')) : null)
169
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '作用于报告、全局总览与 Token 区(模型分布/Top8/累计)')),
170
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 5 } }, '报告统计「时间范围内有活动」的任务;总览只显示范围内有活跃的会话;Token 区按 run 的本地日落点过滤(今日与近 7 天为固定口径,不受范围影响)。')) : null)
140
171
  }
141
172
 
142
173
  function GlobalBoards() {
@@ -206,6 +237,24 @@
206
237
  // usageSummary 由 host 从各任务 t.usage 现算(t.usage 来自 Worker/Verifier 会话 v4 日志的
207
238
  // assistant/message.usage 聚合)。这里只做展示、不做计费断言;日志读不到/还没有 run 结算时
208
239
  // usageSummary.total 为 0,一律显示「暂无数据」。
240
+ // 日账(usageSummary.byDay:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
241
+ // 双指标口径:t = 总量(含缓存读)、e = 有效消耗(输入+输出+缓存写,不含缓存读)。
242
+ // e === null 表示该日只有老形态 number 日账 / 存量兜底(没有逐 run 拆分,有效值不可知)——
243
+ // 展示时退化为 t 并在文案/title 上标 ~ 近似,绝不把总量冒充有效值。
244
+ // dayKey 口径与 host 记账完全一致——本地 getters 拼,绝不用 toISOString()(UTC 会让
245
+ // 晚上 8 点后的消耗落到次日,今日消耗直接错位)。
246
+ function localDayKey(d) {
247
+ var x = d || new Date()
248
+ function p2(n) { return (n < 10 ? '0' : '') + n }
249
+ return x.getFullYear() + '-' + p2(x.getMonth() + 1) + '-' + p2(x.getDate())
250
+ }
251
+ // 近 n 天的本地日 key 序列(旧 → 新,最后一个是今天);本地日期构造天然跨月/跨年正确
252
+ function lastNDays(n) {
253
+ var out = []
254
+ var now = new Date()
255
+ for (var i = n - 1; i >= 0; i--) out.push(localDayKey(new Date(now.getFullYear(), now.getMonth(), now.getDate() - i)))
256
+ return out
257
+ }
209
258
  function fmtTokens(n) {
210
259
  var v = Number(n) || 0
211
260
  if (v >= 1000000000) return (v / 1000000000).toFixed(1) + 'B'
@@ -226,31 +275,90 @@
226
275
  function TokenUsage(props) {
227
276
  var u = props.usage
228
277
  var box = { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 }
229
- 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 消耗')
278
+ // 范围激活标记:Token 区的模型分布/Top8/累计已按「统计范围」过滤(host 侧重算),
279
+ // 不加标记的话用户没法判断看到的数字是全量还是范围内——标记与 RangeFilter 同源(activeRange)。
280
+ var tg = activeRange()
281
+ var tgOn = !!(tg.from || tg.to)
282
+ 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
+ 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)
230
284
  if (!u || !u.total) return React.createElement('div', { style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据(Worker/Verifier 会话日志里还没有 usage 记录)'))
231
- var models = Object.keys(u.byModel || {}).map(function (m) { return { model: m, total: u.byModel[m] || 0 } }).sort(function (a, b) { return b.total - a.total })
232
- var maxM = models.length ? (models[0].total || 1) : 1
233
- var top = u.topTasks || []
234
- var maxT = top.length ? (top[0].total || 1) : 1
285
+ // ===== 主数字口径统一(task-muwq9u04):模型分布行与 Top8 行的主数字一律显示**有效消耗**(e),
286
+ // 含缓存读的合计(t)退到 title 悬浮;否则同一块里「今日有效 3.7M」与「单任务 21M」并排自相矛盾
287
+ // (用户就是这么判成 bug 的)。数据源:host 的 byModelEff(模型有效分摊)与 topTasks[].effective;
288
+ // 老 host 缺 byModelEff / 老卡无五分量留账 → 退化用合计值并在 title 标 ~(口径不伪造)。
289
+ // 排序按**显示口径**(有效)降序:条形长度与行序一致,否则首行不是最长的条。
290
+ var models = Object.keys(u.byModel || {}).map(function (m) {
291
+ var tt = u.byModel[m] || 0
292
+ var ee = (u.byModelEff && typeof u.byModelEff[m] === 'number') ? u.byModelEff[m] : null
293
+ return { model: m, total: tt, eff: (ee === null ? tt : ee), approx: ee === null }
294
+ }).sort(function (a, b) { return b.eff - a.eff })
295
+ var maxM = models.length ? (models[0].eff || 1) : 1
296
+ var top = (u.topTasks || []).map(function (x) {
297
+ var ee = (typeof x.effective === 'number') ? x.effective : null
298
+ 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
+ }).sort(function (a, b) { return b.eff - a.eff })
300
+ var maxT = top.length ? (top[0].eff || 1) : 1
301
+ // 日账:今日数字取本地日 key,没有日账(byDay 缺字段/老 host)时退化为 0,不炸也不误报。
302
+ // 读侧兼容两种单元形态:老 number(只有总量,有效值不可知)→ { t: n, e: null }。
303
+ var byDay = (u.byDay && typeof u.byDay === 'object') ? u.byDay : {}
304
+ function dayOf(v) {
305
+ if (v && typeof v === 'object') return { t: Number(v.t) || 0, e: (v.e === 0 || v.e) ? Number(v.e) || 0 : null }
306
+ return { t: Number(v) || 0, e: null }
307
+ }
308
+ var todayKey = localDayKey()
309
+ var todayCell = dayOf(byDay[todayKey])
310
+ // 有效消耗缺失(老日账/存量兜底)→ 大数字退化为总量并打 approx 标 ~,口径不伪造
311
+ var todayApprox = todayCell.e === null
312
+ var todayEff = todayApprox ? todayCell.t : todayCell.e
313
+ var days = lastNDays(7)
314
+ var maxDay = 1
315
+ 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 }
235
317
  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
+ // 累计三分量:有效(真实成本)+ 缓存读(占了 total 的大头,必须单列才看得出虚高来源)+ 合计
319
+ var effTotal = (typeof u.effective === 'number') ? u.effective : ((u.input || 0) + (u.output || 0) + (u.cacheWrite || 0))
236
320
  return React.createElement('div', { style: box },
237
321
  head,
238
322
  React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap', marginBottom: 8 } },
239
- React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand } }, fmtTokens(u.total)),
240
- React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(本看板累计)'),
241
- mini('输入', u.input), mini('输出', u.output), mini('缓存读', u.cacheRead), u.cacheWrite ? mini('缓存写', u.cacheWrite) : null),
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,
326
+ 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
+ mini('输入', u.input), mini('输出', u.output), u.cacheWrite ? mini('缓存写', u.cacheWrite) : null),
328
+ // 近 7 天迷你条形:高按区间 max 归一(今天高亮 brand,其余浅底 + 边框),
329
+ // 柱高一律取**有效消耗**;e 不可知的日退化为总量(title 标 ~ 近似)。
330
+ // 7 天全为 0 时整块不渲染(零残留,不占版面)
331
+ hasDayData ? React.createElement('div', { style: { marginBottom: 8 } },
332
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(有效消耗)'),
333
+ React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 4 } },
334
+ days.map(function (k) {
335
+ var c = dayOf(byDay[k])
336
+ var approx = c.e === null
337
+ var v = approx ? c.t : c.e
338
+ 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 } }),
342
+ React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2, whiteSpace: 'nowrap' } }, k.slice(5)))
343
+ }))) : null,
242
344
  React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } },
243
345
  React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
244
- React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按模型分布'),
346
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按模型分布(有效消耗)'),
245
347
  models.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : models.map(function (m) {
246
- return React.createElement(UsageRow, { key: m.model, label: m.model, value: m.total, max: maxM, color: C.brand, title: m.model + ':' + String(m.total) + ' tokens' })
348
+ 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' })
247
349
  })),
248
350
  React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
249
- React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8'),
351
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8(有效消耗)'),
250
352
  top.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : top.map(function (x) {
251
353
  var t = getTask(x.id)
252
- return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.total, max: maxT, color: C.ok, labelColor: t ? C.brand : C.text2, title: x.title + '(' + String(x.total) + ' tokens / ' + (x.runs || 0) + ' 次 run)' + (t ? '——点击查看详情' : ''), onClick: t ? function () { state.detailId = x.id; notify() } : undefined })
253
- }))))
354
+ 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
+ }))),
356
+ // 口径边界:本区只统计看板派发的 Worker/Verifier run,主窗口对话自身不越界纳入
357
+ // 范围说明(task-muwc7hjd):用户常把 RangeFilter 当成「整页过滤」,但今日大数字与近 7 天柱子
358
+ // 是自身固定口径(今日=本地今天、近 7 天=最近 7 个本地日)——不随范围变,必须在文案里讲清,
359
+ // 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
360
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话;主数字(模型分布 / Top 8)与「今日」「近 7 天」均为有效消耗口径(输入+输出+缓存写,不含缓存读),含缓存读的合计在悬浮 title 里单列对照'),
361
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'))
254
362
  }
255
363
 
256
364
  // ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
@@ -386,6 +494,19 @@
386
494
  var _a = useState(false), open = _a[0], setOpen = _a[1]
387
495
  var _p = useState(null), pos = _p[0], setPos = _p[1]
388
496
  var ref = useRef(null); var btnRef = useRef(null)
497
+ // 开关点击即时反馈(反馈:勾选几秒才同步,task-muw5uudk):
498
+ // checked 是受控值(读 Props→state,由 3s 轮询通知才更新),此前 onChange 只发 rpc →
499
+ // 点下去要等 set-board-config + fetchTasks 双往返(安静板卡上 tasksHash 不变还会被渲染节约吃掉)才翻面。
500
+ // 口径:**先写 state + notify(点击瞬时翻面)→ 再 rpc('set-board-config') → 失败回滚 state + notify
501
+ // 并复用错误条 reportReadErr(沿用「读路径错误防线」:绝不让开关停在未落盘的值上)**。
502
+ // 成功路径的权威纠偏在 kernel.fetchTasks(配置变化检测),此处不重复 fetchTasks:3s 轮询自会带回真值。
503
+ function cfgRollback(key, prev) { state[key] = prev; notify() }
504
+ function setCfg(key, next) {
505
+ var prev = !!state[key] // 乐观更新前的值,失败回滚用
506
+ state[key] = next === true // 归一成布尔:默认开口径由服务端权威值(fetchTasks)纠偏
507
+ notify() // 立即翻面,不等 rpc
508
+ rpc('set-board-config', { key: key, value: next === true }).catch(function (e) { cfgRollback(key, prev); reportReadErr('设置保存失败:' + readErrText(e)) })
509
+ }
389
510
  useEffect(function () { if (!open) return; function onDown(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }; document.addEventListener('mousedown', onDown); return function () { document.removeEventListener('mousedown', onDown) } }, [open])
390
511
  function toggle() {
391
512
  // position:fixed + 视口坐标:面板容器是 overflow:hidden + maxHeight:60vh,
@@ -414,8 +535,26 @@
414
535
  // 学习飞轮 v1 总开关:关掉后不生成候选教训、prompt 不提软召回、详情页不渲染「沉淀」按钮
415
536
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '学习反馈'),
416
537
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
417
- React.createElement('input', { type: 'checkbox', checked: !!props.feedbackEnabled, onChange: function (e) { rpc('set-board-config', { key: 'feedbackEnabled', value: e.target.checked }).then(fetchTasks).catch(function () {}) } }),
418
- React.createElement('span', null, '候选教训(Verifier 驳回/仲裁结论自动生成候选,Worker prompt 提示先检索历史教训)'))) : null)
538
+ React.createElement('input', { type: 'checkbox', checked: !!props.feedbackEnabled, onChange: function (e) { setCfg('feedbackEnabled', e.target.checked) } }),
539
+ React.createElement('span', null, '候选教训(Verifier 驳回/仲裁结论自动生成候选,Worker prompt 提示先检索历史教训)')),
540
+ // 回执开关(用户要求「回执可以做一个开关,放到设置里」):派发/完成两类回执各自可关,缺省都开。
541
+ // 只影响回执播报,不影响派发与状态机;歧义裁决通知不在此闸门内(见下行说明文案)。
542
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '通知'),
543
+ React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
544
+ React.createElement('input', { type: 'checkbox', checked: props.notifyDispatch !== false, onChange: function (e) { setCfg('notifyDispatch', e.target.checked) } }),
545
+ React.createElement('span', null, '⚡ 派发回执(任务被 Worker/Verifier 领走时播报)')),
546
+ React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260, marginTop: 3 } },
547
+ React.createElement('input', { type: 'checkbox', checked: props.notifyDone !== false, onChange: function (e) { setCfg('notifyDone', e.target.checked) } }),
548
+ React.createElement('span', null, '✅ 完成回执(任务完成或阻塞时聚合播报)')),
549
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '歧义裁决通知不受这两个开关影响(任务等人裁决必须提醒)'),
550
+ // 史诗拆分总开关(板级 epicSplit,缺省 true):**只关引导,不禁机制**——关掉后 Team 提示词不再
551
+ // 注入「大任务必须拆分」第 6 条、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
552
+ // 子卡、史诗自动收口/hooks 状态机照常(用户/主窗口明确要拆时不受阻)。勾选态缺字段=开,与 host 同口径。
553
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '功能'),
554
+ React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
555
+ React.createElement('input', { type: 'checkbox', checked: props.epicSplit !== false, onChange: function (e) { setCfg('epicSplit', e.target.checked) } }),
556
+ React.createElement('span', null, '🧩 史诗拆分:大任务引导拆为 epic + 子任务')),
557
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '关掉只停引导:显式 parentId 建子卡与史诗自动收口照常工作')) : null)
419
558
  }
420
559
 
421
560
  function TeamView() {