万象-会话交接与任务迁移
> 一句话:在上下文被压掉之前,把"现在在哪、下一步做什么"变成磁盘上的事实。
🔴 注意事项(硬约束 · 2026-09-23 立)
当任务上下文使用量达到阈值(默认 90%)时,不得中断或影响当前正在执行的任务。
阈值只表示"该准备交接了",不表示"可以停下来";相应的处理仅在当前任务执行完成后才触发。
| 时点 | 允许做什么 | 禁止做什么 |
|---|---|---|
任务执行中(含 PreCompact 压缩前那一刻) |
只登记待处理标记(毫秒级:不扫 transcript、不写大文件) | 生成文档、扫描大文件、改动任何任务状态、要求用户配合 |
任务执行完成后(Stop / FinalStop,本轮任务已跑完) |
生成/刷新交接文档,并提示补写 | 阻塞下一回合、打断用户后续操作 |
可核验判据:PreCompact 事件只写 .workbuddy/relay/pending.json;
交接文档只在 Stop / FinalStop 里产出。relay.log 会分别留下
PreCompact -> 仅登记待处理标记(不打断任务) 与 Stop -> handoff ... 两行。
为什么立这条:交接的目的是不丢进度;若为交接而打断正在跑的任务,
就变成"为了保住进度而破坏进度"。压缩是系统的动作,交接是我们的动作 ——
系统可以打断,我们不可以加码。
一 · 适用与路由
本技能是任务辅助总入口,进来先分流到三条线:
| 线 | 用户想干什么 | 走哪个脚本 | 落地物 |
|---|---|---|---|
| A · 会话接力 | 换个会话接着干 / 上下文快满了 | scripts/relay.py |
交接文档 + 指针 |
| B · 位置迁移 | 把任务挪到别的空间、临时↔空间、重复分组合并 | scripts/task_relocate.py |
产出 sessions.cwd 与记录槽位的修订脚本(只读读取、用户执行) |
| C · 合并归档 | 把几个相似任务并成一个、让原任务从侧栏消失 | scripts/task_merge.py |
合并包 + 归档字段 |
★ 三线的执行前提不同,这是本技能最要紧的一条:
| 线 | 什么时候才能动手 |
|---|---|
| A | 应用运行中(要读当时的 transcript 与水位) |
| B / C | 应用必须完全退出(要改数据库、要搬正被进程占用的会话文件) |
> 拿 A 线的时机去改库,会截断会话历史 —— 这是本技能最容易犯的错。
什么时候不用:
| 手上是 | 该走 | 为什么 |
|---|---|---|
| 要把项目交给别人接手 | omni-delivery-handoff(A 篇手册 / B 篇打包) |
读者是人,要可独立上岗;本技能读者是下一个会话 |
| 只是想让记忆更耐用 | omni-delivery-handoff(C 篇接续快照) |
C 篇是手动纪律(每轮回写快照);A 线是自动水位触发 |
| 整理文件 / 文件夹、回收站溯源 | omni-file-governance |
那是文件层治理;本技能只管任务 / 会话层 |
| 会话还在同一个窗口里顺利推进 | 什么都别做 | 交接文档是"换窗口"用的,不是每轮都写 |
先判一句:要换会话、要挪任务、要并任务(三者之一)才进本技能;否则不进。
与 C 篇的关系(重要,含一处推翻):
omni-delivery-handoff §C1 曾断言「别指望"上下文水位"信号,本机没有可靠的 token 计数器暴露给模型」。
该结论部分被推翻(2026-09-23 实测):模型自身确实看不到水位,但脚本能读到 ——
transcript 每条 message 带 message.usage.input_tokens(本轮会话实测 46,572 → 144,372 单调递增)。
所以正解不是二选一,而是两条一起用:C 篇负责"始终最新",本技能负责"跨窗口时不留死角"。
二 · 三条线的工作流
A 线 · 会话接力
- 测水位 —— 读 transcript 最后一条 usage,除以窗口上限 → 产出水位百分比。判据:拿到
used / window / pct三个数。 - 判触发(只在任务执行完成后) ——
pct ≥ threshold_pct(默认 90)为真,且当前时点是Stop/FinalStop(本轮任务已跑完)。
若此刻是PreCompact(可能落在任务中途)→ 只登记待处理标记,不生成任何文档(见文首注意事项)。
判据:relay.log能区分"仅登记"与"已生成"两种行。另外有 5 个百分点节流(regen_step_pct),防止每个回合都刷文档。 - 抽事实 —— 从 transcript 机械提取:产出物(Write/Edit 的 file_path)、命令(Bash/PowerShell)、任务、读过的对象;从三维记忆文件读标题索引。判据:每条都能回指到源文件行。
- 成文 —— 填八节骨架,机械段标「⚙️ 自动」,语义段留「✍️ 待补」占位。产出
<工作区>/会话交接.md+.workbuddy/relay/history/<时间戳>.md归档 +.workbuddy/relay/latest.json指针。 - 补写 —— 模型补全「✍️ 待补」五段(目标/进度/待办/决策/坑)。判据:五段都不再含
待补字样。 - 接续 —— 新会话里跑
prompt把「读 <路径> + 复述回读」发给接续者。
# 测水位(人工随时可跑)
python <技能目录>/scripts/relay.py probe --cwd "<工作区绝对路径>"
# 手动生成交接(用户说"上下文满了"时)
python <技能目录>/scripts/relay.py build --cwd "<工作区绝对路径>" --trigger manual
# 打印接续提示词(发给新会话的第一句)
python <技能目录>/scripts/relay.py prompt --cwd "<工作区绝对路径>"
# 冒烟测试
python <技能目录>/tests/smoke_test.py --cwd "<工作区绝对路径>"
// hooks/hooks.json 的三条接线(PreCompact / Stop / SessionStart)
{
"PreCompact": [{ "hooks": [{ "type": "command",
"command": "python \"${CODEBUDDY_PLUGIN_ROOT}/scripts/relay.py\" hook", "timeout": 30 }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "... relay.py hook", "timeout": 30 }] }],
"SessionStart":[{ "hooks": [{ "type": "command", "command": "... relay.py hook", "timeout": 20 }] }]
}
# 核心判据(scripts/relay.py · compute_usage 的等价简化版)
used = int(last["message"]["usage"]["input_tokens"]) # 累积上下文,非单轮增量
window = int(cfg["context_window"]) # 默认 200000
pct = used * 100.0 / window
trigger = pct >= cfg["threshold_pct"] # 默认 90
# 注意:cache_read_input_tokens 是 input_tokens 的子集,不可相加
B 线 · 任务位置迁移
归属键只有一个:sessions.cwd(侧栏按它分组)。以下两处须保持一致,脚本会一并纳入修订:
| # | 涉及项 | 位置 |
|---|---|---|
| ① | sessions.cwd |
~\.workbuddy\workbuddy.db |
| ② | 记录槽位目录 | ~\.workbuddy\projects\<cwd转写>\<会话id>.{jsonl,meta.json,file-rollback.ndjson} + <会话id>\ 全树 |
转写规则 = 盘符小写 + : \ / 一律转 - + 合并连续 -(<盘>:\workbuddy\X → <盘>-wordbuddy-X)。实测 41/41 命中。
| 操作 | 命令 |
|---|---|
| 临时任务 → 空间任务 | move --id <SID> --space <目标工作区> |
| 空间任务 → 临时任务 | move --id <SID> --to temp(只改开关,cwd 与槽位不动) |
| 空间任务 → 空间任务 | move --id <SID> --space <目标工作区> |
| 多条会话合并到同一空间 | merge --id A --id B --space <目标工作区> |
| 重复空间分组归并 | normalize [--dry-run] |
| 看现状 / 体检 / 回滚 | spaces · list --temp · slots · check · rollback --last |
硬判据:sessions.status = working 一律拒绝迁移(不得中断正在跑的任务);
运行中守卫(2 分钟内有心跳即中止);移动用 os.replace 而非 shutil.move;
逐文件 sha256;目标同名拒绝覆盖;每次运行写独立快照可回滚。
它解决的实景问题:库内同一工作区可能同时存在 D:/x/y 与 D:\x\y 两种写法 → 侧栏显示成两个空间分组。
normalize 只改库里的字符串、不搬任何文件(两种写法生成的槽位名相同)。实测本机有 2 处此类重复。
C 线 · 任务合并与归档
完整方案见 references/任务合并方案.md(判定标准 / 三层保留 / 原任务处理三问的正式答案)。要点:
- 判定:C1 同空间(硬判据)+ C2 标题 2-gram Jaccard ≥ 0.35 + C3 首条真实诉求 Jaccard ≥ 0.35;
「强建议」= C1 且(C2 或 C3);成组用 Kruskal + 单组上限 8(朴素并查集有连通传染,实测连出过 48 条)。 - 标题要从数据库补(默认开,
--no-db关):自动化会话的 jsonl 里没有标题,只靠文件侧会整批被跳过(实测跳掉 46 份)。 - 三层保留:L1 索引进新任务(3–10k token)/ L2 全文逐字节副本留
sources// L3 按需检索。
绝不拼接会话文件 —— 实测两条就 226,027 token,拼接后新任务开局即超窗。 - 原任务处理:
pack完全不动原任务;要让它从侧栏消失用archive(默认--mode deleted,
因为deleted_at是有实证的隐藏开关,archived的效果从未验证)。 - 本技能的脚本不写宿主数据库:数据库一律以只读方式打开(
mode=ro/immutable=1)读取现况;
一切改动只生成.sql修订脚本(同时产出.undo.sql可一键还原),
由使用者在应用完全退出后用sqlite3CLI 执行。脚本不做任何直连写入。
护栏:改前整组备份(db+-wal+-shm)+ 字段级快照 + 按 run 归档可回滚。 - 🔴
pack成功 ≠ 合并生效(2026-09-24 实测):两步不自动衔接,pack收尾只会提示你去确认。
用户报「昨天合并了但列表还是 N 条」时,先查deleted_at是否非空 —— 为None即归档从未执行,
不是异步/缓存/前端未刷新。排查口径见references/任务合并方案.md§3.1。 - 🔴 自动化日报是「序列」不是「重复」(2026-09-24):每天定时新建同名会话(如 07:30 日报),
因judge()不按创建时间判重,会被判为「强建议 / 标题相似 1.00」。
⇒ 归档只是止血,今天归档明天再生一条。判据:问「删掉它明天会不会又出现」 ——
会 ⇒ 是序列,需从自动化侧收敛(固定会话续写 / 定期批量归档)。详见 §5.1。
R="<SKILL_DIR>/scripts/task_merge.py"
python "$R" scan # 扫候选(只读)
python "$R" plan --ids A --ids B # 预览方案(只读)
python "$R" pack --ids A --ids B # 生成合并包(不动原件)
python "$R" verify --pack "<包目录>" # 校验完整性
python "$R" archive --ids A --ids B --pack "<包>" # 生成归档 SQL(不改库)
# ↑ archive 只出脚本,要生效须手工三步:
# ① 备份 workbuddy.db / -wal / -shm 三条
# ② 完全退出 WorkBuddy(托盘也退)
# ③ sqlite3 "$USERPROFILE\.workbuddy\workbuddy.db" < <生成的 .sql>
python "$R" restore --run last # 生成还原 SQL(同样手工执行)
四条实测坑:① 旧会话首条 user 消息常是宿主注入的提示词,不过滤会让所有会话"标题相似 1.00";
② 缺标题必须留空串(共用占位符会算成完全相似);③ <conversation_history_summary> 类标记会混进标题;
④ archived 与 deleted_at 是两套独立机制(archived 那 18 条的 deleted_at 全为空)。
三 · 失败模式与踩坑
| 现象 | 根因 | 处置 |
|---|---|---|
user_turns: 0,用户原话抽不出来 |
实测:role / content 在 jsonl 顶层,不在 message 之下 |
先读 o["role"],再退到 msg["role"];content 同理 |
| 用户原话变成一坨系统提示 | 本机 user 消息以 <system-reminder data-role="user-context"> 开场 |
优先截取 <user_query>…</user_query> 内容;否则剥掉 reminder 块 |
| 交接文档列表被劈成两半 | 命令/原话含多行,多行内容插进列表项会把列表截断(同 C4 铁律 6) | 落盘前把内容 " ".join(text.split()) 折叠成单行 |
python relay.py probe 找不到 transcript |
会话目录命名规则(盘符小写 + 分隔符转 -)不可靠 |
用 jsonl 里的 cwd 字段校验,不靠目录名猜 |
| hook 没反应 | 技能目录下的 hooks/hooks.json 不会被当插件加载(CLI 只认插件根的相对路径 hooks/hooks.json) |
见 references/接线指南.md:注册为插件,或走手动通道 |
| 水位百分比离谱(如算到 127% 仍未压缩) | context_window 设小了 —— 实测本机 200000 偏小(见 机制实证.md §9) |
跑 probe,按 stderr 的「窗口告警」把 context_window 调到 ≥ 实测 used;或改用 abs_threshold_tokens 走绝对量 |
| hook 明明执行了却"静默" | 分支内抛异常被兜底 except 吞掉(实测踩过一次:"读 " + Path 拼接报 TypeError) |
查 .workbuddy/relay/relay.log 的 hook error(...) 行;日志是唯一硬证据 |
- 失败 1(实测):内联
python -c写含中文/引号的内容会被 Git Bash 当命令执行、退出码仍为 0。→ 一律Write成.py再跑;本次冒烟测试脚本tests/smoke_test.py就是这个定式的产物。 - 失败 2(实测):
VAR=x; cmd && cmd这类复合命令在本机沙箱直接报sandbox-center cmd decisionRecord missing actual resource subject。→ 拆成单条命令,或写成脚本文件。 - 推翻条件:若日后客户端把
usage.input_tokens改成按"本轮增量"计(而非累计),则compute_usage的判据失效 → 改为对usage_seq做差。 - 边界:本技能保证"文档里有真事实",不保证"文档写得全" —— 语义段落(目标/决策/待办)靠模型补写,漏写时接续者必须回头问用户。
四 · 检查点
🔴 CHECKPOINT 0 · 不打断任务(最高优先级):生成动作只能出现在 Stop / FinalStop。
PreCompact(可能落在任务中途)只允许登记标记。违反即视为本技能失败 —— 判据见文首「注意事项」。
🔴 CHECKPOINT 1 · 生成前:确认 is_substantial 通过(工具调用 ≥5 或用户轮数 ≥2)。过短的会话不生成 → 避免在工作区堆噪声文件。
🛑 CHECKPOINT 2 · 交付前:交接文档必须满足三条 —— ① 八节标题齐;② 元信息段的水位与时间戳非空;③ 「✍️ 待补」段落已补写或已被显式标注"未补写"。缺项先别交付。
破坏性操作:会覆盖工作区根的 会话交接.md(每次覆盖,历史留归档)。除此之外无删除行为。首次运行前建议确认工作区根没有同名文件。
五 · 反例黑名单与红线
- 不许把交接文档写成成果汇报 —— 它给的是"接手者",不是"评审人"(成果汇报讲做了什么,交接讲现在在哪、下一步做什么)。
- 禁止在交接文档里抄长正文(设计文档、规则全文)。只放状态与索引,否则它自己变成一份要啃的长文。
- 禁止把凭据(API Key、口令、票据)写进交接文档或日志。
- 禁止在任务执行中生成交接或做任何耗时动作 —— 见文首「注意事项」,这是本技能的最高约束。
- 红线:
transcript与生成的文档可能含真实项目名/甲方名 —— 若要对外分享交接文档,先过脱敏(走omni-knowledge-distill的脱敏扫描)。
回滚与备份:归档目录 .workbuddy/relay/history/ 每次生成都留副本,可直接取回上一版主文件。本技能不改动 transcript 与记忆文件(全程只读)。
关于数据库(2026-09-24 起):本技能所有脚本都不写宿主数据库。需要改归属 / 归档时,
脚本只生成 .sql 脚本(连同一份 .undo.sql 还原脚本)放在备份目录,由用户先备份
workbuddy.db / -wal / -shm、完全退出 WorkBuddy,再用 sqlite3 CLI 手工执行。
只读连接用三级回退链(mode=ro → mode=ro&nolock=1 → immutable=1),
即使库目录不可写(只读盘 / 沙箱)也能正常读取。
六 · 资源
| 文件 | 干什么用 |
|---|---|
scripts/relay.py |
A 线核心引擎:probe / build / hook / prompt 四个子命令 |
scripts/task_relocate.py |
B 线:任务位置转移与合并(spaces / list / slots / check / move / merge / normalize / rollback / runs) |
scripts/task_merge.py |
C 线:任务合并与归档(scan / plan / pack / verify / states / archive / restore) |
hooks/hooks.json |
四事件接线(PreCompact / Stop / FinalStop / SessionStart) |
config.json |
A 线阈值、窗口上限、生成节流、文件名与归档开关 |
references/机制实证.md |
A 线判据的全部实测证据(字段、默认值、复验方法),只读取运行期可观测输入,可复算 |
references/接线指南.md |
hook 三种接线路径 + 验证步骤 + 不生效时的降级方案 |
references/任务合并方案.md |
C 线判定标准 / 三层保留 / 原任务处理三问的正式答案 |
tests/smoke_test.py |
A 线一键冒烟,退出码 0 = 通过 |
落账:每次生成追加一行到 <工作区>/.workbuddy/relay/relay.log(含水位与触发方式),便于回溯"什么时候交接的、当时多少水位"。
七 · 交付前自检
- [ ] 结构:frontmatter 十键齐;正文 ≥80 行;代码块 ≥3、文件名 ≥5。
- [ ] 内容:水位判据给出具体数值与来源字段,第三方可复算。
- [ ] 冒烟:
tests/smoke_test.py退出码 0,输出留档。 - [ ] 证据:交接文档八节齐,且「⚙️ 自动」段落的内容与磁盘真实情况一致(抽查 ≥3 条)。
常见误用与常见问题(FAQ)
常见误用(反模式)
| 误用 | 后果 | 正解 |
|---|---|---|
| 在任务执行中生成交接或做耗时动作 | 影响正在跑的任务 | 禁止 —— 这是本技能最高约束 |
| 上下文达到阈值(默认 90%)时中断当前任务 | 任务被打断 | 不得中断或影响当前正在执行的任务 |
| 把凭据(API Key、口令、票据)写进交接文档或日志 | 泄漏 | 禁止 |
| 在交接文档里抄长正文(设计文档、规则全文) | 它自己变成一份要啃的长文 | 只放状态与索引 |
迁移 sessions.status = working 的会话 |
会中断正在跑的任务 | 硬判据:一律拒绝迁移 |
| hook 明明执行了却「静默」,就以为没执行 | 实测踩过一次:分支内抛异常被兜底 except 吞掉("读 " + Path 拼接报 TypeError) |
查 .workbuddy/relay/relay.log 的 hook error(...) 行 —— 日志是唯一硬证据 |
常见问题
- Q:三条线怎么分? A:A 线会话接力/B 线任务位置迁移/C 线任务合并与归档;先走「一 · 适用与路由」。
- Q:有哪些反例不能踩? A:见「五 · 反例黑名单与红线」。
- Q:交付前要检查什么? A:见「七 · 交付前自检」;检查点见「四 · 检查点」。
- Q:回流是必需的吗? A:是 —— 见「八 · 经验回流(出厂件 · 硬条款)」。
- Q:失败模式在哪看? A:见「三 · 失败模式与踩坑」。
八 · 经验回流(出厂件 · 硬条款)
hooks/hooks.json—— PreCompact / Stop / SessionStart 三事件hooks/feedback.mjs—— 投递实现(自限时钟、失败静默、不造记录)FEEDBACK.md—— 写给使用者的说明
> ⚠️ 打包 / 上架时,hooks/ 与 feedback.mjs 必须提升到插件根(hooks.json 里用的是
> ${CODEBUDDY_PLUGIN_ROOT},技能目录下不会被加载 —— 已实测)。