- 变更规模:新增 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/ 知识文件,按口径入库)
56 KiB
name, description, agent_created
| name | description | agent_created |
|---|---|---|
| workbuddy-session-forensics | 在 WorkBuddy 桌面版上定位「某个历史会话」并复盘它 —— 从会话标题反查会话 id、创建/改名/最后活动时间、工作目录;**抽取该会话的完整对话原文(用户原话 / AI 正文 / reasoning 思考 / 全部工具调用)**;并据此分析「AI 当时为什么这么做 / 为什么停下来问用户 / 为什么没按用户点名的方法走」。当用户说「继续 XX 会话的任务」「接着上次那个会话做」「改了工作区路径继续之前的会话」,或说「看下 XX 会话为什么…」「复盘某个会话 AI 为什么…」时使用。**也用于「会话卡住了 / 卡死 / 锁死了 / 没反应 / 一直转圈 / 发消息都不恢复」的排查**(★ **四型分开办**,判据见 §2f–§2j:②f「那一轮中途断了」· ②g「agent 在跑但工具循环不收敛、不给回话」· ②h「活干完了但结果推不出去」· **②i「投递悬挂:消息进了队列、没人取出来执行」**;**也用于问「某个进程是不是某个会话的后台任务 / 能不能一直开着」**(§2j:端口 → pid → 父链,看有没有 `sandbox-cli.exe ← WorkBuddy.exe`)。 | true |
WorkBuddy 历史会话定位(桌面版)
0. 为什么需要这套流程
用户常以会话标题引用一段工作(「继续『参考决策方法逐条判断处理』会话的任务」)。
桌面版没有给出「按标题查会话」的接口,conversation_search 对本机新建的近期会话可能返回 0 命中。
于是必须自己从本地落盘数据里反查。
✅ 结论(2026-09-16 复测):近期会话的原文就在本机、完全可读 —— 路径见 §1。 ⚠️ 旧版(2026-09-13)曾断言"近期会话 jsonl 不存在、只能靠间接源复原",该结论已被推翻(当时多半是踩了 §1 的「目录名的坑」:搬迁后新会话进了新目录名,只找旧目录自然找不到)。先按 §1 定位,别急着走 §3 的降级路径。
⛔ 但转录有「保留窗口」—— 2026-09-28 实测:本机只留近期会话(projects/*/*.jsonl 共 14 个、时间跨度仅 09-26 ~ 09-28;sessions 表 15 行、最早 created_at ≈ 09-26)。⇒ 用户问「上个月/更早的某个会话」时,转录与元数据大概率都已经不在了,此时别硬找 sid,直接走 §3 降级路径 + §2e 反查法,并如实告知「对话原文已不在本机,但工作区日志完整」。判断"是不是落在窗口内":先看 find <projects> -name "*.jsonl" 的最早 mtime,早于它的会话一律按不可读处理。
1. 三个数据源(各管一段,缺一不可)
| 数据源 | 路径 | 有什么 | 坑 |
|---|---|---|---|
| ① 会话元数据库(★首选) | ~/.workbuddy/workbuddy.db → sessions 表 |
id / cwd / title / custom_title / status / created_at / updated_at / last_activity_at / model / permission_mode |
✅ 2026-09-16 实测已可用且最新(67 行,含当天建的会话)—— 旧版"可能已静态化、最新行停在 09-11"的结论已作废。用 python sqlite3 以 file:...?mode=ro 打开(本机无 sqlite3 CLI) |
| ② 转录本体(★原文在这里) | ~/.workbuddy/projects/<工作区目录名>/<sid>.jsonl |
完整对话:用户原话 + assistant 正文 + reasoning 思考 + 全部 function_call / 结果 |
⛔ 旧版断言"近期会话根本没有 jsonl / 只能靠间接源复原" —— 2026-09-16 实测已作废:近期会话 jsonl 就在本机且是最新的(含当天 06:5x 的写入)。见下方「目录名的坑」 |
| ③ 会话日志(辅助) | ~/.workbuddy/logs/<YYYY-MM-DD>/sdk/conversations/<sid>.log(+ .log.1 轮转) |
每次 LLM 往返的原始记录(单文件可达 5–10 MB) | ⚠️ 旧版指路的 logs/<日期>/edge-sync.log 在本机已不存在 —— logs/ 现在是 main.log / renderer.log / sdk/ … 的结构,别再按老路径找 |
1a. 「新会话 / 空工作区,用户只说『继续』」怎么反查(2026-09-21 实测定型)
场景:会话是新开的(工作区里除了 .workbuddy/*.log 什么都没有),用户开口就是「继续」。此时没有上下文可继承,必须自己找出「继续的是哪条线」。按序三步:
- 先借 bash-guard / stop-dialog-guard 日志定位 transcript 路径:每个工作区的
<工作区>/.workbuddy/stop-dialog-guard.log每行都带cwd=…|tp=<转录路径>|…session_id…⇒ 即使workbuddy.db里查不到今晚的会话(实测本机sessions表最后一行停在 09-12,当天会话一行没有),也能直接拿到 transcript 的真实落盘路径。 tp=里的路径是相对/截断了前缀的(实测写成ramData-WorkBuddy-2026-09-21-22-11-36\16df8c5f-….jsonl,像是被切掉了盘符段)⇒ 别照抄,用 python 按 basename 反查: 遍历真正的 projects 根去找同名<sid>.jsonl。- projects 根跟着活动配置目录走:本机
CODEBUDDY_CONFIG_DIR=E:\ProgramData\.workbuddy⇒ 转录在E:\ProgramData\.workbuddy\projects\<工作区目录名>\<sid>.jsonl(不是~/.workbuddy/projects,按~找会 0 命中)。
拿到自己的 transcript 后,抽 type=message & role=user 的第一条(用 §2b 的 get_text,注意元素是 input_text 而非 text)—— 那就是本会话真正的起点,也就是用户说「继续」时指的线。
⚠️ 同一时刻用户可能对多个会话都发了「继续」(实测 22:12 前后三个会话的 guard log 都出现同样的 stdin_len=408)⇒ 别挑"最后活动时间最近的那个会话",只认本会话 transcript 的第一条用户消息。
环境坑(同一晚实测)
- bash 的 coreutils 会被宿主 shim 打断(
ls/find/tail直接 not found)⇒ 每条命令先export PATH="/usr/bin:/bin:$PATH"。 - PowerShell 工具 stdout 全空(连
Write-Output都拿不到回显)⇒ 列目录、查文件一律用 Bash + Python(os.listdir/os.stat),别在 PowerShell 上耗轮次。
⚠️ 目录名的坑(2026-09-16 实测,本次"以为原文不可读"的真正原因): 工作区目录名 = 工作区当前绝对路径的「去盘符、盘符后加
-、分隔符换-」形式 ——
E:\ProgramData\AI技能\aliyun-dsh-server→e-ProgramData-AI技能-aliyun-dsh-serverD:\AI技能\aliyun-dsh-server(搬迁前)→d-AI技能-aliyun-dsh-server搬迁工作区会换目录名 ⇒ 老会话留在旧目录、新会话进新目录,两个目录会同时存在。 ⇒ 找某个 sid 时两个目录都要
find(find ~/.workbuddy/projects -name "<sid>*"),只看一个会误判"原文不存在"。⛔
~/.workbuddy/sessions/*.json不是会话 —— 它们按 host 进程 pid 命名(如35764.json),内容是{pid, sessionId:"interactive-35764", cwd, url…}的进程心跳,没有任何对话。别被目录名骗了。
1b. 辅助索引(补充取证:这个会话动过哪些文件)
五个目录都按会话 id 命名,与转录同一套 sid:
| 目录 | 文件 | 内容 | 价值 |
|---|---|---|---|
~/.workbuddy/changes-index/ |
<sid>.json |
该会话每次文件写入的 summary(如「7 个文件变更,+340 −0」)+ 逐文件 filePath / action / additions / deletions |
★★ 能精确复原「这个会话动过哪些文件」 |
~/.workbuddy/changes-detail/ |
<sid>/ |
每次改动的明细 | ★★ |
~/.workbuddy/artifact-index/ |
<sid>.json |
产出物索引 | ★ |
~/.workbuddy/file-history/ |
<sid>/ |
文件历史快照 | ★ |
~/.workbuddy/file-tree-manifests/ |
<sid>.json |
工作区文件树快照(可达数 MB) | ★ |
1c. 成本 / 积分取数(2026-09-16 加,实测;只回答"这个会话花了多少")
✅ 纠正(2026-09-16 晚二次实证,两处都要改口径):rawUsage 有值,且带逐次 credit —— 把每个 JSON 行递归收集所有 rawUsage 节点再求和,得到的会话级积分与 session_usage.credit_json 逐位吻合(本次 5 个会话全部对上:9.37 / 8.93 / 33.71 …)。
⛔ 之前两次记成"恒为空"的原因:
d.get("rawUsage")抓不到(它不在顶层,藏在嵌套结构里)。⇒ 必须递归遍历,判据是"和session_usage对上"。
def walk(o, hits): # 递归收集 rawUsage 节点
if isinstance(o, dict):
for k, v in o.items():
(hits.append(v) if k == "rawUsage" and isinstance(v, dict) else walk(v, hits))
elif isinstance(o, list):
for x in o: walk(x, hits)
# 每行 json.loads(line) 后 walk(d, hits);积分 = sum(r.get("credit") for r in hits)
现成脚本:<工作区>/.workbuddy/tools/credit_by_sess.py。
| 想查 | 表 / 字段 | 说明 |
|---|---|---|
| 逐轮积分 | session_usage → credit_json |
JSON {<轮标识>: <积分>},各值即该轮消耗;used/size = 当前水位 / 上下文窗口。⚠️ 它只覆盖部分会话(本机实测 11 行 / 会话数更多)⇒ 全覆盖口径用上面的转录 rawUsage |
| 逐请求用量 | automation_runs → runs_json |
⚠️ 2026-09-16 晚实测:本库的 runs_json 里只有 cwd / success / startedAt / finishedAt / conversationId / output,没有 usage(旧笔记说"内有完整 usage"与本库不符,别再照抄)⇒ 要逐请求明细请用转录 rawUsage |
| 该运行开在哪个会话(★ 找"自动任务新建的会话"就靠它) | automation_runs → metadata_json → sessionId |
证明「每次自动化运行 = 新会话」;配 automations 表拿 name/prompt |
| 自动化周期 / 状态 | automations → rrule / status / next_run_at / deleted_at |
FREQ=HOURLY;INTERVAL=n ⇒ 每天 24/n 次;⚠️ status 仍是 ACTIVE 也可能是软删除(看 deleted_at),且 next_run_at 会停在停摆那天 |
实测成本模型(可直接引用)
- 积分 ≈ 单价 × 一轮内的工具调用次数(线性,样本 8 次运行)。
- 单价随水位上升:水位 <10 万 ≈ 0.10 积分/次工具调用;15 万 ≈ 0.41 ⇒ 同会话内约 4 倍。
- 固定注入 = 35,192 token/请求(tools 20,734 + systemPrompt 10,395 + skills 3,919 + mcp 144);但缓存命中 99.5% ⇒ 很轻,不是主因。
- ⛔ 「开新会话省积分」只对单价有效:三个自动化全新会话的首轮分别烧 8.93 / 10.05 / 13.82 积分(首轮 31–54 次工具调用)⇒ 压不掉"次数"的钱。
- 真杠杆排序:① 一轮内工具调用次数(线性、主导)→ ② 水位 → ③ 固定注入(很轻)。
现成脚本:<工作区>/.workbuddy/tools/cost_model.py(聚合全部自动化运行的 usage / 缓存命中 / 分类占比 / 周期清单)。
1c-1. 逐次 credit 的正确读法 +「钱花在新增上」(2026-09-24 实测 · 跨工作区复用)
⛔ rawUsage 节点的字段名与 message.usage 不同:有 prompt_tokens / prompt_cache_hit_tokens /
prompt_cache_miss_tokens / completion_tokens / credit;没有 input_tokens ⇒ 取那个会全得到 0(本次踩过)。
一次请求 = 一个 rawUsage 节点,每个节点自带自己的 credit ⇒ 会话总积分 = Σ credit。
实测结论(推翻"重发历史很贵"的直觉):
- 缓存命中率 98.4% ~ 99.3% ⇒ 缓存部分近乎不计费。
- ⇒ 积分 ≈ Σ(本次新增内容 × 全价);"新增" = 本次工具入参 + 本次工具结果 + reasoning + 回复。
- 固定注入(systemPrompt / tools / skills / 工作区规则)第 2 次起全部命中缓存 ⇒ 确实"很轻"(与本文件第 96 行互证)。
- 但单价仍随水位抬升:实测 30 万+ 水位段稳定 0.50 积分/次,低水位 0.25
0.36 ⇒ **约 1.52 倍**。
实物量级(2026-09-24 · dsh-decision-laya 两会话):105 次请求 = 27.2 积分;380 次请求 = 159.1 积分。 ⇒ 用户口中的「一轮会话 20–30 积分」= 一个会话跑了约 105 次请求,不是被某份大文件注入吃掉的。
1c-2. 「这个工作区到底受不受省积分钩子保护」判据(2026-09-24 定型)
省积分钩子(限流提示 / 拦大输出 / 技能守卫)的作用域 = 硬编码的目录名白名单:
stop-dialog-guard.py 与 skill-load-guard.py → _SCOPES_DEFAULT = ('aliyun-dsh-server','dsh-ai1net-desktop');
bash-output-guard.py → SCOPE = 'aliyun-dsh-server'(单值)。
⛔ 新建工作区不在白名单 ⇒ 三条保护全部静默失效(脚本照跑、日志照写、但一步都不做)。
一步判据(只读):grep -c 'in_scope=True' <工作区>/.workbuddy/stop-dialog-guard.log —— 得 0 ⇒ 该工作区从未被保护过
(⚠️ in_scope=False 也会写日志,别被"有日志"骗了)。对照:aliyun 同时刻 in_scope=True 且 预算告警=True。
脚本:<WS>/.workbuddy/tools/credit_curve.py <工作区> [sid前8位](单价/水位分桶 + 缓存命中率 + 分段积分占比)。
- ⛔ 旧版判据已作废:曾写「这五类目录与
.jsonl同一时刻集体停更 ⇒ 有文件=转录在本机、无=已纯云端」—— 2026-09-16 实测:它们全都在正常更新(changes-index/有当天 07:00 的文件),"有没有这批文件"不再能推断转录可读性。判断转录是否可读,直接find ~/.workbuddy/projects -name "<sid>*"。 - ⛔ 不要翻
~/.workbuddy/cache/conversation-product-spill/—— 名字像「会话产物」,实为 UI 配置产物(acc-product-config-*.json),不含任何对话内容。 - 次要源:
~/.workbuddy/workspace/sessions/<sid>(工作区侧会话态)、~/.workbuddy/logs/<日期>/sdk/conversations/<sid>.log。
★ 标题的两个坑(2026-09-13 实测补充)
- 自动改名会把标题截断到约 20 个字符(
RENAME … isUserDefined=false): 例:用户看到的「检测用户回到dsh页面并恢复进」实为「检测用户回到dsh页面并恢复进程状态」被切掉。 ⇒ 用户口中的"会话标题"常常只是前缀,反查时务必用中段关键词(如「回到dsh」)模糊匹配,别拿整句去 grep。 - 同一会话会连续被改名多次(创建后几秒内就可能改两次):
EB_SYNC_ADDED里的title=是首条消息原文,随后§3.4 RENAME才是 UI 上显示的名字 —— 判断"哪个 sid 是它"要看 RENAME 的末次值,不要只看 ADDED。 isUserDefined=true= 用户手动命名(不会截断);false= 系统自动生成。
2. 标准动作(按序,全是只读)
# ① 按标题反查会话 id —— 直接查元数据库(最快,2026-09-16 实测可用)
# python -c "..." 里:sqlite3.connect('file:<HOME>/workbuddy.db?mode=ro', uri=True)
# SELECT id,title,custom_title,created_at,updated_at,last_activity_at
# FROM sessions WHERE title LIKE '%关键词%' ORDER BY updated_at DESC;
# (本机没有 sqlite3 CLI;中文标题记得 ensure_ascii=False 输出、或写成 JSON 再 Read)
# ② 判断原文能不能读 —— 注意两个工作区目录名都要找(搬迁会换目录名)
find ~/.workbuddy/projects -name "<sid>*"
# ③ 会话日志(辅助,含每次 LLM 往返)
ls -l ~/.workbuddy/logs/*/sdk/conversations/<sid>.log*
- 时间戳是毫秒 epoch(
1789484860643)。⛔ 别用 awk 做ms/1000(会打印成1.78917e+09丢精度),日期换算交给 python。 - 本机
bash的 PATH 常被 shim 重置(find/grep/head/tail全 not found)⇒ 每条命令先export PATH="/d/Program Files/Git/usr/bin:/d/Program Files/Git/bin:/c/Windows/System32:/c/Windows:$PATH"。 - ⚠️ 复杂的中文正则优先用内建 Grep 工具,别在 bash 里拼(编码 + 转义双重坑)。
2b. 从 jsonl 抽取对话(★字段名是坑,2026-09-16 实测)
每行一个 JSON 对象,用 type 区分六种记录:
type |
关键字段 | 装什么 |
|---|---|---|
message |
role = user / assistant;content[] |
用户输入与 AI 正文 |
⚠️ 用户输入的 content[].type 是 input_text(⛔ 不是 text) |
message.role == "user" + content[] 里 type:"input_text" |
🔴 2026-09-30 实测踩到:按 type=="text" 抽 user 消息 ⇒ 抽出 0 条,于是误以为"这条会话没有用户消息/证据是假的"。正确姿势: {"role":"user","content":[{"type":"input_text","text":"…"}]} ⇒ 抽 (b.get("type") or "") in ("text","input_text"),或直接对文件 grep 唯一串(见下)。 |
reasoning |
rawContent[].text(该元素 type == "reasoning_text") |
AI 的思考过程 |
function_call |
name / arguments / callId |
工具调用 |
function_call_result |
callId / output |
工具结果 |
file-history-snapshot / ai-title |
— | 快照 / 标题生成,分析时忽略 |
⛔⛔ 最容易踩的坑:文本元素类型是 input_text / output_text,不是 text。
按 x.get("type") == "text" 抽取 ⇒ 每一段都抽成空字符串,且不报错(本次实测:14 条 user + 160 条 assistant 全抽空,白跑一轮)。
def get_text(d):
c = d.get("content")
if isinstance(c, str):
return c
out = []
if isinstance(c, list):
for x in c:
if isinstance(x, dict) and x.get("type") in ("input_text", "output_text", "text"):
out.append(x.get("text", ""))
return "\n".join(out)
- 过滤"真实用户输入":
<system-reminder/<task-notification开头的都是注入噪音,不是用户说的; 真正的话用<user_query>…</user_query>包着(新版本会把user_query放在 system-reminder 尾部)⇒ 用正则 先抠<user_query>,抠不到再看原文。 - assistant 正文可能极长(3.5 MB jsonl ⇒ 60 万字符)⇒ 先出"前 400 + 尾 1200 字符"的摘要版扫脉络,再对命中项拉全文,别一上来全文 dump。
- 本机 python 读写必须显式 UTF-8(
io.open(..., encoding="utf-8", newline=""));否则 CP936 静默乱码。
2b-bis 用户说「看最后 N 轮对话」时的两段式(★ 2026-09-18 实测定型,4 次调用出全文)
⛔ 别直接全文 dump(一个 3.5 MB 的 jsonl 会撑爆上下文)。两段式:
- 出索引:按 §2b 抽全部记录时给每条打上"记录序号
i",先只打印U / UQ的i | 行号 | 前 90 字符⇒ 一眼看出"最后 N 轮"落在哪几个i。 - 取区间:对
i ≥ <倒数第 N 轮的 i>再跑一次,整段落盘到tmp/<任务名>-<日期>/detail.txt(含REASONING/CALL <工具名>/ASSISTANT正文),再用 Read 分段读。
要点:① 记录序号 i 是"人可读的轮次锚点"(⛔ 别用行号 —— 一行可能含上万字符);② detail 模式必须把 reasoning 一起打印 —— "AI 当时怎么想的"就在这里,是复盘的关键证据;③ 工具调用只打 name、不打完整参数(参数体积不可控,需要时再单查)。
实测(2026-09-18 复盘 dd6abea4「覆盖网络线-序27执行棒-E3候选数可查」):索引 1 次 + 落盘 1 次 + Read 2 次 = 4 次调用拿到最后 3 轮全文(含 reasoning 与工具序列),足够定位"AI 为什么连续两次把用户口径读错"。
2c. 分析「AI 为何停下来问 / 为何没按方法做」(2026-09-16 定型)
同一套数据能直接回答"AI 的决策错在哪",动作是三段取证:
| 步 | 动作 | 能证明什么 |
|---|---|---|
| ① | 统计 function_call.name 的分布 |
Skill 计数 = 0 ⇒ 用户点名的方法论(如"参考决策方法")从未进入上下文;AskUserQuestion 计数 = 0 ⇒ 提问全发生在正文里(hook 拦不到) |
| ② | 抠出全部 message/user 正文 |
用户真实诉求与被驳回的上抛("按你的规划执行"、"XXX 不就行了")。⚠️ 注意用户回复里的**"还是…"**= 对 AI 上抛的二次纠正 |
| ③ | 全文搜 reasoning 里的判断语 |
决策方法 / 自决 / 上抛 / 门禁 / 拍板 / 边界 / R7 / R8 —— 本次正是靠这一步证明"AI 想过判据,但用错了判据(把 systemd 单元 ⇒ 只报告不动手 当成硬红线,于是把"该自己删的旧控制面"拿去问用户) |
产出形态:用户真实意图 → AI 实际动作(含 reasoning 原话)→ 判据对照 → 根因分层 → 我改了什么。
⚠️ 复盘结论必须落到规则文件或技能(否则下次照犯);本次落在 CODEBUDDY.md §1(规则冲突裁决顺序)+ 技能 dsh-decision-method §4.5。
2d. 「看自动任务新建的会话」怎么一次找齐(2026-09-16 定型)
用户说「自动任务新建的会话」「自动化开的会话」时,不要凭标题猜,按链走:
-- ① 元数据 → 拿到全部「自动化运行 = 新会话」
SELECT automation_id, status, metadata_json FROM automation_runs;
-- metadata_json.sessionId / conversationId 就是那个新会话的 sid
-- ② 会话名与时间
SELECT id,title,created_at,last_activity_at FROM sessions WHERE id IN (...);
-- ③ 自动化本身(名字 / prompt 原文 / 是否软删除)
SELECT id,name,status,scheduled_at,rrule,created_at,updated_at,deleted_at FROM automations ORDER BY created_at DESC;
⚠️ 两个作用域陷阱:① automation_update list 看不到库里的全部自动化(实测只见少数几条),要全量必须查库;② 自动化运行必带新 sessionId ⇒ "开了几个新会话" = automation_runs 的行数。
验收式复盘(判"这套机制到底有没有用"):把"该轮的预期口径"与实测并排 —— 预期通常写在上一个会话的 prompt / 立项记录里(如"≤10 次调用 / ≈1 分"),实测从 jsonl 数 function_call、从 rawUsage 求和。两列一对,结论立刻有据。
2e. 「转录已被清理,只记得做过某件事」怎么反查(★ 2026-09-28 定型)
场景:用户开口是行为的描述而非标题 —— 「之前有个会话抓取过线上视频、用 ffmpeg 提取视频关键帧,是哪个会话?文件在哪?」。转录窗口已过(见 §1 保留窗口),靠 sid 反查这条路是死的。改按「行为 → 工作区 → 日期 → 残留物」四条腿找:
| 步 | 动作 | 判据 / 命令要点 |
|---|---|---|
| ① 全工作区 memory 日志批量 grep(★主路径) | 用 Grep 工具搜所有 .workbuddy/memory/*.md,pattern 取该行为的独有名词(工具名 ffmpeg / yt-dlp / scene_0、产物名 frames_combined、作品名、账号名) |
一次命中即给出「工作区 + 日期」,比逐会话读 jsonl 快一个量级。⚠️ Grep 的 path 要给项目根(如 E:\ProgramData),不是单个工作区 |
| ② 读命中的那天日志 | 日志本身就写了工具链、命令要点、产出绝对路径 | 这是"SOP 从哪次实操沉淀而来"的唯一直接证据 |
| ③ 按独有产物名跨盘搜文件系统 | os.walk + 关键词(带剪枝:SKIP node_modules/.git/Windows/Program Files/AppData,depth>=5 停止下钻) |
验证产物是否还在。⚠️ 别用全盘 glob("C:/**/...", recursive=True)(实测直接超时被杀) |
| ④ 交叉验证规范化文档 | 工作区 references/操作规范/*.md、经验方法总结.md、SKILL.md 的强制检查节 |
文档里常写明「基于 <日期> 实测(<具体作品>)沉淀」⇒ 与 ① 的日期互证,能定位到比日志更早的首次实操 |
本次实测产出(可直接作范例):命中 aigc-idea-impression/.workbuddy/memory/2026-08-30.md → 该会话 = 抖音「王墨绫子」3 视频 + B站 BV1vcG36sEEr 的 yt-dlp 下载 + ffmpeg scene/I 帧抽帧;SOP 里"SOP 基于 2026-08-30 实测沉淀"与日志日期互证。残留物核验:产出目录 D:\MCNSkill项目\作品数据\ 与 ffmpeg 工具目录均已不存在,只有数据侧 JSON 迁到了 mcn-data-skills\RedFox数据\作品数据\<日期>\。⇒ 交付时必须同时说明「原产出目录已失效」与「现存可核验的替代物」,别让用户去翻一个不存在的路径。
⚠️ conversation_search(云端)也受窗口限制:实测默认只覆盖近一周;显式传更早的 start_date(如 2026-08-25)直接返回 HTTP 400。⇒ 别把它当"历史全量检索",只当近期补充。
2f. 🔴「这个会话卡住了 / 锁死了」怎么判(★ 2026-09-28 实测定型)
⚠️ 先分清两件不同的事,别一口咬定"锁":用户说「卡住 / 锁死」时,多数是会话那一轮中途断了,不是锁。按下面四步分开取证:
| 步 | 查什么 | 判据 |
|---|---|---|
| ① 读转录尾部找中断点 | 该会话 .jsonl 的最后若干条,看有没有 role=user 且正文含 error-recovery(如 ## Model error retry: Tool Not Found) |
🔴 这条 = 中断点:host 注入错误 → 该轮异常中断(既没继续也没收尾)。它后面若长时间没有 reasoning/function_call 记录 ⇒ 就是"看着卡死"的那段时间 |
| ② 看会话状态字段 | sessions.status:正常收尾 = completed;卡着 = working |
working 只说明"有轮次未闭合",⚠️ 当前正在跑时它也是 working ⇒ 必须结合 ① 的时间间隔判,不能只看字段 |
| ③ 查锁残留(逐个点名,别只查一个) | .exec-lock | .me-lock | .doing-* | .locks/.gate |
全都没有 = 锁没坏。⚠️ 只查 .exec-lock 会漏掉 .gate 泄漏(会让后续抢锁卡满超时) |
| ④ 复测那个"失败的工具/命令" | 重新调用一次 | 现在成功 ⇒ 瞬时故障,⛔ 不是工具被移除、⛔ 不是会话坏了 |
实测案例(2026-09-28):某会话 21:22:07 调一个当时不可用的工具 ⇒ host 注入 Tool Not Found ⇒ 转录 80 分钟零记录、status 停在 working、界面看着"锁死"。同时另一条线的自动化因抢不到全局锁而未开工(两件事互相独立,别混为一因)。⇒ 结论:① 工具瞬时不可用 ≠ 会话来死;② 真·抢不到锁 = 那一棒「未开工」(守规矩停手,但必须重排,因为一次性自动化已消耗、不会自动重跑)。
⚠️ 别做:⛔ 别因为 status=working 就断定卡死(当前在跑的会话也是它);⛔ 别因为"抢不到锁"就去删锁/接管(红线,只能持有者释放);⛔ 别把"工具瞬时不可用"记成"工具不存在"。
2g. 🔴 第二种"卡住":agent 在跑,但在工具循环里不收敛(★ 2026-09-28 22:5x–23:1x 实测定型)
⚠️ 本节初版曾判成「消息从不派发」,那是错的(当时只看了
sendPrompt与 node 进程,漏掉工作区日志里的状态机)。订正于 2026-09-28 23:16 —— 真相是消息派发了、agent 真的在跑,只是每轮只返回tool_calls、永不收尾,所以用户永远等不到回复。
与 §2f 的区别(一眼分):②f 有中断证据(error-recovery 注入后长时间零记录);本型通篇零错误,但工具有在被执行(只是不给用户回话)。
🌟 唯一权威判据:工作区日志的状态机(~/.workbuddy/logs/<日期>/<工作区名>__*.log)
grep -E "SessionRunStateMachine" <该日志> | grep <sid>
看这几项:
busy=/queueBusy=⇒ 恒true表示会话从未进入空闲MODEL_REQUEST_STARTED → TOOL_STARTED → TOOL_ENDED → MODEL_REQUEST_STARTED …反复循环- 🔴 决定性:模型侧
finish_reason=反复是tool_calls、从不出现stop⇒ agent 永远不产出面向用户的回复 = 用户眼中的"界面一直转、发了消息没反应"
三条辅助(按证据强度排序):
| 步 | 查什么 | 判据 |
|---|---|---|
| ① 后台任务 | 工作区日志搜 executeInBackground / background task created |
🔴 若该会话起了 --interval N --max-hours M 型常驻任务 ⇒ 它每 N 秒产一次输出、每次输出都当通知灌回会话 ⇒ 会话被反复唤醒,永远回不到 idle。这是最常见的放大器 |
| ② 会话类型 | sessions.session_settings 里若有 "automation":{"hostComposesUserContext":true,…} |
🔴 该会话是自动化会话 ⇒ 宿主会持续构造上下文并驱动它继续。在自动化会话里手动续聊 = 借用宿主驱动的地盘 ⇒ 它按"持续干活"语义跑,不会按"问答"语义回话 |
| ③ 上下文与压缩 | session_usage.used/size + 日志里 setSessionConfigOption: preMessageCompactPct=? |
preMessageCompactPct=0(关闭发消息前压缩)+ 长上下文(实测 ≈180K tokens、转录 5 MB)⇒ 单次模型响应可达 1.9 MB / 20 s,越跑越慢、越慢越像卡死 |
实测案例(2026-09-28 · 会话「手机↔WorkBuddy 通道 · 夜间就绪检查」/ sid 3a46cebb):该会话由一次性自动化 bc4eed39 于 19:30 创建(prompt 原文:「只读就绪检查…⛔ 不要启动任何常驻服务…本轮只做这一件事,做完即停」)。22:58 正常收尾后,用户在 23:00:40 手动发消息 → agent 起跑并持续工作(改 wb-supervisor-watch.py、23:02:46 起常驻后台任务 --interval 20 --max-hours 6、23:03:28 查 hook.log…),每轮 finish_reason=tool_calls,全程零回复;用户三次点停止(CANCEL_REQUESTED → FORCE_IDLE)都只能把那一轮掐掉,消息本身已作废。
当时误判的两条:🔴 「没有新 node 进程 ⇒ 从未启动」不成立 —— agent 是宿主进程(同一 pid)内的会话级运行,不另起 node;🔴 「转录 mtime 冻结 ⇒ 没在跑」也不成立 —— agent 在跑但只在内存/沙箱里动作,转录正文可以滞后甚至不落盘。
⚠️ 别做:⛔ 别"再发一条看看"(新消息会重开一轮同样的循环);⛔ 别只凭 sendPrompt 返回快、或没看到新 node 进程,就断定"没派发"(必须看状态机);⛔ 别把 daemon.log 里 [conversations] diagnostic log write failed EPERM(当日 08:35 起就在刷)当成当次事故的因;⛔ 别为"恢复它"随手重启应用(会打断其他线正在跑的棒)。
✅ 正确处置:① 用户消息已作废(不是"没收到"),要重新给一次;② 换新会话接着做(新会话不带那些坏条件,实测秒起);③ 若必须救该会话:先让它真 idle(点停止 → 等 FORCE_IDLE),再确认没有常驻后台任务在喂料(executeInBackground 起的进程要确认已结束),然后发一条极简的、一句话能答完的消息试探是否收敛;④ 把「会话内禁起常驻后台长跑任务」与「自动化会话不要手动续聊」两条写进作业规则(agent-operating-rules)。
2h. 🔴 第三种形态:界面显示"一直运行",但看不到任何执行(★ 2026-09-29 实测定型)
关键:这种症状必须拆成两问,否则一定误判 —— ① 它到底跑没跑?② 结果有没有送出去?
| 问 | 查什么 | 本机实测(2026-09-29 · sid 3a46cebb) |
|---|---|---|
| ① 跑没跑 | 工作区会话日志 <日志根>/<上一天日期>/<工作区名>__*.log 里的 [SessionRunStateMachine] + [ToolManager] execute | tool= |
跑了:07:06:08–07:06:28 连做 3 次工具(Edit 真写了 19.9 KB 文件、present_files),最后 AGENT_ENDED → idle busy=false(模型侧 finish_reason="stop",本轮输出 341 KB) |
| ② 送没送出去 | 同日志里数 [ACP StreamManager] sendToClient: Standalone SSE is closed |
没送出去:该文件累计 80,504 条(06 时单小时 56,154;峰值 06:31–06:37 六分钟 34k)。⇒ 宿主仍在往一个已关闭的客户端流推,会话的真实产出到不了界面 |
| ③ 转录佐证 | 该会话 *.jsonl 里 type=message 的最后一条 assistant 时间 |
停在 06:37:09;之后只有 file-history-snapshot。⚠️ 转录 mtime 仍在动 ⇒ 别拿 mtime 当"有产出"(同 §2g) |
| ④ 谁在驱动 | automations + automation_runtime_state |
该会话是后台自动化会话(is_background_automation=1、session_settings.automation.hostComposesUserContext=true);其 schedule_type=once、next_run_at=None、running=0 ⇒ 驱动器已耗尽,没人再驱动它 |
为什么"显示运行中":客户端拿不到回合结束事件(流已关)⇒ UI 永远停在 running。与 §2f/§2g 的区别:§2f 是那轮断了、§2g 是工具循环不收敛、§2h 是活干完了但结果推不出去。
⚠️ 放大器(本机实测):同一秒段里 [ACP Agent] setSessionConfigOption: preMessageCompactPct=0 每隔 ~15 s 被重推一次 ⇒ 发消息前压缩被关掉,在一个十几 MB / 十几万 token 的会话上 ⇒ 单轮上下文越滚越大、响应动辄几百 KB。
✅ 处置:① 先按 ① 判定"真跑 / 没跑",别一上来就说它卡死;② 结果是送不出去 ⇒ 修投递侧(客户端流),不是修会话;③ 查驱动:一次性自动化(once)跑完就不会再来,要续做得新建棒;④ ⛔ 别在 hostComposesUserContext=true 的自动化会话里手动续聊当正式驱动。
📂 旁证取证入口:logs/sandbox/<日期>/、logs/<日期>/sdk/conversations/<sid>.log、以及 wmic process 查网关端口归属。
2h-1. 🔴 真因与修法:会话日志撞上 10 MiB 上限、轮转失败 ⇒ 整批丢写(★ 2026-09-29 07:2x 实测定型,已实修)
判据(三条同时成立即可确诊):
daemon.log里反复出现[conversations] diagnostic log write failed {"code":"EPERM","droppedLines":…,"droppedBytes":…},且droppedLines持续增长(实测 12 秒涨 ~250 行、峰值丢 4 MB+);logs/<日期>/sdk/conversations/<sid>.log大小卡在 ~10,485,656 B(10 MiB 上限)且 mtime 冻结,而同期其他会话的同名日志正常在长(对照即可排除"全盘坏了");- 同目录已存在同样满的
<sid>.log.1⇒ 轮转无空位(.log要滚成.log.1,而.log.1也是满的)。
修法(非破坏、可逆、实测有效)——把卡死的那两个文件改名挪开(⛔ 不要删):
D="E:/ProgramData/.workbuddy/logs/<日期>/sdk/conversations"
TS=$(date +%Y%m%d-%H%M%S)
# 用 python os.rename(MSYS 的 mv 在含中文/长路径时更易踩坑)
"$PY" -c "import os;d=r'<上面D>';[os.rename(os.path.join(d,n),os.path.join(d,n+'.stuck-'+r'$TS')) for n in ('<sid>.log','<sid>.log.1')]"
验收(必须做):① 数十秒内 <sid>.log 被宿主自动重建(会先只有几百字节);② 它再次长大(实测 231 B → 334 KB / 70 秒);③ daemon.log 里该告警不再新增(实测改名后 90 秒零新增)。
⚠️ 关键认知:这两个文件并没有被锁(实测 append/rw 打开都成功)⇒ ⛔ 别再往"查谁占用文件"上耗时间;纯粹是"满了 + 没轮转空位 ⇒ 写不进去"。
⚠️ 代价:挪开的那两段是会话诊断日志(非数据原始记录,.jsonl 转录不动)⇒ 挪走只丢"日志",不丢会话内容。但已被丢写的内容无法恢复(转写里根本没写入)。
🔴 它会反复复发(2026-09-29 07:2x 实测):每个会话的日志涨到 10 MiB 都会重演 —— 实测在我修好 3a46cebb 四分钟后,16a5c457 与 2a0e12a8 又先后撞上限,丢写告警 103 → 153 条持续增长。⇒ 修一次不够,要么持续兜底,要么改宿主上限。
✅ 已落地的兜底(本工作区):
| 件 | 作用 |
|---|---|
$WS/.workbuddy/tools/wb-logcap-sweep.py |
扫描器:找出当天目录里≥9.9 MiB 且 mtime 冻结 >10 s 的 <sid>.log,改名挪开(⛔ 不删)。--dry-run 只看;--min-mb 调阈值;--quiet 静默。🔴 只认 logs/YYYY-MM-DD/ 目录名 —— 曾因 sorted()[-1] 取到 weixinpay 而静默失效(永远报"无需处理")⇒ 判据与修法见 §2i-1 |
$WS/.workbuddy/tools/logcap-sweep.cmd |
双击即用(先 dry-run,再问 Y/N 才动手) |
$WS/.workbuddy/tools/wb-result-hook.py → maybe_sweep_logcap() |
钩子兜底:域内会话 SessionEnd 时自动扫一轮(限流 120 s、硬超时 8 s、错误全吞)。钩子是宿主起的短命子进程 ⇒ 零 token、不占会话 |
⚠️ 扫描器必须只认「当天」目录(实测踩过):一开始扫了所有日期,把昨天已写满、早已停用的日志也误判成"卡住"白改了名。⇒ 两道护栏:① 只取 logs/ 下最新一天的目录;② mtime 超过 6 小时的一律不认。
⚠️ 改名用 os.rename(Python),不是 mv —— MSYS 的 mv 在长路径/中文名下更易踩坑。
⚠️ 本机 curl http://127.0.0.1:18899 的返回可能是假的:本机装了 Proxifier(Proxifier.exe),它会把回环请求也代理掉 ⇒ 端口没在监听时也会返回 502 upstream connect failed … (os error 10061)。⇒ 判"某端口有没有服务"必须看 netstat -ano | grep LISTENING,⛔ 不要凭 curl 的返回下结论。
2i. 🔴 第四种"卡住":投递悬挂 —— 消息进了队列、没人取出来执行(★ 2026-09-29 22:5x–23:0x 实测定型)
症状:用户在界面上发了消息(或某个程序把消息投了进去),界面一直转、永远没有回复;同时该会话没有任何执行迹象(转录不写、状态机不动)。
与前三型的区别(一眼分):②f 有中断证据;②g 工具在循环执行;②h 干完了推不出去;②i 是"消息收到了、也入队了,但没有任何东西去排空队列" —— 执行这一步从未发生。
🌟 唯一判据:投递那一行是 resolveWaiter 还是 parkInQueue(工作区日志 <配置根>/logs/<日期>/<工作区名>__*.log)
grep -F "<sid前8位>" <该日志> | grep -E "PromptIterator|prompt\(\) called"
route=resolveWaiter+hasWaiter=true⇒ 投递方阻塞等在那里 ⇒ 消息会被立即执行(正常形态)- 🔴
route=**parkInQueue**+hasWaiter=**false**⇒ 投递方没等 ⇒ 消息只是"停进队列",需要另一次触发去 drain;若该会话没有驱动源 ⇒ 消息永久躺在队列里
四条佐证(缺一不可,避免误判):
| # | 查什么 | 判据 |
|---|---|---|
| ① | 状态机最后一次转换 | AGENT_ENDED … to=idle busy=false ⇒ 上一次是正常收尾的,不是卡在循环里 |
| ② | 该会话全天投递路径分布 | 若只有最后一条是 parkInQueue(其余全 resolveWaiter)⇒ 这是孤例故障,不是配置问题 |
| ③ | 投递后有无执行 | 无 RUN_PREPARING、转录 mtime 与最后一条正文都早于投递时间 ⇒ 确定没被消费 |
| ④ | 有无驱动源 | 查 sessions.session_settings.automation + automation_runs:once 型 run 一旦 resultState=delivered 且 next_run_at=None ⇒ 驱动器已耗尽,不会有人来 drain |
2i-1. 🔴 定量指纹 + 恢复判据(★ 2026-09-30 复测复现 · 用户给了窗口 22:55–23:20)
这一步能把"症状"变成"确诊" —— 数全天两个计数,异常会自己跳出来:
# ① hasWaiter 分布:正常会话几乎全是 true
grep -a -c "hasWaiter=false" <该日志>
# ② 客户端状态丢失:只会在出事那个小时出现
grep -a -c "No state found for connectionId" <该日志>
本次实测(主会话 fe146dd9,2026-09-29)
| 计数 | 读数 |
|---|---|
hasWaiter=false |
全天仅 3 次,且全部落在 22:53:00 / 23:05:01 / 23:05:37(queueLen 0→1→2 逐条堆积) |
hasWaiter=true |
86 次(正常形态) |
No state found for connectionId |
22 点这一小时 303 次,其余小时 0 次 ⇒ 客户端连接状态在该小时反复丢失 |
| 恢复点 | 23:12:16 route=resolveWaiter … hasWaiter=true ⇒ 队列被排空;同一刻宿主 pid 由 32356 换到新实例 ⇒ 客户端重新挂上 |
⇒ 确诊口径:parkInQueue + queueLen 递增 + hasWaiter=false + 同小时 No state found 飙升 ⇒ 客户端没有 waiter ⇒ 没人 drain;
处置=让客户端重新挂上该会话(切走再切回/重开该会话窗口);⛔ 别反复发消息试探(只往队尾再堆一条)。
⚠️ 两层别混(同窗口常常同时出现):本次 22:58:40–22:59:50 还叠着 daemon.log 的 diagnostic log write failed(droppedLines 496→2580)
—— 那是"会话日志层"(§2h-1),不解释"消息卡住"。⇒ 一次事故里看到两个异常时,分别归因、分别取证,⛔ 不要合成一个因
(教训:multi-session-collab/references/pitfalls.md P0-2 / P0-5)。
⚠️ 与"诊断日志满额"(§2h-1)会互相放大:日志满额后,投递与执行过程在诊断日志里全空白 ⇒ 观感更像"死住"。排查 ②i 时顺手看一眼该 <sid>.log 是否已撞 10 MiB。
✅ 处置:① 那条消息已作废、不会自愈 —— 要重新给一次;② 别"再发一条试试"救它(若该会话是 hostComposesUserContext=true 的自动化会话,手工续聊不算正式驱动,见 §2g);③ 要续做该线 ⇒ 新建一棒(一次性自动化);④ ⛔ 不要删锁 / 接管(无锁残留)。
实测样本(2026-09-29 · 会话「接续 · 机制线(钩子锚点真实投递取证)」/ sid fe146dd9):22:06:05 正常收尾回 idle;此后 22:53:00 / 23:05:01 / 23:05:37 连续三次投递全部 route=parkInQueue / hasWaiter=false,且第二次起 queueLen=1(队列里压着前一条);宿主机零 RUN_PREPARING、转录最后写入 22:05:35 ⇒ 一条都没执行。而 22:53 之前的 13 次(21:11–21:57)全是 resolveWaiter 且正常执行 ⇒ 🔴 分界点在 22:53,不是"偶发一次",而是"从这里起变成常态"。
🔴 必须两侧日志对读(本次的关键):hasWaiter 是双方认知不一致的地方 ——
| 看哪边 | 文件 | 同一次投递记成什么 |
|---|---|---|
| 宿主侧 | logs/<日期>/<工作区名>__*.log |
[AcpView][PromptIterator] … route=parkInQueue hasWaiter=false ⇒ 不执行,只入队 |
| 客户端侧 | logs/<日期>/sdk/conversations/<sid>.log |
method:sendPrompt → state-machine:transition PROMPT_SENT from idle to valid to working → runtime-status:persisted {"status":"working"} ⇒ 自认"已发送、正在工作" |
⇒ 所以别拿会话状态字段当判据:客户端会把 working 写进库(不是"残留",是它基于错误假设主动写的),而宿主那边根本 idle。
🔴 客户端侧的三个特征行(一搜即中,比状态机更省事):
handlePost: Received cancel notification for session <sid>—— 用户在界面上点了「停止」- 紧跟
cancel: received cancel request+Ignoring cancel for idle session <sid>⇒ 停止无效(宿主认为闲着,可用户明明看到它在转) cancelAllInFlightPrompts: Scheduling sweep for 1 in-flight prompt(s)⇒ 那条"在飞"的 prompt 正是被 parkInQueue 挂住的那条(它被计入 in-flight,所以界面永远在转)
⇒ 「用户反复发消息 + 反复点停止都无效」是 ②i 的典型用户侧症状,可直接作为问诊入口。
⚠️ 四条踩过的坑(2026-09-29 23:0x 实测,血泪):
queueLen会累积、且不会自动重试:实测queueLen=0(22:53)→1(23:05:01)→2(23:05:37)⇒ 每再投一次就再压一条,队列只进不出。所以"用户又发了一条"要读成"又压了一条",⛔ 不是"重试成功"。- ⛔ 别拿
ending POST /api/v1/acp [Nms]的耗时当判据 —— 实测全天没有任何 ≥ 800 ms 的 POST(全是 1–5 ms),正常与异常完全一样。我一开始看[2ms]就推断"客户端发完即断 ⇒ 没人等",是错的(幸好回查证伪)。hasWaiter不是从 HTTP 连接时长推出来的。 - 🔴 最有价值的线索是"变点",不是那一条异常:把该会话全天的
PromptIterator行排成队看 —— 本次 13:56–22:03 共 30+ 次全是resolveWaiter / hasWaiter=true(全部正常执行),22:53 起才变parkInQueue。同入口、同前置动作(投递前那串set_mode/set_model/set_config_option前后完全同构),只有"等待者"这一项变了 ⇒ 说明投递方/入口在某一刻换了。别只看那一条就下结论"偶发"。 - 前置动作同构性要自己验:本次用"每次投递前 ±30 s 的
handlePost序列"做对照,确认异常前的那串配置请求与正常时逐条同构 ⇒ 才能把变量锁定在hasWaiter上,而不是瞎猜"是不是客户端版本变了"。
⚠️ 要查实现时的路径坑:parkInQueue / hasWaiter 在 .../Programs/WorkBuddy/resources/app.asar(本机 09-21 版)里零命中(resolveWaiter 命中的是无关的 MCP waiters)⇒ 运行中的宿主比那份 asar 新,字符串检索要基于实际运行的版本,别拿旧 asar 下结论。
⚠️
sessions.status与last_activity_at都不可单独作判据:本次二者分别显示working与22:53:00,而last_activity_at的刷新其实是客户端的配置同步动作(set_session_config_option/set_mode/set_model,即"UI 打开了这个会话")造成的,不代表执行。⇒ 一律回到状态机 +PromptIterator那两行。
2i-1. 🔴 兜底扫描器 wb-logcap-sweep.py 曾静默失效(★ 2026-09-29 23:0x 实测,已修)
判据:--dry-run 打印 「无需处理:没有 >= 9.9 MiB 的会话日志」,而明明存在满额文件(本次实测满额 10.0 MiB、已冻结 2.9 小时)⇒ 不要相信这句"无需处理",去查它挑中的是哪个目录。
真因:logs/ 下同时住着 weixinpay / update / startup / sites / sandbox / perf / migration / editor_sdk / Diagnostics / Crash-Log 等非日期目录;裸 sorted(it.iterdir())[-1] 取到的是 weixinpay ⇒ 该目录无 sdk/conversations ⇒ continue ⇒ 一个候选都扫不到。(讽刺的是:它"永远正常退出",所以钩子兜底也一直以为没事。)
修法:目录名必须匹配 ^\d{4}-\d{2}-\d{2}$。修复后首次运行即扫出 3 个满额并挪开(fe146dd9 冻结 10,438 s / f0fb0dbc 冻结 139 s / 2d81f349 冻结 1,618 s),40 s 内前两个已被宿主重建并恢复写入。
教训:这类"扫描-跳过"型兜底必须有自证(列出它扫的是哪个目录/扫到几个候选),否则"没动作"与"坏了"长得一模一样。
2j. 🔴 某个进程「是不是某个会话的后台任务」「能不能一直开着」(★ 2026-09-30 实测定型)
触发:用户问「XX 工作区那个会话开的服务/工作台,是不是后台任务?能不能一直开着?」「那个端口还活着吗?」
三步取证(全只读,⛔ 不碰那个进程)
# ① 端口 ⇒ pid(只认 LISTENING 那行)
netstat -ano | grep ":<端口>" # 例:TCP 127.0.0.1:8900 ... LISTENING 27112
# ② pid ⇒ 父链(技能自带,纯 ctypes,⛔ 不依赖 psutil)
python <skill>/scripts/proc-parent.py <pid>
# ③ 定性:宿主日志里搜「它是怎么起来的」
# 日志:<配置根>/logs/<日期>/<工作区名>__*.log
grep -a "executeInBackground\|background task created\|processExit" <该日志>
怎么读
| 读数 | 结论 |
|---|---|
父链里出现 … ← sandbox-cli.exe ← WorkBuddy.exe |
✅ 是会话/宿主托管的后台任务(挂在 WorkBuddy 进程树下) |
父链里是 services.exe / svchost.exe / explorer.exe |
⛔ 不是后台任务 ⇒ 独立进程(计划任务 / 启动文件夹 / 手工) |
日志有 [BashTool] executeInBackground … | no timeout (background) + [BashTool] background task created | taskId=XXXX | mode=pipe |
确认是后台任务,且无超时(不会到点被杀) |
日志有 [SandboxPipeHandle] processExit | processId=pipe-N | exitCode=… | killed=… |
它什么时候死过(killed=false ⇒ 自己退的,⛔ 不是被回收) |
⚠️ 日志中文是**「UTF-8 字节被按 GBK 解」的乱码**,还原一行:
fixed = s.encode("gbk", "replace").decode("utf-8", "replace")
存活边界(2026-09-30 实测,别凭直觉答)
| 事件 | 后台任务会掉吗 |
|---|---|
创建它的会话 status=completed |
❌ 不掉(实测:会话 00:18 已完成,:8900 仍在 LISTENING、HTTP 200) |
| 单次工具调用结束 | ❌ 不掉(⚠️ 与"前台子进程随调用结束被回收"正好相反) |
| 后台任务超时 | ❌ 不会(no timeout (background)) |
| 该工作区窗口 / 那个 WorkBuddy 实例关闭 | ✅ 掉 |
| WorkBuddy 整体退出 / 重启 | ✅ 掉,且不会自动回来 |
| 进程自己崩 / 改完代码要重启 | ⚠️ 会(实测 00:17:00 `exitCode=127 |
⇒ 一句话口径:「后台任务」跟着 WorkBuddy 活,不跟着会话活。 要「关了 WorkBuddy 也还在」必须做成独立进程(计划任务 / 启动文件夹)—— 后台任务做不到,⛔ 别承诺"一直开着"。
三个环境坑(本机实测,省 20 分钟)
- ⚠️ PowerShell 工具在某次会话里可能"零输出"(同一条命令在别的会话正常)⇒ 别在上面耗;用 bash 的
netstat+ 本技能的proc-parent.py。 - ⚠️
export MSYS_NO_PATHCONV=1之后 ⛔ 别把/e/...交给python.exe—— 原生程序会解释成E:\e\...(实测报can't open file 'e:\e\...')⇒ 一律传E:/...。 - ⚠️ Git Bash 里
tasklist /FI "…"会被路径转换吃掉(/FI变成E:/…/FI)⇒ 需要MSYS2_ARG_CONV_EXCL='*',或干脆不用它。
3. 原文读不到时怎么复原任务(三步 · 降级路径,先按 §1 实测能否读到)
- 工作区共享日志:
.workbuddy/memory/YYYY-MM-DD.md(多会话共用同一份,按小节标题 + 时间 + 里面的锁名ME=xxx-HHMM归属到具体会话);配MEMORY.md状态层。 - 项目文档库的「待办 / 交接单 / BRIEF」单一来源。
- 服务端 / 生产态只读实测(
git log、ls、cat package.json、cgroup 读数)—— 这是唯一能把"文档说"与"实际是"分开的证据层。
别做:不要凭
MEMORY.md一句话就下结论;文档常滞后于事实(本次实测就发现 3 处账实不符)。
4. 附带:工作区搬迁后必查的连环故障(2026-09-13 实证)
改了工作区路径后,第一件事是修「活动配置」里 hooks 的脚本绝对路径:
-
⛔ 先确认真·活动配置在哪(2026-09-13 血证,本技能旧版写错过):先跑
env | grep CODEBUDDY_CONFIG_DIR(本机 =E:\ProgramData\.workbuddy)—— 要改的是该目录下的settings.json。C:\Users\Administrator\.workbuddy\settings.json是搬迁前的遗留副本,改了完全不生效:09-13 同机两个会话各改一次(06:55 / 07:11),白改,还被误读成「hook 是启动时快照、新开会话也没用」—— 真实原因是改错文件。 -
症状:所有 Write/Edit 被拒,报
can't open file '<旧盘符>\...\xxx-hook.py'。 -
修法:只替换路径串(
sed -i),绝不整段覆盖hooks—— 它是多会话共享配置,顶层键覆盖会静默抹掉别人的钩子。 -
校验 A(配置本身):
python -c "import json;d=json.load(open('settings.json',encoding='utf-8'));print(list(d.keys()), list(d['hooks'].keys()))",确认顶层键与 hooks 事件都没少。 -
★校验 B(宿主到底执行了哪条命令 —— 最硬的证据,不用猜):看宿主日志
~/.workbuddy/logs/<日期>/<工作区名>__*.log[HookExecutor] spawn … timeout=… cmd=<命令逐字>← 宿主实际执行的命令串(可直接和配置文件比对)[HookExecutor] abnormal exit … code=N← 钩子崩了(脚本路径/解释器不对)⇒ 宿主 fail-closed:该机所有 Write/Edit 被拒- 有 spawn、无 abnormal exit、却没拦住 ⇒ 钩子跑通了但走了「放行」分支 = fail-open(脚本对读不懂的载荷放行是常见设计面)⇒ 去给脚本加一行「原样落盘 stdin」的诊断再复现。
- ⚠️ 钩子脚本的改动即时生效(脚本内容每次调用现读),只有
settings.json的 hooks 条目是应用启动时快照。
-
★hook 命令是「应用启动时快照」:改完
settings.json的 hooks 条目必须完全重启 WorkBuddy(关窗 ≠ 退出)才加载。 判据(2026-09-13 07:2x 实测,用的是活动配置、未被"改错文件"污染):07:25 改了活动配置 → 07:26:27 宿主仍执行旧命令 → 07:30:01 完全重启后 07:30:48 才执行新命令。 ⚠️ 注意:同一件事当天曾被写成"两次实测定论",但那两次改的都是非活动文件,不构成证据(见本节第一条)。 自证看宿主日志的[HookExecutor] spawn,或钩子自己写的低频日志(.workbuddy/lock-hook.log的SessionStart行)。 ⚠️ 一个会误导人的假象:若中途存在指向新路径的目录联接(如D://旧路径 → E://新路径),「改完就能写」会被误读成「配置是现读的」—— 当天同机两个会话独立踩了同一坑。判据:拆掉联接后再试一次,仍报旧路径 ⇒ 快照无疑。 🧯 应急兜底:此类钩子常有意不拦 Bash(避免把自己锁死)⇒ 路径失配期间可用 shell 写文件过渡。
5. 同名多会话:用户说「找最新的那个」时怎么办
判据(两个都算,取更大者):
createdAtMs(创建时间)—— 在edge-sync.log的§2 CREATEbody 里lastActivityAtMs(最后活动)—— 在§3.4.1 ACTIVITY行里
实例(2026-09-13):方案规划 有两个 —— ca58e09f(09-11 23:52 建 / 最后活动 09-12 16:54)与 ba6c2de4(09-12 17:32 建 / 最后活动 09-12 19:17)。两指标一致指向后者 ⇒ 判定安全;若两指标打架,两条都列出来问用户,别猜。
交付姿势(用户说「同步 X 到这个会话」时):
- 本机拿不到云端原文(见 §1 / §3),别承诺「我把原文搬过来了」。
- 正确交付 = 一份「会话同步包」:① 身份卡(id / 标题 / 改名史 / 生命周期 / 活动点)② 三条「原文不可读」的硬证据 ③ 该会话工作线的内容复原(用活动点与共享日志段落逐一对齐时间戳做归属)④ 它对当前状态的影响 ⑤ 「若确要原文」的可行路径。
- 落位:
<工作区>/.workbuddy/session-sync/YYYYMMDD-同步包-<标题>-<sid8>.md(放.workbuddy/下,不会被当临时产物清掉)。 - ⚠️ 归属必须标置信度:共享日志是多会话混写的,同窗并行会话的记录单列一节,别混进「这个会话做了什么」。