-
## 定时:每日一次,挂在 DISABLE_BACKGROUND_AGENTS 早退之后 dreamSweep 串行扫所有本体,**不并发**:每轮可能起两次 LLM 调用, 几十个本体并发 = 同时拉起几十个 claude 子进程 —— 这个代价本仓库付过 (2h43m 漏出 82 个进程 / 17.5 GB,28-followups §五 第五条)。慢一点没有代价。 空转保护在 dreamOnce 里,所以「76 个本体全扫一遍」的实际成本是 76 次目录读。 定时器 unref —— 少了它,短命进程会被 24 小时的 timer 吊住不退出。 只挂定时**不立刻跑一轮**:启动那刻跑没有额外收益,反而让每次重启都烧一遍。 一个本体抛异常不拖垮整轮,但**必须留下是谁抛的**,否则现场只看到「今天没沉淀」, 查不出是没原料还是一直在抛。 ## 定义文件:受管区块 + 三道闸门 形状是一对 `<!-- dream:begin/end -->` 之间的**受管区块**,区块外一个字节不碰。 自由追加会让「哪些是人写的、哪些是 dream 写的」几轮之后再也分不开, 而分不开就没法分别回滚。区块整体重写还顺带**幂等** —— 同一批经验重复跑不会越写越长。 三道闸门,缺一不写: ① definition-write-gate:当前内容必须已在 git 历史里(你定的前置条件); ② **agent.md 的前 2000 字符必须仍然自足** —— 规划 agent 只看这一段 (graph-engine.js:915 的「角色定义摘录」),改了等于悄悄换掉它读到的画像; ③ 写完复核毕业闸门,不过就**立刻还原** —— agent.md 写坏会让本体雇不进任何公司。 任何一道不过:不写,并把原因逐个文件返回(`definitions[]`)。**不写不该是静默的。** ## 测试上又纠了自己一次 三条新用例第一版是「在测试里比对形状」而不是真调 —— 和上一轮同一个毛病。 改成对**真被 git 跟踪且干净**的本体(dream-screener)走真路径: 真写、验区块外字节不变 / 幂等 / 毕业闸门仍过,finally 还原并断言还原干净。 agent.md 那条同理真调,验「不写 + 理由点名前 2000 字符 + 一个字节都没动」。 dreamSweep 加 agents 参数,让错误隔离那条能真调而不是真扫 76 个本体。 全量:144 文件 / 1330 测试全绿,仓库干净。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 前置:存量记忆的搬家(19 文件 / 269 行) 写入侧上一轮改了,但**存量不会自己搬家**。实查现有 `## 关键事实` 里 **没有一条真事实** —— 全是重复心跳行、工具调用 id、和以 `...` 开头的半句。 而 memory.md 会被注入**每一轮**提示词,这些东西一直在冒充事实。 scripts/migrate-memory-raw-section.js 把它们搬进「原始记录(未提炼)」。 先 dry-run 再 --apply,改前逐个 .bak(这些是 gitignore 的运行态,没有 git 历史兜底)。 判据刻意窄:只搬 `- [YYYY-MM-DD] … → …` 这一种形状(旧自动追加的唯一形状)。 **搬错比留错更坏** —— 留下最多多一条噪声,搬错是把人写的东西降级成日志。 ## 真跑:innovator-evolver,40 条原料 { "distilled": 4, "accepted": 4, "rejected": [] } 耗时 114s 四条产出都做到了抽象:yt-dlp / Invidious → 「本地下载工具 → 备用镜像源」; scorer.js / SKILL_v1.md / system/templates/... 一个都没出现。 **顺着 sources 抽查过**(可追溯的第一次实际用途):4 条引用全部对得上原料。 「按关键词覆盖率打分会被刷到虚高分」那条引了 #21/#22/#27/#28,而 #27 原文正是 「从零重写,采用 expected_output 逐 case rubric + dimCeil 上限机制,杜绝 coverage-based」。 **它是跨 4 条记录归纳出来的**,不是逐条改写 —— 正是「一次调用吃掉全部原料」 那个选择要换来的东西。 ## 闸门单独验过:零拒绝 ≠ 闸门有效 真跑里 4 条全过,说明去敏闸门**一次都没触发**。所以单独喂了三条给 dream-screener 真跑: · 含公司名+域名+绝对路径 → false,理由点名「域名 lemonai.ai 和绝对路径 /Users/…」 · 心跳日志原文摘录 → false,理由点名「原文搬运」「工具调用 ID 可关联到具体运行实例」 · 干净的抽象经验 → true 三条判据都按 agent.md 写的那样生效,**且拒绝理由都点到了具体是哪几个字** —— 那是 agent.md 里明确要求的。 ## 空转保护实跑 第二次调用(原料没变):{"skipped":"no-new-material"},一次 agent 调用都没起。 ## 还没做的 定时挂载(决策 C)、写定义文件(SKILL.md / soul.md / agent.md)、 决策 D(质量怎么评 —— §5.5 那次抽查是**人工**顺着 sources 回查的,不是自动的)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
## 两个本体(都过得了毕业闸门) · dream-distiller —— 把一批原始记录提炼成可复用经验,交 ```json 数组 {title, body, sources};没有值得留的就交 []。 · dream-screener —— 逐条判「这条能不能给别家公司看」,交 {shareable, reason}。 **决策 A 的独立性写在两处,缺一不可**: · 代码层:判定**一条一次调用**,提示词里只放 title + body,**不放原料**; · 本体层:dream-screener/agent.md 明写「你看不到原料,也看不到是哪家公司 —— 这是故意的」。守卫里有一条专门断言这句话在 —— 否则下一个人会顺手把原料 塞进提示词「让它判得更准」,而那恰好把独立性拆掉了。 soul.md 也承担判据的一半:screener 的人格写的是「你是一道闸门,不是审稿人。 审稿人的本能是帮它改好,闸门的本能是不确定就不放」。 ## 两处失效方向都是硬的 · 提炼取不出 JSON → **抛**,不是静默返回空。「产出格式坏了」和「没提炼出东西」 是两件事,混为一谈会让前者以后者的形式永远藏下去 —— 每天显示「今天没经验可沉淀」,而真相是解析一直在失败。 · 判定解析不了 / shareable 不是布尔(比如给了字符串 "true")→ 判否, 并把原文片段带进 reason,让人看得出是「判否」还是「判炸」。 ## 纠了自己两次 ① 测试第一版把解析与失效分支**在测试里重抄了一遍**再断言 —— 那是在验一份复制品, 代码改了它照样绿。改成加一条 setDreamAgentCaller 缝,只换掉「真起 CLI」那一层, 解析、判空、失效方向全走真代码。 ② agent.md 的相对引用按 **cwd 深度 3**({aid}/{cid}/{taskId}/)解析,不是按文件 自身位置 —— 仓库根是 ../../../../../。我按文件位置写了 ../../../, agent-md-refs.test.js 当场判红。**守卫替我抓的,不是我自己发现的。** ## 还没做的,写进文档 §5.4(别当成 dream 上线了) · 定时挂载没做(决策 C 说每日一次):现在只有 dreamOnce(agentId) 这个可调用单元, 没挂调度、没 REST 入口。 · 还没写定义文件:只写本体根的 memory/experience.md(纯经验积累,删掉无副作用)。 三个 git 跟踪的定义留给下一步,闸门与 definitionWritability() 已就位, 缺的是「往里写什么、写成什么形状」那半边设计。 · **没有一次真 CLI 验收**:CI 里换掉的是 callDreamAgent 那一层,验的是解析与失效方向, 不是模型写得好不好。两个本体的提示词**至今没有被任何模型读过** —— 这正是 P3B W4 记过的形态(mock 从不读提示词)。 · 决策 D(提炼质量怎么评)仍未定;可追溯是它的便宜起步,不是替代品。 全量:144 文件 / 1323 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
## 步骤 2:「有哪些公司」只认 company_agents 表 system/agents/{aid}/{cid}/ 是**约定不是校验**。实查 96 个二级目录里混着 train/ workspace/ 和带时间戳的测试残留(tmp-w7-*-1786986291739-*)。 拿目录枚举公司 = 把测试夹具当经验原料喂给提炼 agent。 改成查表,另补保留段 _global(全局 agent 的运行态落点,没人「雇」它所以不在表里)。 **目录从此只用来定位文件,不用来枚举公司。** 守卫里真造了 train/ 和 tmp-w7-* 两个目录,断言它们不进原料。 ## 空转保护:水位线按「条数」不按 mtime 实查 75 份 memory.md 只有 7 份有非模板内容 —— 每日跑起来绝大多数 agent 没有新原料。 不拦的话每天几十次「读完发现没东西」的 LLM 调用在烧钱。 **判据用条数而不是 mtime**:mtime 会被任何一次无关写入推动(例如别处重写整份 memory.md),条数只在真的多出记录时增长。用 mtime 的话,空转保护会在 「文件被碰过但内容没变」时失效。 ## 决策 A 的失效方向写进代码 去敏判定 agent **抛异常也算拒绝**,并把「为什么」写进 rejected[]。 否则现场只看到「少了一条」,查不出是判否还是判炸。 「少沉淀一条经验」与「泄漏一次机密」代价不对称,判据必须偏向贵的那一侧。 ## 步骤 5:可追溯 每条原料带 source({公司}:memory.md#{行号}),提炼产物落盘时把 sources 写进 experience.md 的引用行。没有它,dream 的产出无法验证对错 —— 与 FDE 那边「LLM 自己汇报分数」是同一个问题。 ## 步骤 3 还差什么(写在文档 §5.3,别当成做完了) 编排 / 空转 / 闸门 / 落盘 / 可追溯都跑通了(6 例守卫), **缺两次真 agent 调用的默认实现**:提炼 agent 与去敏判定 agent 的本体还不存在。 默认实现现在是一句显式 throw(「尚未接线」)而**不是静默返回空** —— 静默返回空会让调用方以为「跑了但没提炼出东西」,那是本仓库反复出现的失效形态。 也还没写定义文件:writeDistilled 目前只写本体根的 memory/experience.md (纯经验积累,删掉无副作用)。三个 git 跟踪的定义留给下一步, definitionWritability() 已能报出「现在能不能写、不能写是为什么」。 全量:144 文件 / 1316 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
29-DREAM §四 的五步里先落两步。 ## 步骤 4:本体定义文件的 git 历史闸门(决策 B.3) 用户定案:soul.md / agent.md **直接落盘**,但写之前必须确认这份文件已经在 git 历史里 —— 别导致直接修改而没有历史。 definition-write-gate.js 两条判据都满足才放行: ① 文件被 git 跟踪;② 该文件没有未提交改动。 判不出来(不在仓库 / git 不可用)一律拒绝 —— fail-closed。 **为什么是「拒绝并报告」而不是「脏就先替它提交一把」**:后者看着更顺手, 但那正是本仓库记过的一类事故 —— 多会话共用同一个 worktree(27-P3C §15.4)。 一份未提交的改动很可能是**另一个会话正在写的东西**,替它提交等于把别人的 半成品写进 main 且署错名。把「要不要提交」留给人。 守卫 5 例,含「拒绝时必须给出下一步」那条 —— 撞上闸门的人(或 agent) 要能照着做,否则只能猜。 ## 步骤 1:memory.md 的自动写入不再冒充「关键事实」 原实现取任务与结果的第一行、砍到 80/100 字符,写进 `## 关键事实`。 那不是提炼,是搬运 + 截断。实测产物(cto/_global/memory/memory.md): - [2026-08-20] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01RsQ3gh...) **工具调用 id 被当成了「关键事实」**,而 memory.md 会被注入每一轮提示词。 28 §5.6b 点名的错误版本是「自动把原文写进共享 SKILL.md」—— 这套已经是那个错误的单公司版,只是还没跨公司共享所以还没造成泄漏。 改法: · 换段 —— 自动写入落到「原始记录(未提炼)」,`## 关键事实` 只由提炼产物填。 读的人和 agent 一眼分得清「日志」和「结论」;这一段同时是 dream 的输入面。 · 滤掉确定的噪声(工具调用标记 / 空结果 / 只有心跳标记的任务), 但**不猜「哪条重要」**——猜重要性需要理解内容,那是 dream 的活, 在这里做等于把提炼下放到一个正则里。 · 不再截成半句:上限放宽到 200 并取完整第一行。截断残渣读起来像事实、 其实缺主语,比不留更坏。 ⚠️ 踩到一条:噪声判据初版有「任务与结果都短于 8 个字符就丢」。那是按英文长度 想出来的阈值 —— 中文 8 个字是一句完整的话,实测「结果甲甲甲甲甲」(7 字) 整条被丢。已删该规则。**凡是拿长度当信息量代理的判据,先问一句这是按哪种文字算的。** 全量:143 文件 / 1310 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
28-followups §1.1 一直是「方向已定、设计未定」。这份把取舍摆出来并记下拍板结果。 ## 先看清原料:它现在是脏的(全部实查,不是推测) · **写进 memory 的不是「事实」,是截断的日志行**。appendToMemoryMd 的全部逻辑是 「取第一行、砍到 80/100 字符」。cto/_global 的实际内容里,三条「关键事实」是 `🔧 [TOOL_CALL] Bash (ID: toolu_01RsQ...)` —— 工具调用 id。 §5.6b 点名的错误版本是「自动把原文写进共享 SKILL.md」;**现在这套已经是那个错误的 单公司版**,只是还没跨公司共享,所以还没造成泄漏。 · 长期记忆是一个 3000 字符的滚动窗口,没有按重要性保留的机制。 · 96 个「公司态目录」里混着 train/ workspace/ 和**带时间戳的测试残留** (tmp-w7-*-1786986291739-*、qroute-co-13/14)。dream 第一步「枚举所有公司」 现在没有可靠答案 —— 可靠的答案在 company_agents 表里,目录只能用来定位文件。 · 75 份 memory.md 里只有 7 份有非模板内容。**dream 现在几乎没有原料。** ## 四条拍板(2026-08-20) A 去敏 = 独立判定 agent + 写作要求。失效方向是「少沉淀」不是「泄漏」—— 两种错代价不对称时判据偏向贵的那侧,与 P3B §八 FAILING_VERDICTS 同一条经验。 B 落点 = memory/ 与 SKILL.md 都写,**必要时迭代 soul.md / agent.md**。 C 触发 = 定时每日一次。 D 顺序 = 上游修复与 dream 一起做。 ## B 把范围扩大了,随之而来的约束单列一节 「本体只有一份」意味着写 agent.md / soul.md 影响**所有雇了它的公司**, 而不是提炼出这条经验的那一家。所以四个落点按可逆性分层,并记了四条硬约束: · agent.md 有一处 2000 字符截断 —— **作用点要说准**:那是给规划 agent 看的 「角色定义摘录」(graph-engine.js:915),agent 自己那条装配链不截断。 判据因此是「保证前 2000 字符仍然自足」,不是「不许追加」。 (初稿我按记忆写成「装配时只读前 2000 字」,查了代码才改对。) · 这四个里三个是 git 跟踪的**定义**,不是运行态 —— dream 改它们会进 git status。 · 毕业闸门读这些文件,写坏会让本体雇不进任何公司。 · 必须能分别回滚:经验写坏只影响上下文,agent.md 写坏影响所有公司。 ## 还没定的 决策 D(提炼质量怎么评);B.2 里「定义文件直接写还是走提案」—— 我建议走提案并写进了推进步骤,但那是建议不是拍板。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 病症(真跑实测) 2026-08-20 那条 run 的 timeline 里,**每个节点**的产物索引写入都失败: node.output | 产物索引写入失败 contract.json: UNIQUE constraint failed: index 'idx_graph_files_path' node.output | 产物索引写入失败 sdd.md: UNIQUE constraint failed: index 'idx_graph_files_path' 19 次 agent 调用、每次两三个文件,无一成功。 ## 根因:加了新索引、没删旧索引 产物索引加「版本」之前,唯一索引是无条件的 `UNIQUE(company_id, COALESCE(task_id,''), rel_path)`。加版本之后建表块换成了 带谓词的 idx_graph_files_current(WHERE superseded_by IS NULL),**但没人删旧的那条** —— `CREATE ... IF NOT EXISTS` 只管不重复建,不会删已有的。 于是 recordFile 那套「先把旧行 superseded_by 指向新行、再插新行」的版本化写法 在存量库上整个失效:旧索引不认 superseded_by,同路径第二行直接撞。 而重试恰恰会让同一条 rel_path 被产出第二次。 ## 失效方向:静默 writeFileIndex 把 recordFile 的抛降级成一条 timeline,**节点照样 success**。 后果是产物索引在任何有重试的 run 上都不全 —— 而下游节点正是靠这份索引 按 kind 找上游产物。不报错、不影响测试、git status 干净。 ## 为什么没有任何测试照到 新建库压根没有那条索引(建表块里早就只剩 current 那条),**只有存量库会踩**。 这是同一类问题的**第三次**(tasks 加列的索引顺序、墙钟锚点迁移、这次)。 判据固定成一句话:**凡是碰 schema,「存量库重启」那条路必须单独走一遍。** ## 修 + 验 迁移里一句 DROP INDEX IF EXISTS idx_graph_files_path。 守卫 graph-files-stale-index.test.js 走存量库重启那条路,并验版本化语义没被削弱 (同路径两行、当前版本恰好一条、旧行指向新行)。反向验过:不删就红。 生产库已实跑验证:删索引后同路径连产两次不再抛,graph_files 24 行未受影响 (备份 /tmp/graph.sqlite.pre-idxdrop)。 全量:141 文件 / 1300 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
28-followups §3.1。**审计结论与那条待办原来的写法不一样,两列要分开处理。** ## source_kind:删 与 origin 一一映射(global_agent↔builtin / company_hired↔created), 实查生产库 76 行零例外。全仓唯一读侧是 migrate() 里那条 origin 回填, 而那是给「origin 列出现之前写下的行」用的一次性迁移。 顺序:**先回填、再删列**,两步都用 hasColumn 守着(幂等)。反过来做不报错 —— 库能开、测试能过,只是那几行 origin 悄悄是错的。守卫 source-kind-drop-migration.test.js 走存量库重启那条路,专门钉这个顺序, 反向验过:弄反立刻红。 写侧散在 3 处生产 INSERT + 35 个测试文件。批量改的时候正则误伤了 4 处 (吃掉了 JS 默认参数、位置占位符、ON CONFLICT 的赋值行),逐个修回并复查了 diff —— **这种跨几十个文件的机械改动,靠「全量跑一遍」兜底比靠正则写得对更可靠**。 ## legacy_path:不能删,它还没死 原待办说「所有读取点都已改成从 id 推导」——那半句对新行成立,对存量行不成立: · 新行 legacy_path 恒等于 system/agents/{id}(确实冗余); · **存量行里存着批次 4 之前的公司侧路径**,而 graph-seed-service 判「悬空行 / 孤儿行」 靠的正是这个路径指向的目录还在不在。从 id 推导出来的路径永远指向 system/agents/, 那条判据会整个失效。 实查生产库:76 行里还有 2 行是老路径(org-planner / org-coder 指着已不存在的 data/companies/mycompany/agents/)—— 正是这条判据要认出来的那种行。 退出条件写进了 graph-db.js 的建表注释(不只留在文档里): 库里不再有 legacy_path <> 'system/agents/' || id 的行。 ## 顺带记一条:getDb() 会就地迁移生产库 删列那次,生产库的列是被一条**查状态用的**一行 node -e 删掉的 —— 服务当时没重启,人也没打算迁移。结果是对的(幂等;与备份逐行核对 76 行一致、 origin 全部按映射回填正确),但「读一下状态」不该顺手改 schema 这件事值得知道。 闸门只挡测试进程,故意不挡普通脚本(挡了服务自己也起不来)。 注释里给了只读的正确姿势。 全量:140 文件 / 1299 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
## 验收 4:从「静态成立」变成真的跑过一次 原状态是 🟡「静态成立,未真跑」—— 人读代码把 fixture verdict → FAILING_VERDICTS → readNodeProduct → callAgentNode 判 fail 这条链读通了,但没有任何一次真跑或真链路证据。 现在 graph-real-chain-smoke ⑥ 走完整条:fixture 产出 verdict="attributed" → 节点 fail、fail_reason=input_underspecified、沿 fail 边点亮下游、 判词落在**下游节点**行上(判词的载体是「点亮它的那次状态转换」,不是失败节点自己)。 反向验过:verdict 改成 passed 立刻红。 写 fixture 时踩到一条值得记的:fail_reason 是封闭域 ATTRIBUTION_REASONS, 第一版编了个 contract_invalid,被**按设计降级成 null** 并留一条 timeline —— 引擎不认识的归因一律不信。判据没错,是 fixture 错。 ## 验收 2 / 8:由 2026-08-20 那次真跑验掉(§六ter) · 2 —— 19 次真 agent 调用之后 `git status --porcelain` 完全为空,交付物都在项目根内。 这正是 §六bis「后半被并行 agent 打破」那条的干净环境重验。 · 8 —— 判据取「它引用了只可能来自提示词的内部 id」:triager 判词原文写着 「org-coder-impl 私有目录 rn_96a8cd696dcc 下只有 _research.md」, rn_96a8cd696dcc 是 graph_run_nodes.id,agent 没有别的途径知道它。 ## 验收 9:产出端通了,消费端没等到那个形状 真跑里 triager **确实产出了两半地址**(orgNode + workNode)—— 这一条本身有分量:27-P3/P4 §八.22 第 13 处记的正是「引擎四环全通, 但产出方 agent.md 里根本没有这个键」,那半条现在通了。 紧接着「忽略跨层地址」那条**不是缺陷,是判据在正常工作**:这次回退发生在 work 层内部,edge.to 本身就是 agent_call 节点,已是最细粒度,没有跨层可言。 缺的是**那个形状**:一次 org 层回退边、且目标是 subgraph 节点。 别把「机制没验」和「机制没有」混为一谈 —— 定点重置的逻辑侧有 graph-cross-layer-address.test.js 三例注入式覆盖,缺的只是真跑里发生过一次。 ## 顺带 · template-verdicts 的样本非空那条补 ENOENT 容忍。它扫**活目录**, 而 graph-real-chain-smoke 会在 system/agents/{aid}/{cid}/ 下建了又清运行态 —— 文件在「列出名字」和「读它」之间消失是正常的。walk() 与 violations() 早就容忍了, 只有这一处漏了,实测偶发红过一次。**扫活目录的守卫,每一处读都要容忍 ENOENT。** · 真跑 timeline 里发现一条未修的:每个节点的产物索引写入都撞 UNIQUE idx_graph_files_path 失败并被降级成日志(重试时同路径再写一次)。 后果是 files.json 产物索引在有重试的 run 上静默不全 —— 归 P4 一起处理,记在 §六ter。 全量:139 文件 / 1297 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
agent_call 与 subgraph 是**两段不同的默认实现**(defaultAgentExecutor / defaultPlanWorkGraph),各有各的 onAcpEvent 累加、各有各的守卫开关分支。 上一轮只覆盖了前者,「规划子图」那半条链仍然是零真实覆盖。 现在走完:defaultPlanWorkGraph → AgentManager → ClaudeCliProtocol(mock) → extractPlannedGraph 解析回复 → createWorkGraphSafely → 建子 run。 断言两处假 planner 天然绕开的接线: · 子 run 的定义确实来自 fixture 那张图(节点名 / systemAgentId 逐个对) · 规划那一轮烧的 token 也进了预算 —— 走的是 defaultPlanWorkGraph 自己那条 onAcpEvent,与 agent_call 那条不是同一段代码 ③ 的守卫断言补上 subgraph 分支的正反对照。 新增 fixture workgraph-planning.json:图 JSON 同时放进 chunks 与 result (mock 下 output 取哪一半由 AgentManager 决定,不该由 fixture 来赌)。 反向验过:抽掉 fixture 里的图 JSON、或把它的 usage 归零,对应用例分别变红。 **覆盖边界写在用例注释里**:跳过 resolvePlanningAgentName 里的按需 hire (预塞一行 company_agents)—— hire 要一条真实公司记录,那条路径另有 company-hire-org-layer.test.js 盯着;这条 smoke 验的是规划链路,不是招聘。 全量:139 文件 / 1296 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 一、W7「零边图」不是待办 —— 它早修了,是注释在骗人 `company-service.js` 的判据注释仍写着「判据用**有没有边**」,并留着一段 「已知残留:单 org node 的最小图没有边,判据③不生效……真跑到那一步再看」。 **那个残留在 `58a2865`(08-17)就修掉了**:`orgGraphMembership` 现在的主判据是 `authored_by='planner'` 的节点存在(「orggraph-planning 提交过一张图」的直接证据, 零边也认),边数降为存量行的回落判据;守卫 `company-hire-org-layer.test.js` 的 `CO_ZEROEDGE` + `W7-fix` 两条盯着。 注释没跟着改,于是它把读的人**引向一个已经修好的缺陷**去重修一遍 —— 我就是这么被 带偏的:读到那段注释就没再往 `orgGraphMembership` 里跟,还据此往待办清单里写了一条。 这是「指路牌失效」那一族,和 CLAUDE.md 死链同型,只是方向相反(那边指向不存在的东西, 这边描述已不存在的洞)。 判据的措辞也改了:**「成型」的判据在 graph-service.orgGraphMembership(),不在这里**, 免得下一个人照注释理解。P3B 文档与 docs/README.md 的对应条目一并更正。 ## 二、还上「真实路径 smoke 进 CI」那笔欠账的一半(27-P3/P4 §十 硬规矩 2) 原话是「凡是靠注入点验的能力,都要有一条真实路径的 smoke 进 CI 才算验收完成 —— 这条欠账仍未还」。此前唯一走真实路径的 `scripts/real-pm-smoke.js` 要真 API 额度、 人工跑、而且验的是 DevLoop 不是图执行。 做法是把「真 LLM」和「真代码路径」拆开:**只把最外层的 CLI 子进程换成 fixture 回放**, setAgentExecutor / setPlanWorkGraph 一个都不调。链路完整走 advanceOne → defaultAgentExecutor → AgentManager → ClaudeCliProtocol → onAcpEvent → 落盘 → readNodeProduct → settleNode → spendBudget → 边界守卫。 三条断言各盯一处**假 executor 天然绕开**的接线: · ① mock/files 真的落到节点产物目录(假 executor 直接 return,这一环整个不存在) · ② token 用量真的从 session/result.usage 数进预算 —— **就是 §六 验收 10 卡了两轮的那一维** · ③ boundaryGuardApplicable 在这条路径上为真(注入假 executor 时它恒为假, 那条唯一有实际后果的分支实际零覆盖);带反向对照,注入后必须变假 反向验过:断掉①的落盘、断掉②的累加,对应用例分别变红。 为此改了一处 mock:session/result 的 usage 原来硬编码 {input:0,output:0}, 现在 fixture 可指定(缺省仍是 0,既有 fixture 行为一个字不变)。 **不改这一处,token 那一维在 CI 里永远产不出数** —— 验收 10 当初就是卡在这儿。 **没还清的部分写在文档里,别当成还清了**:真 LLM 仍进不了 CI; subgraph 那一支(defaultPlanWorkGraph)仍只有注入式覆盖; 多节点 / 回退边 / 熔断没进这条 smoke,它只推一个节点。 afterAll 清掉真实链路建出的公司态目录(gitignore 挡着不进库,但留着会让别的 用例遍历 system/agents/ 时多扫一份)。 全量:139 文件 / 1295 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 移进 done/ · 27-p3c-goal-task-model.md —— 五批实施完毕,§15.5 / §16.8 的后续项也全部收掉。 新增 **§十七**:欠账清单对账、边界守卫从「乱扣帽子」改成「留证据不判罚」、 测试污染生产图库(第四次踩同一族,但换了个方向进来)、那条卡了两天的 run, 以及 **§17.5 真跑验收** —— org-planner-sdd/benchmark/validator 全 success、 work run completed、父节点自动拉回、org-coder 起实现层子图、 org-coder-validator 判 fail 走回退边打回 impl(loopCount 1)。 **返工闭环第一次由真 agent(不是注入的假 executor)走通。** · 27-p3c-batch3-handover.md —— 四条复核结论全修完、§五 那条 run 已解开, §七 待办全部打勾。从待办转为**事故记录归档**:§六 那几条多会话协作规则是真踩出来的。 两份的相对链接都已重指(done/ ↔ todo/ 跨目录),no-dead-path-references 守卫通过。 ## 留在 todo/ 的,状态说清楚 · 27-graph-agent-architecture.md —— 状态行校准。那一行是别人判断「能不能搬 done」的依据。 · 27-p3b-real-run-runway.md —— 加状态行:验收 7 ⛔ 真跑不成立(readme-writer 混进 org graph,**未修**)、2/4/8/9 未在干净环境验。缺陷② 的**根因**今天修了, 但特意写明「**根因修了不等于验收点验了**」—— 不然下一个人会照着状态行当成验过。 · 27-p3-p4-work-graph-console.md —— 在索引里写明留 todo 的原因: §十 那笔「真实路径 smoke 进 CI」的欠账未还。 · 28-followups.md —— §3.3 标已修(含「GET /info 的 404 闸门仍没修,别当成修过」); §五 守卫表加两行(graph-db-prod-guard / heartbeat-company-scoped), 并补第七条的来历。 ## docs/README.md 索引补上新增的 done 条目与 P3B / 28-followups 行,主线说明改成 「当前未完成的是哪几件」——原来只说完成了什么,读的人得自己推。 全量:138 文件 / 1292 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
28-followups §3.3。`PUT /api/agent/:agentId/heartbeat`(前端 Agent 详情页走的就是它) 用 `getBodyDir(id)` = `bodyDir(parseAddress(id).agentId)` 把公司段丢掉, 然后**整份重写本体 config.json**。 两个后果,都不报错: ① 跨租户串写:co-a:ceo 改心跳,co-b:ceo 跟着变(同一份本体); ② 一旦该 (agent, 公司) 有了公司态文件,读侧优先取公司态 → **这个端点的写入对它变成静默无效**。界面收到 {success:true}, 实际在跑的还是旧值;GET /info 同理报的是本体缺省,不是在跑的那份。 症状是「界面写 30 分钟、调度器按 15 分钟在跑」,两边都不报错。 company.js 那三个端点在 28 号已经修好了,这个入口漏了 —— 同一个 bug 的第二扇门。 ## 改 读写都走 heartbeat-service 的 readHeartbeatConfig / writeHeartbeatConfig, 与心跳挂载读的是同一个答案。入参校验归服务(intervalMinutes 直接进 setInterval, NaN / 0 / 负数会让定时器退化成每 tick 触发一次,而每次 tick 真起一轮 LLM), 非法值在写盘前抛,端点翻成 400。 顺带:404 闸门也改走服务的寻址。`cid:role` 里冒号后面是**角色名**,未必等于本体 id (company_agents 的映射)—— 拿角色名当本体 id 判存在性,凡是角色名 ≠ 本体 id 的 都会被判成「agent 不存在」,而写侧其实找得到。**判 404 和写入必须问同一个问题。** ## 守卫 heartbeat-company-scoped.test.js 补 3 例(放进同一个 describe 而不是另起一个: 夹具共用,另起的话上面那个 afterAll 会先把 server 关掉,实测 ECONNREFUSED): · 写公司态、本体字节不动,且**写完立刻能从读侧读回来** —— 这条才是「静默无效」的精确形态; · GET /info 报的是在跑的那份,不是本体缺省; · 非法 intervalMinutes → 400 且旧值还在。 反向验过:把端点退回旧写法,这 3 条立刻红。 ## 记一条没修的 GET /info 的 404 闸门仍是 `getBodyDir(id)`,对 `cid:role` 会直接 404 —— 那是 /info 整体寻址的既有缺口(它还要按同一个目录读 identity / agent.md / cron), 不在心跳这一条的范围内。测试里用裸 agent id 绕开,并在注释里写明别当成修过了。 全量:138 文件 / 1292 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
## 上一版漏了什么 预算行挂在 root run,我就只在 root 的转换上结算。而**活动发生在树里任何一层**: 人工重试一个 work 层节点动的是子 run(blocked → running),root 全程 blocked。 后果:子 run 跑整场、墙钟一次都没开始计,旧锚点还留着 —— 第一次 checkBudget 立刻判耗尽。单测当时全绿,因为它们只喂了 root。 **是真起服务、真点重试才撞出来的**:那条卡住的 run 重试后子 run 起来了, root 的 started_at 仍是 08-18。 ## 改 · 任何一层进 running 都开始计时(预算行仍写 root,readBudget 自动上溯)。 · 离开 running 时先查树里还有没有 running 的 run —— 有就不结算, 否则一层停了会把整段计时提前掐断。 · 树里已经有 run 在跑时,另一条进 running 不重锚,否则前一段被抹掉。 ## 存量重锚必须在开库时做 钩子只在「进 / 出 running」时触发,而升级那一刻正停在 running 的 run 不会再进一次 running —— 旧锚点一直挂着,下一次 checkBudget 直接判耗尽。 ⚠️ 第一版把这段迁移写进了 graph-db.js 的 migrate(),而 graph_runs 是 work-graph-db 建的表、graph-db 的迁移块跑在它之前 —— 新库上直接 `no such table: graph_runs`,getDb() 起不来。已挪到建这张表的模块里。 和 §16.2 那次「加索引写进建表块」是同一类顺序问题,这是第二次。 守卫 budget-wallclock-migration.test.js 走的是**存量库重启**那条路 (照抄 tasks-executor-migration 的 freshDb 写法):老锚点归零、running 的续上、 其余断开、没配 wallclock_ms 的不许被塞字段、坏 json 不许把开库卡死、幂等。 ## 真跑验证 重启服务 → 迁移在真库上跑到(run_9168bd24f46c 的 started_at 归 null、active_ms=0) → 重试 org-planner-sdd → POST /next → **节点 success,attempt 2**, 预算 8/100 次调用、78,858/5,000,000 tokens,org-planner-benchmark 已转 ready。 这条 run 从 2026-08-18 16:19 卡到现在,是 27-p3c-batch3-handover §五 「还没解决的原始问题」。 全量:138 文件 / 1289 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 病症:blocked 和墙钟预算互相拆台 墙钟按 `now - spent.started_at` 算,而 started_at 是「第一次花钱的绝对时刻」。 blocked 的定义恰恰是**挂起等人工** —— 预算耗尽、节点判罚都会把 run 打到 blocked。 于是:人隔夜来处置 → 等待那段被算成「已用」→ 一起跑就报 wallclock_ms 耗尽 → 再 blocked → 再等人。**一条过夜的 run 从此永远醒不过来。** 而且报出来的原因具有误导性:写着 wallclock_ms(跑太久),实际是等人等太久。 真事:run_9168bd24f46c(e2e-demo 做官网)08-18 15:48 起跑、16:19 判 blocked, 只花了 7/100 次调用、69,961/5,000,000 tokens —— **预算用掉不到 2%,两天没能重跑**。 它正是 27-p3c-batch3-handover §五 那条「还没解决的原始问题」。 ## 修法 spent 里改记 `active_ms`(累计活跃时长),started_at 退化成「当前这一段的起点」, 不在 running 时为 null。已用 = active_ms + 当前段。 起止结算挂在 graph-run-sse.afterRunTransition 这个**单一漏斗**上,不去枚举 「哪些地方会让 run 停下来」—— 枚举漏一条就是静默少算。 ⚠️ 钩子必须放在那个 early return **之前**:进 running 既不是收敛、也不空坑, 两个条件都不满足,放后面就变成「只结算停下来、从不开始计时」, 症状是 active_ms 恒 0、墙钟形同虚设,而且照样全绿。守卫里专门有一条钉这个位置 (反向验过:把钩子注释掉,那一条立刻红)。 spendBudget 不再兼职锚墙钟 —— 一个事实一处持有者。 ## 存量行 active_ms 缺失的行:旧锚点里混着等待时间,真实活跃时长复原不出来, 按 0 起算、一笔勾销。有意为之 —— 另外两维(调用次数 / tokens)是精确的且没受影响, 真正兜住花钱的是那两维,墙钟只是跑飞保护。所以那条卡住的 run 不需要动库, 重试进 running 时自动重新锚定。 ## ponytail 只认 running 一个态。manual 模式下两次点击之间 run 也停在 running, 那段人类思考时间仍会计入 —— 上限已知,比原来跨天累计小几个数量级。 要更准得按 agent 调用的起止配对计时,那是另一套账。 ## 顺带 goal-task-acceptance 两条用例原先 deepEqual 整个 budget 对象,现在剥掉 spent 只比申报的额度 —— 它守的是「用户填的额度没被静默换成默认值」, 跟运行期账本长什么样无关;账本另有断言(起跑那一刻 agent_calls/tokens 仍是 0)。 全量:137 文件 / 1284 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 病症 agent-chat-tenant-isolation.test.js 每跑一次,往共享生产库 data/graph.sqlite 落 2 条 goal + 2 条 task。实测 29→31。库里 31 条 goal 里 **22 条是它的残留**, 测试期间一直全绿。 根因是批 5 的存储迁移留下的半截清理:/task/dispatch 的元数据从 system/tasklog/*.json 换进了 tasks 表,而这个用例的 afterAll 只会清那个目录 —— 清理代码还在、清的东西已经不在那儿了。 ## 这是第四次,但换了个方向进来 前三次(BENCHMARK_DB_PATH / USERS_DB_PATH / SKILLS_DIR,记在 test-isolation-env.test.js 文件头)都是「设了 env,生产代码不读」。 那条守卫查的正是这个形态。 这次是**一个字都没设** —— 没有可查的东西,守卫自然查不出来。 静态扫源码也判不出「这个测试会不会开库」(要跨 require 链推导)。 ## 修法 1. 用例自己开一次性库(fs.mkdtempSync + GRAPH_DB_PATH),afterAll 连目录删掉。 赋值必须在 require 路由**之前** —— graph-db.js 的 DB_PATH 是模块级 const。 2. graph-db.getDb() 加运行期闸门:VITEST 为真且没设 GRAPH_DB_PATH → 抛, 报错里点名 env、点明「要在 require 之前设」。 放在 getDb() 不放模块顶层:只为取常量而 require 是合法用法 (goal-task-model.test.js 就这么用),顶层抛会误伤。真正开库才是越界那一刻。 3. 守卫 graph-db-prod-guard.test.js,4 例:撞库要抛且报错可照做、设了 env 要能开、 没有 VITEST 时不生效(防止把生产一起锁死)、样本非空。 反向验过:注释掉闸门那一行,第 1 例立刻红。 ## 顺带修一条 flaky(我自己上一版留下的) agent-docs-port.test.js 先 walk 出文件清单再逐个读,中间别的用例把 zz-* 夹具的 运行态目录清掉 → ENOENT。**报错长得像端口违例,其实跟端口无关**,实测 8 次跑中 2 次红。 只放过 ENOENT 一种(文件没了),其它读错误照抛 —— 一律 try/catch 吞掉的话, 权限错、编码错会被当成「无违例」。 ## 存量清理 删掉生产库里 created_by LIKE 'zz-tenant-%' 的 22 goal + 22 task (备份 /tmp/graph.sqlite.bak-c3a5d46)。零 run 引用它们。 其余 9 条 goal 是真实数据,一条没动。 全量:136 文件 / 1276 测试,连跑 8 次全绿(改之前 8 次里红 2 次)。 跑完后生产库行数零增量。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
主体由另一个会话完成(其会话已关闭,由我接手核对并提交),我补了两处修复 与一条守卫。 ## 它修的是什么 六处「服务端自己给某个 agent 发一条 chat」各自写死 `hostname:'localhost', port: systemConfig.port`(3010)。单测把路由挂到 `app.listen(0)` 的临时端口上,回调却照样打进**线上 3010** —— 每跑一次汇报 用例就给真 CEO 发一条 chat、起一个常驻 claude CLI 子进程、留一个 callback-T-* 会话目录。症状完全静默:测试全绿,只有进程数和内存在涨。 (这条是被我在批 5 新加的 task-dispatch-store 用例触发出来的:那之前没有 任何测试会走到汇报回调。) 抽成 selfChat:端口从 req.socket 取(收到这条请求的那个监听端口,天然就是 「我自己」),测试环境整体短路。 ## 我接手时发现并修掉的两处 `task/transition` 的两个调用点删掉了 `const chatReq` / `const postData` 的 声明,**却留下了 `chatReq.write(postData); chatReq.end();` 两行**。 `node --check` 照样通过 —— 那是运行时 ReferenceError 不是语法错;而它在 `setImmediate` 里抛就是未捕获异常,**直接杀进程**。 真起服务打了一遍 POST /api/agent/ceo/task/transition:200,服务仍在。 修之前这条路必炸。 ## 守卫:server/tests/unit/self-chat-callbacks.test.js 1. chatReq / postData 只许出现在 selfChat 函数体内 —— 「删了声明留下调用」 这类改造残留,语法检查和单测都抓不到,只有真跑到那条路径才炸; 2. 不许再有第二处自己拼 http.request 打 systemConfig.port(分叉的开始); 3. 测试环境必须短路。 第一版守卫把 selfChat 上方那段**注释**里引用的写法当成漏改报了红 —— 那是文档不是代码,已跳过注释行。 全量:135 文件 / 1272 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## ① system/tasklog/ 改名保留(§十二.5) 派单存储统一进 tasks 表后,旧目录改名为 system/tasklog.bak-unify/ 而不是 直接删 —— 跑顺一个版本再清。回填脚本改成**两处都认**(先原位置再备份位置, 顺序不能反:万一有人从备份恢复出一份新的 tasklog,那份才是要回填的)。 ## ② agent 提示词里的端口 3000 → 3010 packages/agent-core/base/ 下那几份是**所有 agent 的工具说明**,印着 curl http://localhost:3000/... 而实际端口是 3010。agent 照着打拿到的是 连接被拒、不是「端口错了」,它多半会去猜别的原因。 这不是假想:批 5 回填出来的历史派单正文里,有人手写了「(端口 3010, 不是 3000)」在纠正它 —— 靠人肉在每条任务书里打补丁。 守卫 agent-docs-port.test.js:扫 base 模板与 agent 本体,禁 localhost:3000。 判据刻意宽松(不校验「必须等于 config.port」—— 端口可被 PORT 覆盖,写死会在 部署环境误报)。带「样本非空」前置断言。第一版扫出 5 条误报,全是某 agent 目录下 Python venv 里 playwright 的 ThirdPartyNotices —— 那是第三方文件不是 提示词,已把 venv/site-packages/workspace/conversations/train 一并跳过。 ## ③ 边界守卫不再把并行会话的改动算到 agent 头上(§15.5 第 1 条) 守卫的前提是「窗口内新增的仓库改动 = 这个节点写的」。多会话共用 worktree 时 这个前提是假的 —— git status 的差集里没有「谁写的」这一维。实测已发生: 批 3 的开发改动把一条 run 判成 boundary_escape 卡住。 可检测的信号:节点**开跑那一刻**仓库就已经有源码目录的未提交改动 → 有人正在 这个 worktree 上开发 → 窗口内新增的源码改动无法归属 → **降级为只记证据、 不判罚**(与既有的 unattributable 桶同一语义),而不是关掉守卫:证据照留, 人一眼看得出「这次没判罚,因为当时仓库是脏的」。 BOUNDARY_GUARD=strict 强制判罚(CI / 干净环境),=record 一律只记。 守卫 8 例,含关键对照:仓库脏时不判、干净时同样一处越界仍判 fail —— 守卫没被削弱,只是不再乱扣帽子。 ## ④ TaskTimeline 的标签不再撒谎 conversationCount 在批 5 之后只会是 0/1(「有没有起过执行会话」),而界面 写的是 "Convs: N" —— 读的人会以为这条任务只有一次对话。改成有/无的表达。 agents 的语义变化在注释里说明。 注:innovator-hunt/todo.md 与 reddit-scraper/todo.md 里的端口也改了, 但它们是 gitignored 的运行态文件,不入库。 全量:134 文件 / 1268 测试全绿,web:build 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
记三件这一批换来的东西: §16.5 第 3 步怎么做到不碰契约:dispatch 的九步副作用里只有第 ⑧ 步是存储, 把 saveTaskMeta/loadTaskMeta 变成表的适配器、其余八步一行不动,URL 与请求体 形态天然不可能变。字段落点的三个判断也记了(objectiveId 可推导所以不存、 sourceConvId 零读取方所以删、没选 JSON blob 因为 R-19.2 否过那个形态)。 §16.6 删掉的是「猜」不是「记录」:122 行的三策略启发式换成 parent_task_id 显式记录;getTaskTimeline 保留,树从表来、正文仍读 agent 的一手会话记录。 §16.7 两次「测试全绿 ≠ 没坏」:routes/tasks.js 漏改(没有测试走那条路)、 控制台栏宽(构建/单测/探针都不会报)。与 §16.2 那个「存量库一启动就炸」 (测试能覆盖但没人写)合起来是一条经验 —— 新建库的路径、纯逻辑的路径由单测 天然覆盖;「存量数据」「真实接口」「肉眼可见」这三类必须另外安排人走一遍。 §16.8 留了三条:tasklog 目录改名待做、tools-base.md 的端口仍是 3000(实际 3010,已真咬过人)、TaskTimeline.vue 的两处字段语义变了。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 第 6 步:调度器只管图任务(一行判据,但漏了是静默的) listQueue / deriveQueueBlocks / sweep 三处都加 executor='graph'。 派单任务虽然也进 tasks 表,但它的执行载体是「直接起一次 agent 对话」, 由 /task/dispatch 自己触发。漏这一条的后果:调度器拿一条根本没有 run 行的 task 去出队 → syncRunNodesToDef 判空 → abandonRun → **把一条正在被 agent 执行的派单收成 cancelled**。守卫用例把它摆成「最像可出队」的样子 (queued + auto + 无依赖),只有 executor 这一维拦得住它。 ## 第 5 步:GET /api/tasks 改读表,删掉 122 行的会话反推 原来的 getAggregatedTasks 是启发式:扫全部 agent 的 conversations,按会话 id 里的 T-xxx 归组,再用三条策略(meta 的 parentTaskId / from agent 的活跃会话 时间包含 / master conv 的标题相似)**猜**父子关系。猜出来的树没法验证对错, 而且每次请求都要遍历所有 agent 的会话目录 —— route 那 10 秒缓存就是为它加的。 现在父子关系是 tasks.parent_task_id,派单时就显式写下来了,不用猜。 task-service.js 391 行 → 236 行;_inferParentChild / _loadTaskMetas / TASK_META_DIR 一并删除(改完就是零调用方)。 **getTaskTimeline 保留**,但来源拆清楚:任务树从表来(parent_task_id, 可验证),时间线正文仍从 agent 的 conversations/ 扫 —— 那是一手记录, 没有替代品。删掉的只是「从会话反推任务树」那一半,不是会话本身。 两处口径变化(写在代码注释里): - agents:原来是「会话里出现过的 agent 集合」,现在是「谁派的 + 派给谁」。 对派单任务这两者本来就是同一件事 - conversationCount:原来是扫出来的文件数,现在是「有没有起过执行会话」(0/1)。 为一个列表字段遍历所有 agent 的会话目录,正是这次要去掉的那笔开销; 真要看内容走 /api/tasks/:taskId/timeline,那条仍读一手记录 ## 漏改一个消费方,真打接口才发现 routes/tasks.js 自己还在算 t.conversations.length,而 conversations 是会话 反推那套的产物、已随之删除 → 500「Cannot read properties of undefined」。 测试没覆盖这条路(它只在真跑接口时才走到)。已改成用 t.conversationCount。 真打验证:GET /api/tasks 返回 17 条、12 个字段齐全 —— 条数比改前多, 因为测试跑出来的派单现在也进表了,这本身就是「存储真的换了」的证据。 全量:132 文件 / 1257 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
**本文件除了 saveTaskMeta / loadTaskMeta 的内脏和两处扫目录,其余一行没动。** /task/dispatch 与 /task/report 的 URL、请求体、响应体形态全部原样 —— 它们 印在 packages/agent-core/base/common/tools-base.md 里,是所有 agent 的工具 说明,改一个字等于同时改所有 agent 的提示词。 ## 换掉文件存储的三个理由 - 去重要「readdirSync 整个 tasklog + 逐个 JSON.parse」,任务一多就是 O(n) 次 磁盘读,且没有索引可用 - 派单任务在库里看不见 —— 控制台任务列表读 tasks 表,派单那套是平行世界 - 同一个 T- 前缀两套 id 体系,读日志时分不清 ## 落点 - saveTaskMeta / loadTaskMeta 变成 tasks 表的适配器(upsert + 反向映射), meta 的形状一个字段不少:调用方按字段名取值,形状变了就是契约变了 - 去重与父任务推断改成带索引的 SQL(idx_tasks_assignee),**判据一个字没改**, 换的只是从哪儿把候选捞出来。查不动时留声不静默 —— 静默跳过等于去重形同虚设 - 加 summary / execution_conv_id 两列(用户选的方案 A) - sourceConvId **删掉**:全仓零读取方,正是本仓库反复立案的「实现在、零调用方」, 不往新表里再搬一份 - objectiveId 不单独存:它恒等于 okr node id 的首段(OkrService.parseNodeId 与 reportRunOutcome 同一口径),且两处读取方都是 `if (objId && meta.nodeId)` ## 真打过一遍,不只是测试绿 照 tools-base.md 印的请求体原样 POST:200 + 老形态响应;再打一次 → 409 且 existingTaskId 正确;落库 executor='agent' / assignee=qa-judge / status=running, 自动建的 goal 标题「来自 ceo 的派单」、okr_node_id 落对;/task/report 之后 status→completed、summary 与 finished_at 都在;loadTaskMeta 往返出来的形状与 老 JSON 逐字一致,objectiveId 从 nodeId 首段推出 O-001;system/tasklog/ 不再 产生新文件(仍是回填前那 4 个)。 守卫:server/tests/unit/task-dispatch-store.test.js(4 例,盯的是「形状没变」) 全量:132 文件 / 1255 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
记两条这一批换来的东西: 1. 加列时把 CREATE INDEX 写进建表块,会让**所有存量库一启动就炸** —— 建表块先于迁移块执行,存量库上 CREATE TABLE IF NOT EXISTS 是 no-op, 索引引用的那一列还不存在。而新建库(单测默认路径)完全正常。 由此得到一条可复用的规矩:凡是加列,必须同时测「存量库重启」那条路 —— 新建库的路径由每个单测天然覆盖,存量库那条没人走,而生产上只有存量库。 2. 回填沿用原 taskId 而不是新发:conversations/task-{taskId} 是目录名、 汇报链路按它查找、summary 里互相引用。换 id 等于把引用全断掉, 而回填的目的恰恰是别让历史消失。 并记下第 3 步动手前的前提:/task/dispatch 的 URL 与请求体印在 tools-base.md 里(所有 agent 的工具说明),一个字不能改;而 agent-chat.js 在 8-19 被另一个会话大改过,预研写在那之前,所以要重新读现状再动手。 顺带记一处过期且已咬过人的:tools-base.md 印的是 localhost:3000, 实际端口是 3010 —— 回填出来的派单正文里有人手写「端口 3010,不是 3000」 在纠正它。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
两类回填合在同一个脚本里(§十二.5 的要求):同一次数据形态变更分两个 脚本,只会让「跑了哪个没跑哪个」变成新问题。 **沿用原 taskId,不新发一个** —— 这是这一步最要紧的判断。conversations 目录名是 task-{taskId}、汇报链路按 taskId 找回这条派单、summary 里互相 引用。换 id 等于把这些引用全断掉,而回填的目的恰恰是「别让历史消失」。 落点: - executor='agent'、assignee_agent_id ← dispatch 的 to、parent_task_id ← 派单链 - company_id 落 _global:tasklog 的行没有 company 这一维(from/to 都是本体 id), 全局 agent 的会话本来就落在这个保留 id 下(28 §5.1)。不去猜某条派单 「其实属于哪家公司」—— 猜错会让它出现在错误的公司列表里,比都归 _global 更难发现 - 每条派单自带一个 goal:tasks.goal_id 是 NOT NULL 真外键,而派单没有「人提的需求」 那一层。与 §六.3 给 POST /:cid/tasks 设计的兼容路径同一条,不给 goal_id 开可空口子 - 标题把 <br> 还原成换行再取首行,否则整条正文糊成一长条 - dispatched → running(派出去了还没回话,不是终态) 实跑:4 条派单全部回填成功、零失败;再跑一次全部跳过、零新增。 回填后 executor='agent' 的行指向 hunter / digester / master / agent-creator, 原 taskId 一个没变。 守卫:server/tests/unit/tasklog-backfill.test.js(4 例) 顺带修掉自己踩的一个:新测试用 Date.now() 拼临时库路径,撞了 test-id-stability 那条守卫 —— 和另一个会话昨天踩的是同一个。改用 fs.mkdtempSync(仓库既有写法,graph-engine.test.js 的 runsDir 就是它)。 全量:131 文件 / 1251 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
agent 之间派单(POST /api/agent/task/dispatch)与「跑一张 org graph」是 两种执行载体,硬说成一回事是骗自己:前者直接起一次 agent 对话,后者走 调度器出队 + 图执行。所以显式建模而不是让调度器去猜 —— executor(graph|agent)、assignee_agent_id、parent_task_id(派单链, 与 depends_on 的「同级先后」不是一回事)。 迁移的落点对:旧行 executor 缺省 'graph'(批 1-4 建的全是图任务), 另两列 NULL。 ## 加这一步时踩到并修掉的一个会让存量库开不了的 bug 第一版把 CREATE INDEX ... ON tasks(assignee_agent_id) 写进了建表块。 建表块整体先于迁移块执行,而存量库上 CREATE TABLE IF NOT EXISTS tasks 是 no-op —— 表还是批 4 的形态、没有那一列,于是这句索引直接抛 「no such column」,getDb() 起不来 = **所有存量库一启动就炸**。 而新建库(单测默认走的那条路)完全正常,只测新建库根本看不见这个洞。 已把索引挪进迁移块、排在 ALTER 之后。 守卫:server/tests/unit/tasks-executor-migration.test.js —— 真造一个 批 4 形态的库(删索引 + 删三列)再重开,断言三列与索引都补回、 executor 默认值是 'graph'。 另记一处自己踩的:SQL 模板字符串里的注释用了反引号,把 db.exec 的模板 提前闭合了。DDL 注释里一律不用反引号,已写进那段注释。 已知不对称:ALTER TABLE ADD COLUMN 加不了 CHECK 也加不了外键,所以存量库 上 executor 的取值域只由应用层守,与新建库的 CHECK 不同源。写在注释里, 免得下一个人以为库层拦得住。 全量:129 文件 / 1245 测试全绿(+2 新守卫)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
真在浏览器上看了才发现的:320px 减去 padding 只剩 292px 可用,而两个节点 加一条边就要 380px+ —— 实际效果是节点名被从中间切断(`org-cod…`)、必须 横向滚才看得全。而它是**全局导航**,是这个页面上最不该被压的一栏。 同一时刻中间那栏(work graph)拿着 600px 却经常是空的:选中的 task 只要 还没跑过 work graph,那栏就是一句空态提示。 改成 `280px minmax(380px, 0.9fr) minmax(0, 1.1fr) 360px`:org graph 保底 380px,两张画布按 0.9 : 1.1 分剩余空间,屏幕越宽一起长。work graph 仍拿 多数 —— 它才是下钻之后的主画布。rail 态(≤1400px 最左栏收成图标条)同步。 改后实测:org-planner → org-coder 两个节点完整显示,边上的 prd 标签与回退 边的 0/3 熔断徽标都看得见,控制条从两行挤成一行。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
sessions.size 在此之前没有任何人过问。一次心跳泄漏就攒到过 82 个进程 / 17.5 GB(约 215MB/个),而批 4 刚把公司级并行放开(单公司上限 20)—— 机器上同时能起多少完全没有闸门,正是最容易失控的时刻。 AGENT_MAX_SESSIONS,缺省 12,**调用时读不是模块加载时快照**(快照一次就 意味着改配置要重启才生效,而且不报错 —— company-paths.js 顶上那条教训)。 非法值(0/负数/非整数)回落缺省,不当成无限。 满了怎么办:**回收最久没用的空闲会话,而不是直接拒绝**。session 是可复用的 池化资源,杀掉一个空闲的只是让它下次冷启动几秒;拒绝会让一个本来能跑的任务 因为资源原因失败 —— 那是把可恢复的代价换成不可恢复的。 只有一个空闲的都没有(全部在途)时才抛 SessionPoolFullError。刻意抛而不是 静默放行 —— 静默超出正是这个仓库栽过的那一类「超出去但不报错」。 两个实现细节: - 腾位置发生在 createConnection **之前**:起完再判等于该拦的那个已经起来了 - 空闲态用显式的 idleSince 时间戳记,不去戳 IdleTimer 的私有 _timer; getOrCreate 复用时清空它(在途 = 不可被挑走) - _evictForRoom 故意不 await reset():腾位置只需要把它从 Map 摘掉, 子进程收尾可以后台走完;await 的话一次腾位要串行等一次进程退出 守卫:server/tests/unit/session-pool-max.test.js(5 例,不起真进程, 只验判据本身:挑最久空闲的、在途的不许动、全在途就抛、没满不动、非法配置回落) 全量:129 文件 / 1245 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
## 批 4:解除「一家公司同时只有一个 active root run」 - DROP idx_graph_runs_one_active_root(建表块删掉 + 迁移块显式 DROP, CREATE ... IF NOT EXISTS 对存量库不生效) - max_parallel_tasks 从 company.json 调用时读(不是模块加载时快照, 否则改配置要重启才生效且不报错);缺省 1、非法回落 1 并留声、硬上限 20 - 准入判据「从活跃集外进入活跃集」按额度计数 **生死线用 8 个真进程并发抢坑实测**(不是纸面论证):max=1/2/3/5 四组, 每组恰好抢到 max 个、库里活跃数 = max,落选进程各吃 40 次 RunSlotBusyError, 零 other 错误。唯一索引没了之后,准入事务是真守卫。 ## 批 3 对抗复核的四条遗留 - N1 🔴 cancelQueuedRun 的前提被批 3 自己的 requeueBlockedRootRun 改没了: queued 的 run 现在可以是跑过一半的。探针实测取消后子 run 残留 paused/stop_requested=0 —— 那条 running 的子 run 继续调 agent 烧 token。 改为无条件走 engine.stopRun - L1 ⚠️ 恢复路径对 manual 只兑现一半:注释写着「T1 永远回不来这个后果因此 消失」,但调度器故意不碰 manual → 退回队列后没人接。定案:「manual 不自动 出队」约束的是首次起跑的授权,已被人亲手起过的不算新授权。 判据用 attempt>0 而不是 started_at —— 后者会被 requeue 清成 NULL, 恰好在需要它的那一刻是空的(实测踩过) - N2 🟠「退回队列」对用户完全不可见:200 + ok:true + 按钮全灰 + 图消失 + 一句「节点状态一律 pending」的假提示。加 notice 明说 - N3 🟠 isSlotRaceError 把 InvalidTransitionError 当竞态吞掉,而探针证伪了 那条注释:被推走时条件更新返回 0 不抛,真正抛它的只有「调用方声明了非法 转换」。删掉该分支,让它吵起来 四条里三条是同一个模式:注释承诺的前提被本轮自己的改动改没了,注释还留着。 文档 §十三.3 / §十四.4 已就这个模式立过两次案,这是第三、四、五次。 ## 文档 §十五 实施记录,含 §15.4 一次流程事故:87b776c 由第三方会话在对抗复核完成 前误提,提交说明的「1201 用例全绿」压在一份尚未通过复核的代码上。 全量:128 文件 / 1240 测试全绿,web:build 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
起因是「全量测试偶发挂一条」。查下来既不是回归、也不是那 45 个孤儿进程(它们 CPU 早归零、 不再写任何东西),而是**还活着的心跳在测试扫描 system/agents/ 的同时往里建目录**: locator.taskDir() 的落点是 system/agents/<agent>/<companyId>/<taskId>, 运行态目录落在本体目录里属既定设计;但心跳每 5 分钟建一个新的 (cto/_global/ 已 263 个子项且在涨),于是扫描 system/agents/ 的守卫测试 (no-dead-path-references 那一类)会撞上瞬时竞态:列到了目录名,进去时那一层还没写完。 症状是偶发假失败、重跑就好 —— 这类最费人,因为第一反应总是去怀疑自己刚改的代码。 改法:server/index.js 启动的那个 setTimeout 块里,恰好装着全部「会自己起 agent 的东西」 (两处中断恢复 + 任务队列扫描 + 两路心跳),所以一条早退就够,不必在五个地方各加一个判断。 DISABLE_BACKGROUND_AGENTS=1 bash start.sh 实测:127 文件 / 1226 用例全绿,跑前跑后 cto/_global 子项数都是 263。 **验证它生效不能只看日志里的 ⏸** —— 那只证明代码走到了早退。硬判据是目录数前后相等, 也就是真的没有东西在写。文档 §七 把这条写成了可粘的三行。 排掉的两个坑,都记在文档和 .env.example 里: 1. `.env` 里不能给这个键赋空值。start.sh 是 `set -a; source .env` 加载的,.env 里只要 存在同名键(哪怕空的)就会盖掉命令行传进来的 =1,而且不报错。所以 .env.example 里 它只以注释形式出现 —— 一个「示例文件顺手写全」的习惯会直接让开关失效。 2. 开关生效后 `[TaskQueue] 🚀 已启动` 仍会打印,那是另一个模块:services/task-queue.js (ArenaSK 时代 /api/claude-chat 的队列,routes/claude-chat.js 模块加载时起), 和批 3 的 services/task-scheduler.js 同名不同物。它的心跳只扫内存里的 this.tasks, 不碰 system/agents/,不是干扰源。名字撞车,容易误判成开关没生效。 文档同步: - docs/done/claude-cli-session-leak.md §七 重写(原文写的是「得让测试隔离,或跑前停心跳」, 现在后者已实现,改成开关的用法 + 硬判据 + 上面两个坑);原第 2 条降为 §七之二 - CLAUDE.md 测试命令块加一行(这是找「怎么跑测试」的人真正会看的地方) - docs/README.md 索引行补上开关 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
泄漏被修过一次(310a2b2 / 3e718aa),修完照样漏。第二次排查发现原因不是修错了,而是: 系统里有两个互相独立的会话池,上一次只修了其中一个;而被修的那个,还漏了一条分支。 池 A routes/agent-chat.js 的 sessions ← /api/agent/:id/chat,含两路心跳 池 B src/core/session-pool.js 的 SessionPool ← AgentManager.callAgent,图执行 / DevLoop 两者没有任何共享代码,回收逻辑各写各的。这个结构性事实此前没有任何一处记着, 所以第二个人查到同一个症状时会重走一遍全程 —— 这份文档就是为了让第三个人不必再走。 正文八节,除了两个洞各自的成因与修法(b0b2b62 / 2092a2e),有三处是排查方法而非结论: §三 按 cwd 第三层判断孤儿归属的对照表。两个池子都把 cwd 下沉到运行态目录, `_global` / `_no-task` 是 agent-locator 的保留缺省值、两边都会出现,不能靠它区分。 §四 为什么上一版守卫是瞎的。dropSession 和 closeSession 都会让会话从池子里消失, 黑盒探针两种情况都返回 false —— 差别只在子进程死没死,而那是探针看不见的维度。 选探针要问的不是「能不能测出正确行为」,而是「能不能区分正确行为和这个具体的错误行为」。 §八 三步定位脚本,外加一条:先确认跑着的服务器进程是什么时候启动的,再去看代码。 本次排查中,正在跑的实例比修复 commit 早 18 秒启动,代码里有的修复它并没有。 线上验证(PID 63099,13 分钟窗口两轮心跳):触发 5 / 完成 5 / 回收 5,峰值子进程 3、稳态 0。 17:13:32 那个 3 正是泄漏时代的签名(cto+coo+qa-judge 同秒触发),清理前那批孤儿一个都不回收, 现在 30 秒内全部释放。窗口内没有失败心跳,所以 2092a2e 那条 catch 路径是靠单测的变异检验 保证的、不是靠线上观察 —— 文档 §六 里写明了,没算成已验证。 docs/README.md 同步加索引条目(no-dead-path-references 守卫 4 条全绿)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
lemonboss-78 已补齐 §二 的 4 条(均带探针实跑证据),§七 第一条待办随之过期。 把它勾掉,并把 N1/L1/N2/N3 各自拆成一条待办,附优先级判断: N1(取消排队留下还在烧 token 的孤儿子 run)是唯一有直接金钱成本的一条。 顺带提一条给 lemonboss-78 判断的提议:「注释承诺的前提被本轮自己的改动改没了、 注释还留着」这个模式已经第三次立案(§十三.3 / §十四.4 / 本次), 第三次或许不该再只当个案记。提议不代表已立案。 只提交本文件;docs/README.md 与 docs/done/claude-cli-session-leak.md 是 lemonboss-89 的在途工作,未动。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
lemonboss-68 写交接文档时只确知 1 条,其余 3 条刻意留「待补」而不是 凭二手转述硬写 —— 那个判断是对的,记错了反倒会变成新的事实来源。 这里由批 3 作者按 17:00 那次对抗复核的原文补齐,每条都有探针实跑证据。 四条: - N1 🔴 取消排队留下还在烧 token 的孤儿子 run(唯一有直接金钱成本的) - L1 ⚠️ 恢复路径对 manual 不成立(调度器故意不碰 manual,于是坑空着没人接) - N2 🟠「退回队列」对用户完全不可见(200 + 按钮全灰 + 图消失 + 一句假提示) - N3 🟠 isSlotRaceError 的判据注释被探针证伪,且吞真错误不打日志 其中 3 条是同一个模式:注释承诺的前提被本轮自己的改动改没了,注释还留着。 27-P3C §十三.3 / §十四.4 已就这个模式立过两次案,这是第三次。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
87b776c 把 lemonboss-78 还在等对抗复核的批 3 提进了 main(提交者是我,未经作者同意), 复核随后给出 4 条问题。这份文档记下:时间线、main 上各笔提交的归属与状态、 那笔提交说明里「1201 用例全绿」的更正、待决策项,以及这次踩实的多会话协作规则。 顺带把最初那条卡住的 run(T-msyu6u9b-939c,boundary_escape 误伤)的诊断与 界面重跑步骤一并留档 —— 它至今没动,等工作区收敛干净才能跑。 只提交本文件,其余 13 个在途文件原样留在工作区未动。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
310a2b2 / 3e718aa 把「正常跑完的对话」补进了回收路径,但 catch 分支没跟上: } catch (err) { dropSession(key); // 只摘出池子,不杀子进程 } finally { if (kind?.[2]) closeSession(key, ...); // 池子里已经没了,返回 false,空转 else armSessionIdle(key); } dropSession 的语义是「子进程已经自己死了」,可这里子进程活得好好的 —— 错的是它背后的 API 调用(529 / DNS / 证书),CLI 一个都不会自己退。于是会话先被摘出池子,再也没人 杀得掉它:一个既没有引用、也没有回收路径的孤儿进程。 实测:一个跑了 23 小时的实例攒下 45 个孤儿 / 9.6 GB RSS,其中 38 个是心跳。抽查泄漏的 convId,status **无一例外**是 failed;对照 cto 最近 60 次心跳是 55 completed / 5 failed —— 8% 的失败率,一次漏一个,数字对得上。成功的心跳一个都没漏,说明 310a2b2 本身是对的。 改法:catch 里 dropSession → closeSession。可观测行为完全不变(两者都把会话摘出池子、 都让下一轮冷启动),只是多杀一个本该死的进程。 守卫:agent-chat-session-reclaim.test.js 新增一条。这里必须说清楚为什么破例去 spy disconnect —— **旧探针对这个 bug 是瞎的**:dropSession 和 closeSession 都会让会话从 池子里消失,`/chat/:convId/stop` 两种情况都返回 success:false,差别只在子进程死没死。 上一版修复正是因此漏过了这条路径。用 vi.spyOn 让 sendPrompt reject 来复现「CLI 活着、 API 挂了」,再断言 disconnect 被调过。做过变异检验:退回 dropSession 后该用例转红。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
SessionPool 此前只有三条回收路径:换新会话 / reset() / killActive()。「正常跑完」和 「不可重试的报错」都不在其中,而 getKey 里带了 taskId —— 每个图节点一个新 key,于是 一节点一进程、永不释放,且**不报错**。实测 23 小时漏出 7 个图执行进程。 修法与 agent-chat 侧同一套:调用开始时停表(getOrCreate 里 cancel),调用结束时上弦 (agent-manager 两个出口调 armIdle),计时器只在没有在途请求的间隙走,所以再长的单次 生成也不会被 TTL 打断。旋钮共用 AGENT_SESSION_IDLE_MS(默认 30 分钟)。 守卫:server/tests/unit/session-pool-idle-reclaim.test.js Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
队列与调度(§五.1): - 下达任务不再因「公司已有 active root run」吃 409,改为建 queued run + queued task 排队;出队走 updateRunStatus 的 CAS + BEGIN IMMEDIATE 准入事务,触发挂在 transitionRun 这个唯一漏斗上,60s sweep 兜底。 - 依赖已不可能满足的排队任务就地推导成 blocked,且每次 tick 重算不落死,上游重跑 成功后能原路解开。 恢复路径(批 3 复核 F1/F2):三条恢复转换的 from 全在活跃集之外,撞坑满时退回队列而不是 永远躺在 blocked;退不回去的裸 run 干净拒绝,不留半截状态。 控制台三处可见性修复 —— 症状全是纯视觉、不报错、git status 干净: 1. 顶栏墙钟恒 `now - started_at`,没有终点。一条昨晚就 blocked 的 run 第二天还在涨到 「16h 58min」,而那是页面上唯一的时间信号 —— 「已经停了等人处置」被显示成了 「还在跑再等等」,这两件事需要的动作相反。终点改取 run.updated_at,并标「已停」。 2. 越界日志只报「留下 N 处改动」,文件清单躺在 timeline.ndjson 里翻不到。判罚就是按 这份清单下的,而人要判断「这真是 agent 写的吗」唯一的依据就是路径(并行的开发会话 改宿主仓库会被算到当时在跑的节点头上,那是误判,看一眼就知道)。改为列出 entries。 3. run 收敛成 blocked 时只把 task 写成 blocked、不写原因,任务列表那一行只剩一个橙色 的「挂起」,必须逐层点进 org 图 → work 图 → 节点详情才知道发生了什么。改为把 「卡在哪个节点 + 什么 fail_reason + 判词摘要」写进 tasks.blocked_reason。 原因绝不带 UPSTREAM_BLOCK_PREFIX —— 带了会被调度器当成依赖推导出的 blocked 自动 推回 queued,而这类 blocked 本来就等着人处置(守卫在 task-blocked-reason.test.js)。 测试:126 文件 / 1201 用例全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
goal 从「人写一句话」变成「agent 拆成一张 task DAG、人确认后下达」, 收敛时由 org-validator 在 goal 层再验一次。 ## 交付 - task-planning 本体(毕业判据实测通过:agent.md + soul.md 一次写完、 不带 _base 哨兵、路径按 cwd 深度 3 实算核对) - 拆分链路:3 轮 errors 原样回喂 → 不过就降级成 1 条同描述 task, goal 绝不卡在 planning;token 记 goals.planning_tokens - goal 级 org-validator 验收:≥2 条 task 全 completed 才调,单 task 跳过, 有 failed 直接 failed 不调(省一次真花钱的调用) - 控制台四栏 + GoalTaskPane 两级树 + 两步表单(拆分中给逃生口) - 中断恢复:启动时补跑验收与汇报 ## 三条跨进程竞态,全部用真探针实跑复现后才判定修好 1. runGoalSplit 无条件把 goal 写回 draft,抹掉逃生口刚下达的 active —— 而触发它的正是表单自己推荐的那个逃生口。后果是一个正在烧钱的 goal 显示成「草稿」,且从此永不收敛(OKR 不写、上级不唤醒、无日志) 2. CAS 把最终态提前写进库,验收窗口内重启即丢,且 verdict=NULL 与 「单 task 合法跳过」形态相同,查不出来 → 占位判词 + 启动重推 3. 认领语句 SET verdict=RESUMING WHERE verdict IN (PENDING, RESUMING) —— 写进去的值就在 WHERE 里,changes 恒为 1。而函数注释白纸黑字 写着「认领写的是另一个占位值」。注释是对的,代码没兑现。 实测:org-validator 调 2 次 / OKR 写 2 条 / token 双记 → claim 带唯一 token + 时间戳租约,回写条件更新决胜,主路径同闸门 ## 与设计正文的偏离(已记进文档 §十四) 拆分后 goal 落 draft 而非 active(active 的语义已被 markGoalDispatched 钉死为「已下达」);mode 三级继承对显式 tasks[] 同样生效;占位判词不出 API;新增 planning_error / reported_at 两列,后者迁移带回填(否则升级后 第一次启动会把历史终态 goal 全部重报一遍)。 ## 过程 四轮 26 个 agent,复核报出 22 条缺陷,其中 6 条是修复自己引入的。 第四轮修完挂了 3 条测试,全是测试在钉住缺陷本身 —— 其中一条的注释 写着「模拟进程死后重启」,构造的却是新鲜占位值:注释描述 A 场景、 代码构造 B 场景、断言按 A 场景写。 全量:118 文件 / 1112 测试全绿,web:build 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
手工扫了两轮,扫出的是三类会持续复发的失真,不是零星笔误: ① package.json 里 12 处引用指向从未在 git 历史存在过的文件(server/src/cli/auto-dev.js 一族) ② CLAUDE.md 的 docs/ 那行列了 5 个文件名,文档移进 done/ 之后全成死链 ③ 数据存储整节把两个不同的 data 根当成一个写,server/conversations/ 这个路径根本不存在 共性是**不报错、不影响测试、git status 干净**,只有照着做的人(或 agent)撞墙才发现。 而 CLAUDE.md 是每个会话都会加载进上下文的文件 —— 写错等于把错误信息直接灌给每一个后来者。 四条扫描: - CLAUDE.md 里的仓库路径都存在 - CLAUDE.md 里出现的每条 npm run 都在 package.json 里(守反方向:删脚本忘改文档) - package.json 的 scripts / bin / main 指向的文件都存在 - docs/README.md 的相对链接都指向真实文件 判据刻意窄(本仓库有过「防线被整条注释掉」的教训):路径只认「以仓库顶层目录开头 + 含 /」, 且跳过 gitignored 的 —— 那是运行态产物(web/dist/、data/companies/、skills/),在干净机器上 本来就不存在,缺席不是文档错误;这与 no-stale-templates-path.test.js 用的是同一条判据。 上下文里的相对片段(_base/、todo/future/)、glob、<repo>/… 示意写法一律不判。 四条各带「样本非空」下限断言(当前 26 路径 / 16 script 名 / 6 入口引用 / 22 链接), 下限取当前值一半左右:正则一写错就会以「零违例」全绿,那和没有这条测试是一回事。 验证:把同一套判据跑在旧 revision 的文件内容上(只读 git,不碰工作区)—— 3e718aa:路径 ❌3 条、入口 ❌10 条 d6214f8:路径 ❌4 条、入口 ❌10 条 HEAD :全绿 顺带修掉自己写的一处假路径占位符(`data/xxx` → 「以 `data/` 开头的路径」), 文档里本来就不该出现假路径。 28-followups 守卫清单补到第六条,并写明后两条与前四条的形状差异。 npm run test:unit 1061/1061 绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-
顺着上一笔的判据(引用了不存在的东西)继续扫,扫出的不是零星笔误,是一处结构性失真。 事实(按绝对路径解析源码里的 path.resolve(__dirname, ...),不是数字面量 —— 数字面量会错,path-resolver.js 比 db 文件深一层,同样的 '../../../data' 落点不同): 仓库内 <repo>/data/ — 11 个模块(company / okr / graph / devloop / schedule / pipeline …) 仓库外 <repo>/../data/ — 3 个模块(skill-db、user-db、benchmark-db,ArenaSK 时代遗留) 原文把六条全挂在扁平的 `data/` 前缀下,读的人只会理解成仓库内一个根。这不是抠字眼: 备份 / 迁移 / 换机器时漏掉仓库外那半边,skill、用户、benchmark 数据会**静默丢失**。 一并修掉的具体错误: - `data/benchmark.sqlite` → 实际文件名是 `benchmark_logs.sqlite` - `server/conversations/` → 该路径不存在。会话持久化实际有**两套**: ArenaSK/Innovator 侧在 `<repo>/conversations/`(config.conversationsDir), LemonBoss agent-chat 侧在公司态目录下(getConvDir → locator.companyStateDir) - 目录速查里的 `data/skills/` 改成 `<repo>/../data/skills/`,点明 `<repo>/data/skills/` 并不存在 - 补上仓库内侧原本一条没列的 graph.sqlite / companies/ / okr/ / logs/ / agents/ - 每个库补上真实的 env 覆盖名(SKILLS_DB_PATH / USERS_DB_PATH / BENCHMARK_DB_PATH / GRAPH_DB_PATH) 新写进去的每一条路径与每一个 env 名都用脚本核过存在性(10 路径 + 5 env 全 ✅)。 docs/README.md 的 markdown 链接同批扫过,零死链。 npm run test:unit 1057/1057 绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
判据统一:目标文件既不在盘上、**也从未在 git 历史里出现过**(逐个 git log --all 核过), 不是「搬了地方」而是从来没有过。跑起来一律 MODULE_NOT_FOUND。 删除(12 处,指向 4 个从未存在的目录): server/src/cli/ main、bin.auto-dev、example、example-bmi server/src/utils/ logs、analyze、parse server/src/modules/ test:browser-logger、test:design-validator server/src/tests/scripts/ test:multi-cli skills/whatsapp-bot/ whatsapp:auth、whatsapp:start (skills/ 是 gitignored 的运行时目录,确实存在, 但里面没有也从没有过 whatsapp-bot; /api/whatsapp 路由走的是 routes/whatsapp-auth.js,不受影响) 只授权了 auto-dev 那 3 处,其余 9 处是同一类缺陷、同一条判据,一并删了 —— 要退回来 `git revert` 这一笔即可。删完 package.json 零死引用(脚本目标存在性已脚本化核过)。 CLAUDE.md 同步: - 常用命令去掉已删除的 6 条,补上真实存在但之前没列的 graph:seed / okr:migrate - 上一笔加的 auto-dev 警告块随引用一起去掉 - /api/goals 从「向后兼容别名」改为「已退役」—— 7f24ef0(27-P3C §六.1)已落地, OKR 只剩 /api/okr 一个挂载点,且有守卫测试盯着 验证:npm run test:unit 1057/1057 绿;npm run spec:check 52 份 _spec 全同步; CLAUDE.md 里出现的每一条 npm run 都在 package.json 里存在(脚本核对)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> -
新增 §十三,体例照 27-P3/P4 §十:设计正文不改写,只在这里与实现对账, 冲突处以实施记录为准(正文是设计时的判断,本节是跑出来的事实)。 记了与设计的四处偏离,其中两处是设计正文写错了: ① blocked 在 task/goal 两层都不是终态。正文 §四.3 把它与 completed/failed 并列画成终态,实现时实跑复现出后果:预算耗尽后 加预算恢复、跑完,两次条件更新静默命中 0 行,最终 run=completed 而 task=blocked/goal=failed/OKR=failed 永久对不上。RUN_TRANSITIONS 里 blocked: ['running','stopped'] —— 对 run 它是可恢复态, reviveParentFromChild 还会自动拉回。 ② goal 级是唯一汇报出口,reportRunOutcome 已删除(零其它调用方)。 1 goal:1 task 形态下 run 级与 goal 级会把上级就同一件事唤醒两次。 from 的口径同时修正为实际交付者(terminalAgentId 搬进 org-graph-service 当唯一持有者),不再用 created_by —— 那是「谁下的单」,默认字面量 'human',下游会静默落空。 ③ dispatch 挑第一条可跑的 draft,连带补上 PATCH 改 seq —— 原先 409 的 hint 写着「改依赖或 seq」,而那个端点从不读 body.seq。 正文里三处提到 reportRunOutcome 的地方一并标注:§四.7 加了「本节写于 实施之前」的前置说明,§十二(批 5)的前提改成「函数换了、依赖关系 没变」—— A 的回调链路仍长在 B 身上,只是出口从 company.js 的 run 级 换成了 task-settle 的 goal 级。 过程也记了:三轮编排 21 个 agent,对抗复核报 19 条,其中 4 条是 第二轮修复自己引入的。结论:对抗复核不能只做一轮 —— 修复和实现 一样会出错,而且更隐蔽,它长得像在解决问题。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>