• 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>
    yibo authored
     
    Browse File »
  • ## 病症: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>
    yibo authored
     
    Browse File »

  • ## 批 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>
    yibo authored
     
    Browse File »
  • 队列与调度(§五.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>
    yibo authored
     
    Browse File »

  • 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>
    yibo authored
     
    Browse File »
  • 把「人类提的一个需求」与「机器执行的一件活」拆成两级。这不是新概念:
    27-NAMING §四 早就定义了 goal 1 : task N,graph_runs.goal_id / task_id
    两列从 P3 起就一直在写,却没有任何表接得住 —— 本次补上那两张缺席的表。
    
    ## 交付
    - goals / tasks 两张表 + CRUD + depends_on 环检测 + ID 生成器
      T-{base36}-{4hex}:原格式同毫秒必撞,而 tasks.id 是主键,
      「一个 goal 拆 N 条 task」是循环建行,撞了就是 500
    - goal / task 全套端点;POST /:cid/tasks 向后兼容(不带 goal_id 自动
      建 goal);退役 /api/goals 与 /:cid/goals 两个 OKR 别名(零调用方)
    - 终态回写 + goal 收敛 + OKR 写一次:钩子挂在唯一漏斗
      graph-run-sse.transitionRun 上,而不是枚举路径 —— stopRun 与
      「预算耗尽 → blocked」两条最常见的释放路径都不走 settleRun,
      枚举一次漏一次。goal 收敛用 CAS 闸门,并行完成的 N 个 task 只有
      一个赢家做不可重放的副作用(写 OKR / 唤醒上级)
    - blocked 在 task/goal 两层都不是终态:它是「挂起等人介入」,
      与 completed/failed/cancelled 不同类。否则预算耗尽后加预算恢复、
      跑完,两次条件更新静默命中 0 行,最终 run=completed 而
      task=blocked/goal=failed/OKR=failed 永久对不上(实跑复现过)
    - goal 级是唯一汇报出口:删掉 run 级 reportRunOutcome(零其它调用方),
      1 goal:1 task 形态下它会把上级 agent 就同一件事唤醒两次
    - dispatch 挑第一条可跑的 draft,而不是恒取 seq 最小那条 ——
      否则「第 1 条依赖第 2 条」这种合法输入会让 goal 永远下达不了
    - 路径缺陷:writeTaskGoalArtifact 不认 GRAPH_RUNS_DIR 而读侧认,
      设了 env 就写进 A 处、从 B 处读,describeTask 拿到 null 后静默退回
      兜底文案「执行图节点任务」;三处模块加载时快照改为调用时读
    - 存量回填脚本(幂等 + --dry-run):不做的话批 1 反而让历史任务
      更看不见 —— 正是本批要修的问题的反向放大
    
    ## 过程
    三轮多 agent 编排 + 每轮对抗复核,共报出 19 条缺陷(10 条首轮实现的、
    4 条第二轮修复引入的、5 条判定为非问题)。全部已修并有用例覆盖。
    新增测试 4 个文件,含不变式测试 runs-root-single-holder(凡是声明了
    GRAPH_RUNS_DIR 覆盖的模块,该覆盖必须真的被用到)。
    
    全量:116 文件 / 1057 测试全绿。
    
    ## 未做(属批 2-5)
    task-planning 与 goal 级 org-validator 验收(批 2)、调度器与
    run_status='queued'(批 3)、并行(批 4)、两套任务体系统一(批 5)。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse File »