• ## 上一版漏了什么
    
    预算行挂在 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>
    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 »