Files
dsh_ai1net_server/交付物/宿主缺陷报告-会话诊断日志-20261001.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

165 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 宿主缺陷报告 · 会话诊断日志(丢帧 → 会话静默哑掉)
> **提交对象**: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` §四十