Files

164 lines
9.0 KiB
Markdown
Raw Permalink Normal View History

# 宿主缺陷报告 · 会话诊断日志(丢帧 → 会话静默哑掉)
> **提交对象**:WorkBuddy 宿主(桌面版 / DSH 客户端)研发
> **取证环境**:Windows · `CODEBUDDY_CONFIG_DIR=E:\ProgramData\.workbuddy` · 宿主 bundle `cli/dist/codebuddy-lite-wb.mjs`(Sep 21 20:37)
> **取证日期**:2026-10-01 | **取证会话**:`ee3c2d82`(活跃 8 分钟后达 5.0 MB,最终 7.5 MB)
> **性质**:本报告**只做观测与建议**,⛔ 不涉及修改宿主任何文件(我们未改、也不建议本地改 `app.asar`)。
---
## 一、现象(用户可感知面)
1. 长会话**界面停止刷新**、看起来像"卡死",**没有任何提示**。
2. 后台协作程序向该会话**投递通知**(`ok:true` 记账成功)—— **实际全部落空,无人知晓(假绿)**。
3. 用户与 AI 双方**都收不到"这个会话已经废了"的信号**,只能靠人去猜。
⇒ 根因:**单会话诊断日志写满上限后宿主拒写(丢行),而"丢行"这一事实没有被任何一方观测到。**
---
## 二、缺陷 ① 🔴 日志 95% 以上是**零信息心跳行**
**实测读数(会话 `ee3c2d82`,20,925 行 / 5.0 MB 时点)**
| 项 | 值 |
|---|---|
| `event-machine:dispatch` 行 | **20,464 / 20,925 = 97.8%** |
| 单行字节 | **249 B** |
| 真正含内容的行 | **461 行(2.2%)** |
| 若只记"真事件" | 该文件应约 **115 KB**,而不是 **5 MB** |
**证据(连续多行,仅时间戳不同)**
```
2026-10-01T04:35:42.629Z event-machine:dispatch {"instanceId":"ci-1k","input":"tool_call_update","requestId":"01a0f5beea0c7d379ed2c7fd0003baf6","output":{"completeAssistantStream":false,"hasUsage":false,"hasTitle":false,"resourceEffectCount":0}}
2026-10-01T04:35:42.8xxZ event-machine:dispatch {"instanceId":"ci-1k","input":"tool_call_update","requestId":"01a0f5beea0c7d379ed2c7fd0003baf6","output":{"completeAssistantStream":false,"hasUsage":false,"hasTitle":false,"resourceEffectCount":0}}
```
🔴 **同一 `requestId` 下,四个布尔位**恒为 `false`、**`requestId` 也不变** ⇒ 这些行**不携带任何新信息**:
**该行的 JSON 里根本没有 update 的载荷**,只有"某帧到达了"这一个事实。**诊断价值 ≈ 0。**
### 🔴 根因已定位到源码行:`STREAM_CHUNK_UPDATES` 漏了 `tool_call_update`
**位置**:`app.asar`(主进程包,偏移 ≈117,230,304),`main/conversations.js`
```js
if (shouldLogEventMachineDispatch(update?.sessionUpdate, frameContext.replayingHistory,
result.completeAssistantStream === true))
this.init.log("event-machine:dispatch", { … });
/** 默认丢掉历史回放帧和正文流式 chunk;verbose 或 turn 收口帧仍落盘。 */
function shouldLogEventMachineDispatch(sessionUpdate, replayingHistory, completeAssistantStream) {
if (shouldLogContent()) return true; // env WB_CONVERSATION_LOG_CONTENT === "1" ⇒ 全开
if (replayingHistory) return false; // 历史回放帧 ⇒ 丢
if (completeAssistantStream) return true; // turn 收口帧 ⇒ 必写
return !STREAM_CHUNK_UPDATES.has(sessionUpdate ?? "");
}
var STREAM_CHUNK_UPDATES = /* @__PURE__ */ new Set(["agent_message_chunk", "agent_thought_chunk"]);
```
🔴 **读法(关键)**:宿主**已经**有一套"高频流式帧不落盘"的机制 —— 它把
`agent_message_chunk` / `agent_thought_chunk` 两类**主动丢掉**(注释原文:「默认丢掉历史回放帧和正文流式 chunk」)。
**但 `tool_call_update` 不在这个集合里** ⇒ 它**逐帧全部落盘**。
⇒ 而按第一节实测,**`tool_call_update` 恰好就是占比 97.8% 的那一类**(与"正文流式 chunk"完全同性质的高频帧)。
**建议(最小改动 · 一行)**:把 `tool_call_update` 加进该集合(或按 `requestId` 做状态变化去重):
```js
var STREAM_CHUNK_UPDATES = new Set(["agent_message_chunk", "agent_thought_chunk", "tool_call_update"]);
```
**预期收益(按本报告量化基线推算)**:日志量 **↓ ≈98%**(37 KB/次 → 约 0.7 KB/次)
⇒ 撞 10 MiB 的调用数 **280 次 → 约 14,000 次** ⇒ **"会话写爆日志而静默哑掉"这一问题基本消失**。
⚠️ **本地没有可用的开关**:我们完整检查了 `shouldLogEventMachineDispatch` 及其上游 ——
唯一相关的 env 是 `WB_CONVERSATION_LOG_CONTENT=1`,而它是**反方向**的(`shouldLogContent()` 为真 ⇒ **全部**落盘,只会更糟)。
⇒ **没有"静默档"可用**,所以这一项**只能由宿主侧修**(我们不会去改 `app.asar`:那违反我们的红线,且升级即被覆盖)。
**`input` 分布(同会话)**:`tool_call_update` 23,803 | `session_info_update` 434 | `usage_update` 165 |
`tool_call` 164 | `config_option_update` 10 | `current_mode_update` 3。
---
## 三、缺陷 ② 🔴 写满上限后**丢行而非轮转**,且**轮转成功与否不可预期**
**同日同目录(`logs/2026-10-01/sdk/conversations/`)横向对比:**
| 会话 | 当前文件 | 轮转件 | 结果 |
|---|---|---|---|
| `cb60cb60-…` | **10,485,737 B** | **0 个** | 精确撞顶后停止 |
| `5f607d3e-…` | **10,485,717 B** | **0 个** | 同上 |
| `f8a792ab-…` | **10,485,715 B** | **0 个** | 同上 |
| `a202550c-…` | 4,355,503 B | **3 个**(`.log.1/.2/.3`,各 ≈10 MB) | ✅ 正常轮转、会话存活 |
**`a202550c` 的轮转时刻**(文件 mtime):`.log.3` 00:56:07 | `.log.2` 01:14:59 | `.log.1` 01:39:13。
🔴 **读法**:**轮转能力是存在的**(上限 10,485,760 B = 10 MiB,`a202550c` 一天转 3 次、每转一次文件归零)。
但**另外 3 个会话在完全相同的上限上零轮转** ⇒ 轮转**不是必然路径**,**失败时静默降级为丢行**。
**可疑机理(未证实,需宿主侧确认)**:Windows 上重命名要求目标文件**可被其他持有者释放**
(`FILE_SHARE_DELETE`)。若轮转瞬间该文件仍被另一进程/句柄打开 ⇒ 轮转失败。
**⇒ 我们建议的验证**:在轮转失败分支上**打一条明确日志**(如 `ROTATE_UNAVAILABLE`)——
目前**失败是完全静默的**,这正是我们排查了两天的主要原因。
**建议**:
1. 轮转失败 ⇒ **必须留痕**(等级不低于 WARN),并**退避重试**;
2. 轮转失败 ⇒ **不要把"丢行"当作正常路径**:至少应向 UI 暴露"本会话诊断日志已满、已停止记录";
3. 🔴 无论轮转成败,**"日志已满/已丢行"应当是一个可被外部观测的状态**(见下节)。
---
## 四、缺陷 ③ 🔴 "日志快满 / 已满"没有任何外部信号
宿主对**自家 daemon 日志**有指纹限流(`scope+level+首参` 同指纹 1 秒最多 5 条),
但**单会话诊断日志**是零去重、零节流、零告警;而**写满后的"拒写"同样无声**。
**后果链**:`日志满 → 拒写 → UI 静默哑掉 → 协作程序投递仍记 ok:true(假绿) → 无人知晓`。
**建议(最小可用)**:
1. 达上限时**写入一条显式记录**(哪怕写到另一个文件),或
2. 对外暴露**可查询的会话健康状态**(如"本会话日志已停止写入"),或
3. 直接**恢复写入**(轮转 + 去重),使"写满"不再是终态。
---
## 五、我们这一侧已经做的(供对照,非宿主职责)
我们无法改宿主,所以在**宿主之外**补了一道**事前叫停**:
`07-scripts/session-log-guard.py` —— 挂在 `PostToolUse` + `UserPromptSubmit`,
**只读**日志字节数(`os.path.getsize`,⛔ 不读内容)⇒ 软 5 MiB / 硬 8 MiB 时向该会话注入一条提醒。
🔴 **它只能"叫停",⛔ 不能"降速"** —— 上面三条缺陷不修,会话寿命仍然只有约 **280 次工具调用**。
---
## 六、量化基线(可直接用于验收宿主侧的修复效果)
| 指标 | 实测值 | 修复后应变为 |
|---|---|---|
| 每次工具调用日志量 | **144 帧 = 37,502 B** | 大幅下降(去掉心跳行后应 ≈1 KB/次) |
| 调用频率(本机较忙会话) | **≈20 次/分** | 不变 |
| 达 5 MiB 所需时间 | **约 8 分钟** | 显著拉长 |
| 撞 10 MiB 的调用次数 | **≈280 次** | 不再是终态(轮转可靠) |
| 日志中零信息行占比 | **97.8%** | ≈0% |
**换算自证**:`10 MiB ÷ 37 KB ≈ 283 次`,与实测撞顶会话的 264~297 次吻合。
---
## 附:复现命令(只读,可在本机直接跑)
```bash
D="$CODEBUDDY_CONFIG_DIR/logs/$(date +%F)/sdk/conversations"
# ① 零信息行占比
ls -S "$D" | head -1 | xargs -I{} sh -c 'T=$(wc -l < "'"$D"'/{}"); H=$(grep -c "event-machine:dispatch" "'"$D"'/{}"); echo "$H/$T"'
# ② 每次调用的日志量
f="$D/<会话id>.log"; echo $(( $(stat -c%s "$f") / $(grep -c "\"input\":\"tool_call\"" "$f") ))
# ③ 撞顶未轮转的会话(应为空)
ls -l "$D" | awk '$5==10485737 || $5==10485717 || $5==10485715'
```
---
**报告人**:项目侧(会话 `ee3c2d82`)
**关联材料**:`接续包_日志事前叫停_20261001.md`(§1.5 / §6)|`.workbuddy/memory/2026-10-01.md` §四十