417 lines
24 KiB
Markdown
417 lines
24 KiB
Markdown
# 宿主会话诊断日志洪水 —— 根因与一行修复
|
||||
|
|
|
|||
|
|
> 2026-10-01 · 取证方式:解包 `app.asar`(317 MB)定位源码 + 实测会话日志行级统计
|
|||
|
|
> 结论一句话:**97.7% 的日志是同一个「工具调用流式帧」被逐帧记录;排除集合漏了两个 key,补上即减掉约 96% 体积。**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、现象
|
|||
|
|
|
|||
|
|
会话跑久了,宿主界面会**静默哑掉**:工具调用记录在界面上不再出现,用户零感知;同时磁盘上出现大量 10 MiB 整的日志文件。
|
|||
|
|
|
|||
|
|
宿主自身的报错(`<配置根>/logs/daemon.log`):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
[conversations] diagnostic log write failed {"code":"EPERM","droppedLines":2445,"droppedBytes":657690}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
实测计数:`daemon.log` 47 次、`daemon.old.log` 62 次。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、日志到底写了什么(实测行级统计)
|
|||
|
|
|
|||
|
|
样本:`logs/2026-10-01/sdk/conversations/5f607d3e-*.log` 尾段 **16,159 行 / 4 MB**。
|
|||
|
|
|
|||
|
|
| 行标签 | 占比 |
|
|||
|
|
|---|---|
|
|||
|
|
| `event-machine:dispatch` | **97.72%** |
|
|||
|
|
| `state-machine:transition` | 0.99% |
|
|||
|
|
| `method:requests` / `method:requests:result` | 各 0.25% |
|
|||
|
|
| `artifact:*` / `LOCAL_HTTP_CONNECTION` / 其余 | < 1% |
|
|||
|
|
|
|||
|
|
在 `event-machine:dispatch` 内部,**95.83% 是同一条记录**,逐字相同、只有 `requestId` 在变:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{"instanceId":"ci-1","input":"tool_call_update",
|
|||
|
|
"requestId":"01a0f524bb5f7360bbd0fde2f4b2ea16",
|
|||
|
|
"output":{"completeAssistantStream":false,"hasUsage":false,
|
|||
|
|
"hasTitle":false,"resourceEffectCount":0}}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
单行 244 B(含换行 245 B),四个字段恒为假值。**即:一次工具调用的入参是流式增量,每来一帧就落一行。**
|
|||
|
|
|
|||
|
|
### 2.1 帧是"零信息"的(2026-10-01 17:5x 补测,4 个会话全量统计)
|
|||
|
|
|
|||
|
|
帧的**完整字段**只有四个:`instanceId`、`input`、`requestId`、`output`。
|
|||
|
|
|
|||
|
|
- **没有** `seq`、`messageId`、`textLen`、`textHash`(39,999 帧里 "无此字段" 39,999 条);
|
|||
|
|
- `output` 的四个位(`completeAssistantStream` / `hasUsage` / `hasTitle` / `resourceEffectCount`)**恒为 false / 0**;
|
|||
|
|
- 同一轮次内,所有帧**逐字节相同**,只有 `requestId` 随轮次变化。
|
|||
|
|
|
|||
|
|
⇒ **每一帧都不携带任何可读信息**,即使退一步"要保留工具调用记录",这些帧也没有保留价值。
|
|||
|
|
|
|||
|
|
### 2.2 帧数是"活动量"的函数,不是定时心跳(补测)
|
|||
|
|
|
|||
|
|
`requestId` 的实际粒度是**一轮对话**(一次模型请求),一轮内会做很多次工具调用。实测四会话:
|
|||
|
|
|
|||
|
|
| 会话 | 工具调用 | 帧数 | 帧/次 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `3f43ce71…` | 236 | 39,999 | 169.5 |
|
|||
|
|
| `ee3c2d82…` | 218 | 39,965 | 183.3 |
|
|||
|
|
| `ac8de40d…` | 133 | 18,473 | 138.9 |
|
|||
|
|
| `6f3073c7…` | 150 | 31,489 | 209.9 |
|
|||
|
|
|
|||
|
|
**按轮次拆开**看,"帧/次"在 **5.2 ~ 422.5** 之间波动、中位约 **150 ~ 220**:
|
|||
|
|
|
|||
|
|
- `3f43ce71` 逐轮:156.8 / 185.2 / 240.2 / 131.3 / 304.0 / 112.6
|
|||
|
|
- `ac8de40d` 逐轮:5.2 / 39.4 / 41.6 / 79.8 / 121.0 / 154.0 / 156.3 / 247.8 / 367.1 / 422.5
|
|||
|
|
|
|||
|
|
**同一轮内的帧间隔分布**(样本:一轮 20,698 帧、跨度 496 秒):
|
|||
|
|
|
|||
|
|
| 间隔 | <1 ms | 1–10 ms | 10–100 ms | 0.1–1 s | 1–10 s | >10 s |
|
|||
|
|
|---|---|---|---|---|---|---|
|
|||
|
|
| 帧数 | **12,259** | 4,998 | 3,344 | 42 | 41 | 13 |
|
|||
|
|
|
|||
|
|
**中位间隔 = 0 ms**,平均 41.7 帧/秒。
|
|||
|
|
|
|||
|
|
⇒ 结论:帧是**成串爆发**式发出的(**不是**固定节奏轮询),发出密度**正比于该轮的工具有活动量**。
|
|||
|
|
所以帧数由 **「这段时间发生了多少次工具调用相关事件」** 决定 —— 调用越多、单次调用持续时间越长,帧越多。
|
|||
|
|
|
|||
|
|
### 2.3 归因:两件事分开看(这是本节的关键)
|
|||
|
|
|
|||
|
|
「一次工具调用写约 200 帧、约 48 KB」是**两个独立环节相乘**的结果,责任不在一处:
|
|||
|
|
|
|||
|
|
1. **帧的产生** —— 归**宿主 agent 事件机**:它把每次工具调用的状态变化/流式增量**逐条上报**,
|
|||
|
|
这是协议与宿主侧的设计行为。**外部改不了它发多少条**。
|
|||
|
|
- ⚠️ 与"调用方式"的关系:**只体现在"次数"上**(调用越多帧越多),
|
|||
|
|
但**"每次调用 150~220 帧"这个量级不取决于我们怎么写命令**——四个不同会话都是这个量级;
|
|||
|
|
逐轮的 5~422 波动来自单次调用的实际活动量。
|
|||
|
|
- ⇒ **这不是"工具调用方式不对"**:同一个操作换个写法,帧照旧产生。
|
|||
|
|
2. **帧的落盘** —— 归**门控代码缺陷**:按函数注释,这些流式帧**本来就该被丢掉**,
|
|||
|
|
只是排除名单漏了 `tool_call` / `tool_call_update`,于是**全部写盘**。
|
|||
|
|
- ⇒ **这一环才是缺陷所在**(见第三、四节)。
|
|||
|
|
|
|||
|
|
**⇒ 一句话**:`48 KB/次` = **帧的产生**(宿主行为,改不了,不是你调用方式的问题)
|
|||
|
|
+ **帧没被过滤**(厂商缺陷,本应丢弃)。唯一的责任在第二环。
|
|||
|
|
|
|||
|
|
### 2.4 帧能定位到"具体哪个工具"吗?—— 不能(2026-10-01 18:0x 补测)
|
|||
|
|
|
|||
|
|
**帧记录里没有任何工具标识字段。** 对会话日志逐一检查:
|
|||
|
|
|
|||
|
|
| 字段 | 出现次数 |
|
|||
|
|
|---|---|
|
|||
|
|
| `toolName` | 0 |
|
|||
|
|
| `tool_name` | 0 |
|
|||
|
|
| `kind` | 0 |
|
|||
|
|
| `rawInput` | 0 |
|
|||
|
|
| `toolCallId` | 0 |
|
|||
|
|
|
|||
|
|
⇒ 帧只有 `instanceId` / `input` / `requestId` / `output` 四个字段,
|
|||
|
|
**`requestId` 的粒度还是一整轮对话** ⇒ **事后无法把 2 万帧归到某一次具体工具调用上**,
|
|||
|
|
更无法回答"是哪个工具贡献的"。**诊断日志记了 2 万行,却查不出凶手是谁** —— 这本身也是缺陷的一部分。
|
|||
|
|
|
|||
|
|
**能确定的是**:帧的类型是 `tool_call_update`,而这是 ACP 协议里**所有工具通用**的
|
|||
|
|
"工具调用状态更新"通知(不含工具种类信息)⇒ **不是某个特定工具造成的,是所有工具都会产生**;
|
|||
|
|
每个工具调用都会经历"开始 / 进行中 / 完成"等状态变化,因而必然产生若干帧。
|
|||
|
|
数量差异只来自"该调用期间产生了多少次更新",与工具是什么无关。
|
|||
|
|
|
|||
|
|
### 2.5 与"改 json 配置""加 hook"有关系吗?—— 没有(逐条排除)
|
|||
|
|
|
|||
|
|
1. **源码级**:发出点只读三个**运行时参数**
|
|||
|
|
(`sessionUpdate` / `replayingHistory` / `completeAssistantStream`),
|
|||
|
|
**不读任何配置**。json 配置(含 MCP、settings)不在这条链路上。
|
|||
|
|
2. **hook 不在同一条通道**:hook 是宿主另起的**子进程回调**,不产生 ACP 的 `tool_call` 事件,
|
|||
|
|
因此不会给会话日志贡献 `tool_call_update` 帧。
|
|||
|
|
⚠️ 唯一理论上的**间接**影响:若某个 hook 导致工具调用被拒绝并**重试**,
|
|||
|
|
会多出"调用次数",从而按比例多出帧 —— 但这是间接的、且有界。
|
|||
|
|
3. **实测反证(最强)**:四个会话跨整天(08:48 / 10:16 / 12:35 / 15:55,+08),
|
|||
|
|
期间 `settings.json` 与 hook 配置**改动过多次**;但「帧/次」始终稳定在 **169.5 / 183.3 / 138.9 / 209.9**,
|
|||
|
|
**无任何漂移趋势**。
|
|||
|
|
⇒ 帧数**不随配置与 hook 变化**;它只随"这一轮做了多少次工具调用"变化。
|
|||
|
|
|
|||
|
|
### 2.6 🔴 活样本:本会话的日志此刻就是卡死的(2026-10-01 18:04 观测)
|
|||
|
|
|
|||
|
|
写这份报告时所处的会话,其日志文件 `3f43ce71-….log`:
|
|||
|
|
|
|||
|
|
- 大小 **10,485,710 B**(上限 10,485,760 B,**卡在距上限 50 字节处**)
|
|||
|
|
- 最后写入时间 **16:55**;观测时刻 **18:04** ⇒ **一个多小时零写入**
|
|||
|
|
- 同目录下**没有 `.log.1`** ⇒ **轮转一次都没成功过**
|
|||
|
|
|
|||
|
|
⇒ 也就是说:**最近一个多小时的诊断日志全部丢失**。这不是"写太多"的后果,
|
|||
|
|
而是"写不进去"的后果 —— 与第 8 节第 1 条(轮转失败后永不恢复)**完全吻合**,
|
|||
|
|
且**功能不受影响**(会话照常工作),只是**丧失可观测性**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、发出点(源码位置)
|
|||
|
|
|
|||
|
|
`app.asar` 偏移 ≈117,210,567:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
if (shouldLogEventMachineDispatch(
|
|||
|
|
update?.sessionUpdate,
|
|||
|
|
frameContext.replayingHistory,
|
|||
|
|
result.completeAssistantStream === true))
|
|||
|
|
this.init.log("event-machine:dispatch", {
|
|||
|
|
input: update?.sessionUpdate,
|
|||
|
|
requestId: this.init.assembler.currentId,
|
|||
|
|
seq, messageId, textLen, textHash,
|
|||
|
|
output: { completeAssistantStream, stateEvent, turnStopReason,
|
|||
|
|
hasUsage, hasTitle, resourceEffectCount },
|
|||
|
|
...(shouldLogContent() ? { textPreview: readTextPreview(update) } : {})
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
门控定义,偏移 ≈117,230,304:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
/** 环境变量开关:设为 '1' 时输出截断正文预览,并恢复 replay/流式 chunk 的逐帧 dispatch 日志。 */
|
|||
|
|
function shouldLogContent() {
|
|||
|
|
return process.env?.WB_CONVERSATION_LOG_CONTENT === "1";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var STREAM_CHUNK_UPDATES = new Set(["agent_message_chunk", "agent_thought_chunk"]);
|
|||
|
|
|
|||
|
|
/** 默认丢掉历史回放帧和正文流式 chunk;verbose 或 turn 收口帧仍落盘。 */
|
|||
|
|
function shouldLogEventMachineDispatch(sessionUpdate, replayingHistory, completeAssistantStream) {
|
|||
|
|
if (shouldLogContent()) return true;
|
|||
|
|
if (replayingHistory) return false;
|
|||
|
|
if (completeAssistantStream) return true;
|
|||
|
|
return !STREAM_CHUNK_UPDATES.has(sessionUpdate ?? "");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、缺陷定性
|
|||
|
|
|
|||
|
|
函数的注释把设计意图写得很清楚:**「默认丢掉历史回放帧和流式 chunk」**。
|
|||
|
|
|
|||
|
|
但排除集合只列了两种**正文**流式 chunk(`agent_message_chunk`、`agent_thought_chunk`),**漏掉了 `tool_call` 与 `tool_call_update`** —— 而工具的入参同样是流式增量,一次工具调用会产生几十到上百帧。**真正的洪水正是这两个被漏掉的 key。**
|
|||
|
|
|
|||
|
|
也就是说:抑制流式帧的机制已经写好了,只是漏了两个成员,等于没生效。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、修复(一行)
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
var STREAM_CHUNK_UPDATES = new Set([
|
|||
|
|
"agent_message_chunk",
|
|||
|
|
"agent_thought_chunk",
|
|||
|
|
"tool_call", // ← 新增
|
|||
|
|
"tool_call_update", // ← 新增
|
|||
|
|
]);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
预期效果:日志体积下降约 **96%**。
|
|||
|
|
|
|||
|
|
按实测约 37 KB/次工具调用折算,10 MiB 上限对应的会话寿命从 **≈280 次工具调用** 变为 **≈7,000 次** —— 正常会话再也撞不到顶。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、为什么我们这边关不掉它
|
|||
|
|
|
|||
|
|
- `WB_CONVERSATION_LOG_CONTENT` 只能**放大**:设为 `1` 反而变成 verbose(正文预览 + 恢复历史回放逐帧)。**不存在"设为 0 就少记"这条路。**
|
|||
|
|
- 写入器的目录(`path.join(homeDir, "logs")`)与单文件上限(`PER_FILE_DISK_MAX_BYTES = 10485760`)都是**硬编码**;上限字段虽写成可注入(`options.perFileDiskMaxBytes ?? 默认值`),但全包内**只有它自己类里出现过一次**,无任何调用方传值。
|
|||
|
|
- 且"让它写不进去"=直接落进它的丢写分支(丢批 + 指数退避),**那正是"哑掉"这个故障态本身**。
|
|||
|
|
|
|||
|
|
⇒ 结论:**外部无法关闭,只能在上游补那两个 key。**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、我方现状兜底(治标)
|
|||
|
|
|
|||
|
|
已上线 Windows 计划任务 `WorkBuddy-LogCapSweep`:每 1 分钟扫一次当天目录,把满额日志**改名挪开**(`*.stuck-<ts>`,**只改名、绝不删除**),宿主会在数十秒内重建新文件并恢复写入。
|
|||
|
|
|
|||
|
|
局限(已实测):正在被写入的文件**不可以**改名(`WinError 32`),所以只能在它卡住之后救;且哑掉的会话不产生任何事件,**会话内钩子永远不会触发** ⇒ 只有这个"会话外时钟"能救。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、相邻问题(同源,建议一并看)
|
|||
|
|
|
|||
|
|
1. **轮转失败后永不恢复**:轮转 `rotateFile()` 是四步串行(删 `.log.3` → `.log.2→.log.3` → `.log.1→.log.2` → `.log→.log.1`),**四步全成才把计数器归零**。任一步抛错(Windows 上文件被占用即 `EPERM`)⇒ 计数器恒为满值 ⇒ 此后每次写都重试轮转、次次失败、退避封顶 30 秒、每轮只报一次警 ⇒ **该会话日志永久写不进**。建议:轮转失败时也重置计数器或降级为"换名重开",避免单次瞬时占用导致永久死锁。
|
|||
|
|
2. **次要噪声源**:`operation.log` 中 529 行为 `list` 查询,且使用了 `{"page":1,"size":100000}` 这种十万量级分页;另有 `state-machine:transition` 反复记录 `PHASE_WORKING → working, valid:true`。量级远小于第 4 节的问题,但同属"零信息高频写入"。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、在哪里改(完整副本清单)
|
|||
|
|
|
|||
|
|
> 🔴 **2026-10-01 17:2x 更正**:本节最初的清单把两个 CLI 明文包(`codebuddy-headless.js` / `codebuddy-lite-wb.mjs`)列为可改目标,**那是误判**。复核发现它们里面的同名集合是**另一处逻辑**(`createAcpHistoryMarker` 用的历史标记集合),而且**本来就已包含 `tool_call` / `tool_call_update`**;它们**不含** `event-machine:dispatch`,与本次问题无关。真正的补丁点**只在 `app.asar` 归档内**,下表已更正。
|
|||
|
|
|
|||
|
|
全量扫描 `C:\Users\Administrator\AppData\Local\Programs\WorkBuddy\resources`,该字面量在**归档 `app.asar` 内共 3 处**:
|
|||
|
|
|
|||
|
|
| # | 位置 | 归档内绝对偏移 | 说明 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 1 | `app.asar` | 117,230,150 | 定义(后接 `;`)—— **补丁点**|归属文件 `main/conversations.js`(主进程侧) |
|
|||
|
|
| 2 | `app.asar` | 287,051,214 | 定义(后接 `;`)—— **补丁点**|归属文件 `renderer/assets/ui-docs-viewer-Bo02yf1Q.js`(渲染侧) |
|
|||
|
|
| 3 | `app.asar` | 311,175,531 | 写法不同(后接 `.has(updateType)`),属另一路逻辑,**不动** |
|
|||
|
|
|
|||
|
|
完整的 `app.asar` 路径:
|
|||
|
|
`C:\Users\Administrator\AppData\Local\Programs\WorkBuddy\resources\app.asar`
|
|||
|
|
|
|||
|
|
### 改法:就地等长替换(无需解包 · 无需重打包)
|
|||
|
|
|
|||
|
|
由于归档头部记录每个文件的 `size`/`offset`,**改长度会错位**;因此采用**等长**替换:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
替换前(55 字节):new Set(["agent_message_chunk", "agent_thought_chunk"])
|
|||
|
|
替换后(55 字节):{has:()=>true} (末尾补空格凑足长度)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
效果:`shouldLogEventMachineDispatch()` 只在 turn 收口帧返回 `true`,逐帧噪声不再落盘。
|
|||
|
|
配套要做的只有一件事:把被改文件的那条完整性记录(1 个总哈希 + 6 个分块哈希)重算回去——同样等长(64 位十六进制)。
|
|||
|
|
|
|||
|
|
### 已交付的可执行补丁(2026-10-01 · 17:35 修订)
|
|||
|
|
|
|||
|
|
桌面文件夹 `WorkBuddy-日志补丁-20261001\`,**双击即用**:
|
|||
|
|
|
|||
|
|
| 文件 | 作用 |
|
|||
|
|
|---|---|
|
|||
|
|
| `检查状态.bat` | 只看现状(补丁是否已应用 + 完整性核对),**只读不改** |
|
|||
|
|
| `应用补丁.bat` | 打补丁(自动备份) |
|
|||
|
|
| `恢复原始文件.bat` | 还原官方原文件 |
|
|||
|
|
| `logpatch.py` | 补丁程序本体(也可单独命令行跑:`--check` / 无参=打补丁 / `--revert` / `--verify`) |
|
|||
|
|
| `ui-docs-viewer-Bo02yf1Q.js.patched` | 打补丁后该 JS 的内容,供对照 |
|
|||
|
|
|
|||
|
|
已实测:中文路径下双击链路完整跑通(含中文输出);`--revert` 分支在临时目录用假文件验证通过(`CURRENT-PATCHED` → `ORIGINAL-BACKUP`)。
|
|||
|
|
|
|||
|
|
### 🔴 实装后核对,发现并修掉一个**真实缺陷**(务必看)
|
|||
|
|
|
|||
|
|
首轮实装后做独立核对,发现补丁的**归属判定有 bug**:
|
|||
|
|
|
|||
|
|
- `owner_of()` 只用 `offset <= 偏移 < offset+size` 取**第一个**命中项。而 `unpacked` 文件的头部记录是 `offset=0` + 真实 `size`(本例 `editor_sdk.exe`:`offset=0 size=209,494,568 unpacked=true`)⇒ **它把 117,230,150 那一处的归属"抢"走了**。
|
|||
|
|
- 而完整性重算那一步写的是 `if not ow or ow[3]: continue` —— **遇到 `unpacked` 就跳过**。两者叠加 ⇒ 字节被改了(改对了,确实该改),但**它真正的宿主 `main/conversations.js` 的完整性记录没跟着重算**。
|
|||
|
|
- 后果:该文件 `hash=MISMATCH`。虽然实测确认归档**没有**整文件级的顶层哈希、且现存 44 个 `unpacked` 文件本来就不匹配(说明这些记录当前未必被强制校验),但"记录与内容不一致"本身就是隐患,不能留。
|
|||
|
|
- ⚠️ 教训:首轮的"自检全绿"是**假绿** —— 它只检查了自己重算过的那个文件,**没有反向核对"该重算的文件是否都重算了"**。
|
|||
|
|
|
|||
|
|
已处置:
|
|||
|
|
|
|||
|
|
1. **补齐归档**:按文件条目(用其唯一 `offset` 做锚点,避开 basename 歧义)就地重算 `main/conversations.js` 的 `hash` + `blocks`,**先在内存验证通过才落盘**,落盘后差异只落在头部 1 个 1 MiB 块内。
|
|||
|
|
2. **修掉脚本**:`owner_packed()` 改为**排除 `unpacked`**、并在多候选取"最小者";同时去掉"遇 unpacked 就跳过"的写法,改为"找不到打包归属 ⇒ **中止不写盘**"。
|
|||
|
|
3. **补上反向核对**:脚本现在会跑全量 `--verify`(只对打包文件),并在落盘前自检。
|
|||
|
|
|
|||
|
|
### 最终核对结果(2026-10-01 17:35 · 独立核对器)
|
|||
|
|
|
|||
|
|
- 归档**无**整文件级顶层哈希 ⇒ 改字节不会触发整文件校验失败。
|
|||
|
|
- 与官方备份逐 1 MiB 块比对:差异**只在头部**(完整性记录)+ 2 处补丁点,无任何意外改动。
|
|||
|
|
- 全量完整性:**打包文件 17,800 个全部通过,0 个不通过**。
|
|||
|
|
- 两处补丁点均 `hash=OK`(`main/conversations.js` `blocks=1/1`;`ui-docs-viewer-*.js` `blocks=6/6`)。
|
|||
|
|
- 旧串残留 1 处(=那处下文接 `.has(...)` 的另一路逻辑,本就不在范围);新串 2 处。
|
|||
|
|
|
|||
|
|
### 风险清单
|
|||
|
|
|
|||
|
|
1. **应用升级会覆盖改动**,每次升级都要重做(重跑 `应用补丁.bat`)。
|
|||
|
|
2. 这是厂商签名的官方主程序,改动后出问题不受厂商支持;本项目红线也明确 **⛔ 不写官方主程序** ⇒ 这一步只能由使用者自行决定与执行。
|
|||
|
|
3. 回滚源只有一份:`app.asar.bak-<日期>`,**请勿删除**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十、附:可直接提交给厂商的缺陷说明(2026-10-01)
|
|||
|
|
|
|||
|
|
> 下面这段**自包含**,可整段复制到厂商反馈表单 / 邮件里提交,不必附带本报告其余章节。
|
|||
|
|
> 提交入口与必填项 ⇒ 见本节末尾。
|
|||
|
|
|
|||
|
|
### 标题
|
|||
|
|
|
|||
|
|
会话诊断日志被流式工具帧刷爆,且轮转失败后永久停止写入(日志静默哑掉)
|
|||
|
|
|
|||
|
|
### 基本信息
|
|||
|
|
|
|||
|
|
- 产品:WorkBuddy
|
|||
|
|
- 版本:**5.6.2**(Electron 37.10.3-24)
|
|||
|
|
- 平台:Windows(win32 x64)
|
|||
|
|
- 安装路径:`C:\Users\Administrator\AppData\Local\Programs\WorkBuddy\`
|
|||
|
|
|
|||
|
|
### 问题一:97.7% 的会话诊断日志是零信息的工具流式帧
|
|||
|
|
|
|||
|
|
**现象**:单次工具调用约写 **150~220 帧 / 约 37~48 KB** 诊断日志。一份 16,159 行的会话日志里,**97.72%** 的行标签是 `event-machine:dispatch`,其中 **95.83%** 是逐字节相同的同一条记录(只有 `requestId` 在变):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{"instanceId":"ci-1","input":"tool_call_update",
|
|||
|
|
"requestId":"01a0f524bb5f7360d0fde2f4b2ea16",
|
|||
|
|
"output":{"completeAssistantStream":false,"hasUsage":false,"hasTitle":false,"resourceEffectCount":0}}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**帧是零信息的**:字段只有 `instanceId` / `input` / `requestId` / `output`;没有 `seq`、`messageId`、`textLen`、`textHash`(39,999 帧全量统计,"无此字段" 39,999 条);`output` 四个布尔位**恒为 `false`/`0`**;同一轮内所有帧逐字节相同。
|
|||
|
|
|
|||
|
|
**帧是成串爆发的,不是定时心跳**:同一轮内帧间隔分布 —— <1 ms 有 **12,259** 帧(**中位间隔 0 ms**)、1–10 ms 4,998、10–100 ms 3,344,平均 **41.7 帧/秒**。四会话实测「帧/次」= 169.5 / 183.3 / 138.9 / 209.9,逐轮波动 5.2~422.5。⇒ 帧数正比于**该轮工具活动量**,与调用方式无关。
|
|||
|
|
|
|||
|
|
**帧无法归因到具体工具**:`toolName` / `tool_name` / `kind` / `rawInput` / `toolCallId` 在会话日志中**出现次数均为 0**;`requestId` 的粒度是**一整轮对话**(一次模型请求)。⇒ 记了两万行,却查不出是哪个工具贡献的。
|
|||
|
|
|
|||
|
|
**根因(源码级)**:`app.asar` 内偏移 ≈**117,230,304** 处的门控函数,注释写明「默认丢掉历史回放帧和流式 chunk」,但排除集合只列了两种**正文** chunk:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
var STREAM_CHUNK_UPDATES = new Set(["agent_message_chunk", "agent_thought_chunk"]);
|
|||
|
|
|
|||
|
|
function shouldLogEventMachineDispatch(sessionUpdate, replayingHistory, completeAssistantStream) {
|
|||
|
|
if (shouldLogContent()) return true;
|
|||
|
|
if (replayingHistory) return false;
|
|||
|
|
if (completeAssistantStream) return true;
|
|||
|
|
return !STREAM_CHUNK_UPDATES.has(sessionUpdate ?? "");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**漏掉了 `tool_call` 与 `tool_call_update`** —— 而工具的入参同样是流式增量,一次工具调用会产生几十到上百帧。**真正的洪水正是这两个被漏掉的 key。** 发出点在同归档偏移 ≈**117,210,567**。
|
|||
|
|
|
|||
|
|
**建议修复(一行)**:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
var STREAM_CHUNK_UPDATES = new Set([
|
|||
|
|
"agent_message_chunk",
|
|||
|
|
"agent_thought_chunk",
|
|||
|
|
"tool_call", // ← 新增
|
|||
|
|
"tool_call_update", // ← 新增
|
|||
|
|
]);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
预期日志体积下降约 **96%**:按 37 KB/次工具调用折算,10 MiB 上限对应的会话寿命从 **≈280 次工具调用** 提升到 **≈7,000 次**,正常会话再也撞不到顶。
|
|||
|
|
|
|||
|
|
### 问题二:轮转失败后该会话日志永久不再写入
|
|||
|
|
|
|||
|
|
**现象**:磁盘上出现刚好 **10 MiB**(10,485,760 B)整的日志且**不再增长**;`daemon.log` 反复报:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
[conversations] diagnostic log write failed {"code":"EPERM","droppedLines":2445,"droppedBytes":657690}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(`daemon.log` 出现 47 次、`daemon.old.log` 62 次。)
|
|||
|
|
|
|||
|
|
**活样本**:会话 `3f43ce71-….log` —— 大小 **10,485,710 B**(卡在距上限 **50 字节**处),最后写入时间 **16:55**,观测时刻 **18:04**(**一个多小时零写入**),同目录下**没有 `.log.1`**(轮转一次都没成功过)。
|
|||
|
|
|
|||
|
|
**根因**:`rotateFile()` 四步串行(删 `.log.3` → `.log.2→.log.3` → `.log.1→.log.2` → `.log→.log.1`),**只有四步全成才把已写字节计数器归零**。Windows 上任一步因文件被占用抛 `EPERM` ⇒ 计数器恒为满值 ⇒ 此后每次 flush 都重试轮转、次次失败、退避封顶 30 秒、每轮只报一次警 ⇒ **该会话日志永久写不进**。
|
|||
|
|
|
|||
|
|
**建议修复**:轮转任一子步失败时**也重置计数器**,或降级为"换名重开新文件",避免单次瞬时占用导致永久死锁。
|
|||
|
|
|
|||
|
|
### 复现步骤
|
|||
|
|
|
|||
|
|
1. 打开一个会话,连续执行较多工具调用(数十次以上)。
|
|||
|
|
2. 观察 `<配置根>\logs\<日期>\sdk\conversations\<session_id>.log`:体积快速增长(≈37 KB/次工具调用);用文本工具统计行标签,`event-machine:dispatch` 占九成以上,且绝大多数是 `input:"tool_call_update"` 的同一记录。
|
|||
|
|
3. 让该文件涨到 10 MiB(10,485,760 B)左右;此后文件不再增长,界面上的工具调用记录也不再出现(**静默哑掉**),而会话功能本身正常。
|
|||
|
|
4. 查看 `logs\daemon.log`,可见 `diagnostic log write failed` 与 `EPERM`。
|
|||
|
|
|
|||
|
|
### 影响
|
|||
|
|
|
|||
|
|
- 每会话约 10 MiB 上限被零信息帧快速耗尽 ⇒ 会话正常跑几小时后诊断日志即停写;
|
|||
|
|
- 停写**不报错、界面无提示**,排障时"看起来像卡死",实则丧失可观测性;
|
|||
|
|
- 磁盘占用被无价值内容放大近百倍。
|
|||
|
|
|
|||
|
|
### 我方环境(供参考)
|
|||
|
|
|
|||
|
|
- `WB_CONVERSATION_LOG_CONTENT` **未设置**。按源码它只读该变量,且置 `1` 只会**放大**日志(正文预览 + 恢复历史回放逐帧)⇒ **不存在"调小"的开关**。
|
|||
|
|
- 日志目录 `path.join(homeDir, "logs")` 与单文件上限 `PER_FILE_DISK_MAX_BYTES = 10485760` 均为**硬编码**;`perFileDiskMaxBytes` 虽写成可注入(`options.perFileDiskMaxBytes ?? 默认值`),但全包内**无任何调用方传值** ⇒ **外部无法调低或关闭**。
|
|||
|
|
|
|||
|
|
### 附件建议(提交时随附)
|
|||
|
|
|
|||
|
|
1. `daemon.log` 中 `[conversations] diagnostic log write failed` 相关片段;
|
|||
|
|
2. 一份卡住的会话 `.log`(如 `3f43ce71-….log`)+ 同目录文件列表(证明无 `.log.1`);
|
|||
|
|
3. 日志行标签占比统计的截图。
|
|||
|
|
|
|||
|
|
### 提交入口与必填项
|
|||
|
|
|
|||
|
|
- **应用内(推荐)**:右下角/左下角 **头像 → 设置 → 帮助与反馈 → 意见反馈**,粘贴上文,**勾选"上传日志"**。
|
|||
|
|
- **邮件**:`[email protected]`(一般 1~2 个工作日响应;紧急可在标题前加 `【紧急】`)。
|
|||
|
|
- 官方要求随附:**问题标题** · **WorkBuddy 版本**(设置 > 关于,本机为 5.6.2)· **平台**(Windows)· **问题描述** · **截图/日志** · **复现步骤** —— 均已包含在上文。
|
|||
|
|
- ⚠️ 应用内"提交"这一步**必须由使用者本人点**(本机可用的反馈工具只有微信支付专用的一个,不适配产品缺陷)。
|
|||
|
|
- 同款可直接复制版另存于桌面:`C:\Users\Administrator\Desktop\WorkBuddy-故障反馈-提交稿-20261001.md`。
|