# dstalk 项目踩坑记录 (Postmortem) > **性质**: 历史记录,非设计文档。每个条目基于已发生的事故。 > **受众**: 全员。新员工/新会话第一份应读材料。 > **更新规则**: 每次新事故发生后,由 QA 追加一条记录并更新汇总表。 --- ## PM-001: clang-cl 增量构建 stale .obj 文件 - **首次发现**: Wave 5 / CEO 验收阶段 / `plugins/lsp/src/lsp_plugin.cpp` 及 `plugins/tools/src/tools_plugin.cpp` - **症状**: ``` # 编译通过,但运行行为与源码不一致;报错行号指向磁盘上已不存在的旧代码 # clang-cl 报告的 warning/error 行内容与当前源文件内容不符 ``` - **根因**: clang-cl 增量构建的时间戳/依赖追踪偶尔遗漏源文件的磁盘修改,重复使用过期的 `.obj` 缓存。并非 CMake 依赖图错误——`cmake --build` 认为目标已是最新,跳过重编译。 - **影响范围**: 任何使用 clang-cl 增量编译的 `.cpp` 文件;lsp_plugin 和 tools_plugin 至少各中招一次。 - **修复方法**: ```bash rm -f build/**/.cpp.obj # 删除可疑 .obj 强制重编译 cmake --build build --config Release ``` 彻底保险:`cmake --build build --config Release --clean-first`(但耗时)。 - **防御性规则**(同步到 WORKFLOW.md §6): - **R-STALE-OBJ**: CEO 验收编译前,若运行结果与源码不符,先怀疑 stale obj,`rm -f build/**/.cpp.obj` 重试。 - **检测方法**: - 对比 `.cpp` 的 mtime 与 `.cpp.obj` 的 mtime:obj 更旧但 `ninja -n` 显示 "no work to do" 时判定 stale。 - CI 中关键构建(PR merge / release)使用 `--clean-first`。 --- ## PM-002: Boost.JSON header-only 链接错误 - **首次发现**: Wave 3 (胡桐 W3) / Wave 7 (王测 W7) / 5 个插件翻译单元 - `plugins/deepseek/src/deepseek_plugin.cpp` - `plugins/anthropic/src/anthropic_plugin.cpp` - `plugins/session/src/session_plugin.cpp` - `plugins/lsp/src/lsp_plugin.cpp` - `plugins/tools/src/tools_plugin.cpp` - **症状**: ``` # MSVC linker (Release /MD): error LNK2001: unresolved external symbol "class boost::json::value ..." error LNK2001: unresolved external symbol "class boost::json::object ..." # 多个 boost::json 符号未解析 ``` - **根因**: Boost 1.86 废弃了 `BOOST_JSON_HEADER_ONLY` 宏——即使定义了该宏,编译器也忽略它,导致 header-only 模式不生效。缺失的 out-of-line 实现(`boost::json::value`、`boost::json::object` 等)在链接时找不到符号。 - **影响范围**: 任何 `#include ` 且未同时 include `` 的翻译单元。 - **修复方法**: ```cpp #include #include // 必须!提供 out-of-line 实现 ``` 每个使用 `` 的 `.cpp` 文件中,**恰好一次** include ``。 - **防御性规则**: - **R-BOOST-JSON-SRC**: 任何插件 `#include ` 时必须同时 `#include `;新增插件 CI 检查是否有缺失。 - **检测方法**: - `grep -rl '' plugins/ | xargs grep -L ''` — 有前者无后者的文件即缺陷。 - CI Release 构建必须链接成功(/MD 下 Boost.JSON 符号靠 src.hpp 提供,不链接 boost_json 库)。 --- ## PM-003: 跨 DLL 堆释放(/MT 必崩,/MD 侥幸) - **首次发现**: Wave 2.1 / 陈风 W2.1 (安全审计触发 + B3 评审) - `plugins/file_io/src/file_io_plugin.cpp` — 用 `::malloc` 分配,调用方用 `std::free` 释放 - `plugins/tools/src/tools_plugin.cpp:58` — `std::free(host 分配的指针)` - `plugins/session/src/session_plugin.cpp:166` — 同上模式 - `tests/smoke_test.cpp` — 三处 `std::free` 应改为 `dstalk_free` - **症状**: ``` # /MD (动态 CRT): 运行正常(共享 ucrtbase.dll,不同 DLL 的 malloc/free 落在同一个堆——侥幸) # /MT (静态 CRT): 立即崩溃 / heap corruption HEAP CORRUPTION DETECTED: after Normal block (#XXX) at 0x... # 或静默内存损坏,延迟在无关位置崩溃 ``` - **根因**: Windows 每个 DLL 拥有独立 CRT 堆(/MT)或共享同一个 CRT DLL 堆(/MD)。插件在自身 CRT 堆上 `malloc` 得到的指针,传给 host 后 host 调用 `free` —— host 的 CRT 尝试释放一个不属于它的堆上的地址 → 未定义行为。当前项目使用 `/MD` 故不立即触发,但这是一个精度定时炸弹(任何人改 CRT 链接方式就会引爆)。 - **影响范围**: 所有插件与 host 之间传递动态分配内存的场景。涵盖 `malloc`/`free`/`strdup`/`new`/`delete`。 - **修复方法**: ```c // 错误(跨 DLL 堆): char* buf = (char*)::malloc(256); // ... std::free(buf); // 释放者与分配者不同 CRT 堆 // 正确(统一堆): char* buf = (char*)g_host->alloc(256); // ... g_host->free(buf); // 通过 host_api 函数指针回到同一个堆 ``` 详见 `docs/reference/plugin-abi.md` §2-§3。 - **防御性规则**: - **R-NO-RAW-ALLOC**: 插件代码严禁直接调用 `malloc`/`free`/`strdup`/`new`/`delete` 处理跨 DLL 边界数据;必须使用 `host->alloc`/`host->free`/`host->strdup`。 - **R-TEST-MT**: CI 至少一个配置使用 `/MT` 构建并跑 smoke test,确保跨堆问题在任何 CRT 模式下暴露。 - **检测方法**: - `grep -nP '\b(std::)?(malloc|free|strdup|realloc)\b' plugins/**/*.cpp` — 任何命中需审计是否是私有堆操作(不跨 DLL 边界)。 - `grep -nP '\bnew\b.*\bdelete\b'` 同审计。 - ASan (`-fsanitize=address`) 在 Windows 下对跨堆释放敏感,可捕获 heap-use-after-free / alloc-dealloc-mismatch。 --- ## PM-004: plugin_loader 单插件失败导致全部初始化终止 - **首次发现**: Wave 2.1 / 陈风 W2.1 (side-effect 发现) → Wave 9.8 / 黄岭 W9.8 (正式修复) - `dstalk-core/src/plugin_loader.cpp` — `initialize_all()` 函数 - **症状**: ``` # 假设加载 A→B→C 三个插件,B 的 on_init 返回 -1: # 旧行为: [ERROR] Plugin 'B' init failed (code -1) # A 已初始化成功,但 B 失败后 initialize_all 立即 return -1 # C 完全未被尝试初始化——即使 C 不依赖 B # 症状: "明明只坏了一个插件,为什么其他插件也不工作?" ``` - **根因**: 旧版 `initialize_all()` 使用 fail-fast 模式 —— 任何一个 `on_init` 返回非零,整个函数立即返回 `-1`,剩余插件全部跳过。没有"部分成功"的概念。 - **影响范围**: 所有依赖于 `initialize_all()` 正常完成的调用方(CLI、GUI、测试)。一个低优先级插件失败会让整个应用退化。 - **修复方法** (黄岭 W9.8): - 将 fail-fast 改为 fail-continue:单个 `on_init` 失败 → `fprintf(stderr, ...)` + 标记 `failed_names` + `failed_count++` → `continue` 下一个。 - 依赖已失败插件的下游插件自动跳过并 `[WARN]`。 - 返回值语义: `0` = 全部成功;`>0` = 失败的插件数;`<0` = 严重错误(循环依赖 / host_api null)。 - 代码位置: `plugin_loader.cpp:202-256`。 - **防御性规则**: - **R-LOADER-CONTINUE**: 插件加载/初始化**永不 fail-fast**;单点故障不得级联阻断无关插件。 - **R-LOADER-RETVAL**: `initialize_all` 返回值 >0 表示部分成功(调用方必须区分 "完全失败" 和 "部分退化")。 - **检测方法**: - 单元测试:加载 3 个插件,中间一个 mock 为 "always init fail",验证第 1、3 个正常初始化,且返回值为 1。 - smoke test 验证所有 9 个插件加载(当前预期 9/9 通过)。 --- ## PM-005: git push --force 未在子代理 prompt 中禁止 - **首次发现**: Wave 1+ / CEO 规则层面 / 无特定代码文件 - **症状**: ``` # 子代理在独立会话中执行 git 操作,可能使用 --force 推送 # 远端历史被改写,其他协作者的本地分支与远端不一致 # 没有 git hook 阻止,也没有 prompt 模板明确禁止 ``` - **根因**: CEO 规则明确禁止 `push --force`,但该规则只在主会话中有效。子代理的 prompt 模板(`WORKFLOW.md` §9)没有包含此禁忌,子代理不知道这条规则。 - **影响范围**: 所有通过 `Agent` 工具派出的子代理。任何子代理执行 `git push` 时都可能误用 `--force`。 - **修复方法**: 1. 子代理 prompt 模板中增加:"禁止 git push --force / --force-with-lease,禁止 git reset --hard origin/*" 2. 可选:服务端 git hook(GitHub branch protection)禁止 force push 到 master。 - **防御性规则**: - **R-NO-FORCE-PUSH**: 每个子代理任务 prompt 必须包含 "禁止 git push --force / --force-with-lease";如需改写历史仅限本地 `git rebase -i`(未推送的提交)。 - **检测方法**: - GitHub branch protection rule: master 分支禁止 force push。 - 每次 CEO 验收后检查 `git reflog` 是否有 force push 痕迹。 --- ## 防御性规则汇总表 (§6) | 规则 ID | 事故 | 规则一句话 | |---------|------|-----------| | R-STALE-OBJ | PM-001 | 运行结果与源码不符时先怀疑 stale obj,`rm -f build/**/.cpp.obj` 重编 | | R-BOOST-JSON-SRC | PM-002 | `#include ` 的 .cpp 必须同时 `#include ` | | R-NO-RAW-ALLOC | PM-003 | 插件代码严禁 `malloc`/`free`/`strdup`/`new`/`delete` 跨 DLL 边界;统一用 `host->alloc/free/strdup` | | R-TEST-MT | PM-003 | CI 至少一个配置用 `/MT` 构建并跑 smoke test | | R-LOADER-CONTINUE | PM-004 | 插件加载/初始化永不 fail-fast,单点故障不级联阻断 | | R-LOADER-RETVAL | PM-004 | `initialize_all` 返回值 >0 = 部分成功,调用方必须区分 | | R-NO-FORCE-PUSH | PM-005 | 子代理 prompt 必须禁止 `git push --force` / `--force-with-lease` | | R-MAIL-SCOPE | Mailroom v1 | 子代理不得读写超出自身 inbox 之外的他人邮箱内容,仅可投递新邮件 | | R-MAIL-NO-DELETE | Mailroom v1 | 接收者不可物理删除邮件,只能移到 `agents/mailroom/archive/W/`;删除仅限 CEO | --- ## 变更历史 | 日期 | 版本 | 变更 | |------|------|------| | 2026-05-27 | 1.0 | W10.4 初始化。记录 PM-001 至 PM-005,共 5 条事故 7 条防御性规则。 |