# 会话复盘 · 取证手册("某个会话当时到底干了啥 / 为什么卡住") > 取证脚本:`scripts/forensics/proc-parent.py`(纯 ctypes 查进程父链,⛔ 不依赖 psutil)。 > 深度内容(每条的前因后果、实测读数)⇒ `references/pitfalls.md`。 ## 1 先定位会话 - 会话表:`sessions`(`title` / `custom_title` / `created_at` / `last_activity_at` / `cwd` / `status`)。 - 用户说「找**最新的**那个」时,**两个指标都算、取更大者**:`createdAtMs`(创建)与 `lastActivityAtMs`(最后活动)。 ⚠️ **两个指标打架 ⇒ 两条都列出来问用户,⛔ 别猜**。 - ⚠️ **同名多会话**在这一层很常见(标题会被复用)⇒ 必须靠 id / 时间 / cwd 三者交叉确认。 ## 2 读转录(原文在哪、怎么抽) - 转录是 JSON 行文件(`.jsonl`)。抽四类东西:**用户原话** / **AI 正文** / **reasoning 思考** / **工具调用**。 - ⚠️ **转录会被清理**(保留窗口有限)⇒ 过期后只能走 §6 的复原路径。 - 🔴 **别拿转录 mtime 当"有产出"**:agent 在跑时转录正文可能**滞后甚至不落盘**;反之活干完了也可能因推送失败而看起来没动。 ## 3 🔴 四种"卡住"必须分开判(这是本节的核心) | 型 | 症状 | 决定性判据 | |---|---|---| | **中断** | 那一轮从中间断了 | 转录尾部有**宿主注入的错误**(如 `## Model error retry: Tool Not Found`)⇒ 中断点;其后长时间零 `reasoning` / `function_call`。**瞬时工具不可用 ≠ 会话来死** | | **工具循环不收敛** | 消息派发了、agent 真在跑,但**永不回话** | 工作区日志状态机:`finish_reason=` **反复是 `tool_calls`,从不出现 `stop`**;`busy=true` 恒不回 idle | | **干完了推不出去** | 界面显示"一直运行",但看不到任何执行 | 日志里 `sendToClient: … SSE is closed` 大量堆积 + 该轮 `finish_reason="stop"` ⇒ **活干完了,结果送不出去** | | **投递悬挂** | 消息收到了、也入队了,但**没有任何东西去排空队列** | 🔴 **`route=parkInQueue` + `hasWaiter=false`**(正常是 `route=resolveWaiter` + `hasWaiter=true`) | 🔴 **投递悬挂的定量指纹**(把"症状"变"确诊"):`hasWaiter=false` 只会在出事那段出现、`queueLen` **只进不出**(每再投一次就再压一条)、同一小时 `No state found for connectionId` **飙升**。 ✅ **处置=让客户端重新挂上该会话**(切走再切回 / 重开该会话窗口);⛔ **别反复发消息试探**(只往队尾再堆一条)。 ⚠️ **两条别混**:同一次事故里常常**同时**出现"日志层异常"与"投递异常" ⇒ **分别归因、分别取证**,⛔ 不要合成一个因。 ⚠️ **别拿会话状态字段当判据**:客户端会把 `working` **基于错误假设主动写进库**,而宿主那边根本 idle;`last_activity_at` 的刷新也可能只是"UI 打开了这个会话"。 ## 4 日志撞上限(10 MiB)⇒ 整批丢写 **三条同时成立即可确诊**: 1. 守护日志反复出现 `diagnostic log write failed … EPERM`,且 `droppedLines` **持续增长**; 2. 该会话的 `.log` **大小卡在 ≈10 MiB 且 mtime 冻结**,而同期**其他会话的同名日志正常在长**(对照排除"全盘坏了"); 3. 同目录 `.log.1` 也是满的 ⇒ **轮转无空位**。 **修法(非破坏、可逆)**:把卡死的那个 `.log` 与 `.log.1` **改名挪开**(⛔ 不要删)。 🔴 用 Python `os.rename`,⛔ **不要用 `mv`**(MSYS 的 `mv` 在长路径/中文名下更易踩坑)。 **验收必做**:① 数十秒内被宿主**自动重建** ② 它**再次长大** ③ 告警**不再新增**。 ⚠️ **它必然复发**(每个会话涨到上限都会重演)⇒ 需要**持续兜底**(本工作区有扫描器 + 钩子兜底)。 ⚠️ 这两个文件**并没有被锁**(可正常打开)⇒ ⛔ 别再往"查谁占用文件"上耗时间。 ## 5 进程定性("它是不是某个会话的后台任务") ``` netstat -ano | grep ":<端口>" # 只认 LISTENING 那行 ⇒ 拿 pid python <本包>/scripts/forensics/proc-parent.py # pid ⇒ 父链 ``` - 父链出现 `… ← sandbox-cli.exe ← WorkBuddy.exe` ⇒ ✅ **是会话/宿主托管的后台任务**。 - 父链是 `services.exe` / `svchost.exe` / `explorer.exe` ⇒ ⛔ 不是后台任务(独立进程)。 - 更硬的证据在宿主日志里搜 `executeInBackground` / `background task created` / `processExit`。 - ⚠️ 日志里的中文常是「UTF-8 字节被按 GBK 解」的乱码,还原一行:`s.encode("gbk","replace").decode("utf-8","replace")`。 ## 6 转录已被清理时的复原路径(四条腿) 1. **全工作区 memory 日志批量搜"行为的独有名词"**(工具名 / 产物名 / 作品名)⇒ 一次命中就给出「工作区 + 日期」,比逐会话读 jsonl 快一个量级。 2. 读命中的那天日志(它通常写了工具链、命令要点、产出**绝对路径**)。 3. 按**独有产物名**跨盘搜文件系统(⚠️ 必须剪枝:跳过 `node_modules/.git/Windows/Program Files/AppData`,限制下钻深度;⛔ 别用全盘递归 `glob`,实测直接超时被杀)。 4. 与规范化文档互证(文档里常写「基于 <日期> 实测沉淀」)。 ⚠️ **交付时必须同时说明「原产出目录是否已失效」与「现存可核验的替代物」**,别让用户去翻一个不存在的路径。 ## 7 ⛔ 别做 - ⛔ 别因为 `status=working` 就断定卡死(当前正在跑的会话也是它)。 - ⛔ 别因为"抢不到锁"就去**删锁 / 接管**(红线,只能持有者释放)。 - ⛔ 别"再发一条试试"救投递悬挂的会话(会重开一轮同样的循环 / 只往队尾堆一条)。 - ⛔ 别在自动化会话里手动续聊当正式驱动。 - ⛔ 别为"恢复它"随手重启应用(会打断其他线正在跑的棒)。