---
name: workbuddy-context-handoff
description: 给 WorkBuddy（桌面端 CodeBuddy Code 运行时）配置基于 hooks 的上下文 handoff 门禁 —— 上下文占用达阈值时自动强制产出 handoff 文档，新会话自动注入继续干活。也用于排查 WorkBuddy hooks / 上下文压缩阈值 / transcript 用量相关问题。
agent_created: true
---

# WorkBuddy 上下文 handoff 门禁

## 何时用

- 用户要"上下文到 N% 自动生成 handoff / 交接文档 / 新会话续接"
- 用户问 WorkBuddy hooks 怎么配、为什么自动压缩了、上下文用量怎么算
- 需要定位 WorkBuddy 桌面端的运行时行为（阈值、config option、hook 事件）

## 关键事实（已实测，别再猜）

1. **运行时**：桌面端 agent 跑的是 app.asar 里的 `cli/dist/codebuddy.js`（CodeBuddy Code）。
   查行为：解包 asar 后 grep 这个文件。
2. **hooks 位置**：settings 的 `hooks` 键。
   - 用户级 `~/.workbuddy/settings.json`（PRODUCT_LOWERCASE_NAME=workbuddy）
   - 项目级 `<proj>/.workbuddy/settings.json`
   - hooks 跨 scope 合并，同事件并行执行
3. **已实现的 hook 事件**：PreToolUse / PostToolUse / PostToolUseFailure / Notification /
   UserPromptSubmit / SessionStart / SessionEnd / Stop / SubagentStart / SubagentStop /
   PreCompact / PermissionRequest / WorktreeCreate / WorktreeRemove
4. **matcher 是正则**：`new RegExp(matcher).test(matchValue)`。matchValue 按事件：
   SessionStart→`source`(startup/resume/clear/compact)、PreCompact→`trigger`(manual/auto)、
   SessionEnd→`reason`、PreToolUse/PostToolUse→tool_name。
5. **hook 入参**没有 token 用量：`{session_id, transcript_path, cwd, permission_mode, hook_event_name, ...}`
6. **transcript**：`~/.workbuddy/projects/<cwd-slug>/<session-id>.jsonl`。
   逐行 JSON，取最后一次 `message.usage.input_tokens`（+`output_tokens`）即上下文体积。
   `cache_read_input_tokens` 是子集，**不要**再加一遍。
7. **内置压缩阈值**（product.json `tokenUsageThresholds.inputTokens`）：
   preMessage 0.5 / warning 0.6 / critical 0.7 / emergency 0.9。
   → **约 50% 就 pre-message 自动压缩**，原生到不了 80%。
8. **pre-message 压缩不受 `autoCompactEnabled` 控制**，也不读 settings；它读
   `session.meta.preMessageCompactPct`（ACP `session/set_config_option`）。
   桌面端 **Max Mode** 开关把它设成 `"0"` 关掉压缩，关掉开关设回 `""`（默认 0.5）。
   环境变量 `CODEBUDDY_PRE_MESSAGE_COMPACT_PCT` 也能覆盖（值 1-100）。
9. **PreCompact hook 不能做 handoff 触发器**：stdout 只进压缩指令
   (`hookContexts.preCompactInstructions`)；exit 2 只给用户看消息。
10. **能强制模型继续的只有 Stop hook**：`{"continue": false, "reason": "..."}`。
    **能注入新会话上下文的**：SessionStart hook 的 stdout 或
    `hookSpecificOutput.additionalContext`。

## 落地方案

三个文件放 `~/.workbuddy/hooks/`，然后在 `~/.workbuddy/settings.json` 注册：

```json
"hooks": {
  "Stop": [
    { "hooks": [ { "type": "command", "command": "<abs>/run-hook.sh <abs>/handoff-trigger.mjs", "timeout": 15 } ] }
  ],
  "SessionStart": [
    { "matcher": "startup|clear",
      "hooks": [ { "type": "command", "command": "<abs>/run-hook.sh <abs>/handoff-resume.mjs", "timeout": 15 } ] }
  ]
}
```

- `handoff-trigger.mjs`（Stop）：算 `input_tokens / window`，>= threshold 就输出
  `continue:false` + 写 handoff 的指令。必须做两件防死循环：
  `stop_hook_active===true` 直接 return；每 session 只触发一次（`state/<sid>.fired`）。
- `handoff-resume.mjs`（SessionStart）：注入本项目最新未消费的 handoff，
  注入后写 `<file>.consumed` 保证只注入一次。`source==="compact"` 时跳过。
- `run-hook.sh`：解析 node 绝对路径（managed → homebrew → PATH），
  找不到也 exit 0。**所有 hook 脚本异常都要 exit 0**，否则会卡住对话。
- handoff 目录按 `sha1(cwd).slice(0,12)` 分项目，别依赖 app 的 cwd-slug
  （不同版本大小写规则不一致）。

阈值可通过 `handoff.config.json` 调；`contextWindow` 留 null 时按
`settings.autoCompactWindow` → `CODEBUDDY_AUTO_COMPACT_WINDOW` → 200000 兜底。

## 排错

- **hook 不触发**：确认 `~/.workbuddy/settings.json` JSON 合法、命令用绝对路径、
  脚本可执行。手动复现：`echo '<payload json>' | /bin/bash -lc '<command>'`。
- **到不了阈值**：几乎一定是第 8 条 —— 50% 就被压缩了。开 Max Mode，或设
  `CODEBUDDY_PRE_MESSAGE_COMPACT_PCT=80`，或降低 handoff 阈值。
- **grep app.asar 搜不到字符串**：284MB 大文件直接 grep 会漏。用
  `npx @electron/asar extract <asar> <dir>`（3-4s，~800MB）再 grep。

## 交付规范

- 改 `~/.workbuddy/settings.json` 前先备份，改完用 `JSON.parse` 校验且确认其余键未丢。
- 脚本内不写 Unicode 制表/装饰字符（乱码）。中文可正常写。
- 测完清理 `hooks/state/` 与 `handoffs/` 下的测试产物。
