• 真实回归暴露"认领后没有触发源"的真问题,讨论后发现"认领"(pull,CAS
    抢占)要解决的"多方竞争同一条任务"场景根本不存在——只有 CEO 一个人拍板
    要不要派、派给谁。CEO 心跳统一发现+直接派发(push)成本更低,还能顺带
    控制派发节奏,不需要认领。
    
    删除:claimSubtask/unclaimSubtask(work-graph-db.js)、POST /claim、
    POST /unclaim(company.js)、CLAIMABLE_WORK_ROLES 白名单、
    claimable/claimed 两个 status 值、claimed_at 列。
    
    改名:subtask_kind 的 'claimable_work' → 'decomposed_work'
    (materializeClaimableSubtasks → materializeDecomposedSubtasks),
    "认领"这个词不再准确,物化出的行现在就是普通 draft task。
    
    新增:
    - triggerSubtaskPlanning 加递归防护(task.parent_task_id 非空直接跳过)
      ——设计 v2 时发现的真实漏洞:CEO 对子任务调 /enqueue 会默认
      withSubtaskPlanning=true,不挡会对已经是"第几步"粒度的子任务再拆一层。
    - createDecomposedWorkGraph:现造一张两节点轻量 work graph
      (impl=org-coder、validate=org-validator,agent_call 不是 subgraph),
      跳过公司完整 org graph 的 brd/prd/sdd 规划仪式——subtask-planning 已经
      把"怎么做"拆到这个粒度了,再走一遍纯属重复劳动。含一条没有真实验证过
      的假设(org-coder/org-validator 裸调用会不会读不到预期上下文),如实
      写在代码注释和文档里。
    - enqueueTaskRun 按 subtask_kind 选图:decomposed_work 走轻量图,
      普通 task 行为不变(回归测试覆盖)。
    - ceo/heartbeat.md、tools.md 补了巡检 decomposed_work 草稿 + 判断节奏
      + 调用现成 /enqueue 的指令。
    
    测试:删除 task-pool-claim.test.js(随 v1 代码作废),新增
    task-decomposition-dispatch.test.js(12 条),更新
    task-decomposition-validator-gate.test.js 措辞。全量单测 1191 条,
    1189 passed(唯二失败与本次改动无关),零残留 CLI 子进程。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01H85SS5hVC4u1PbVhjmrhH8
    yibo authored
     
    Browse Dir »
  • tasks 表新增 subtask_kind('acceptance_item'|'claimable_work',区分 43 号
    文档的验收条目物化和 Phase 3 的可认领工作项物化,共用一张表不新开)+
    claimed_at 列(批 6 迁移,ALTER ADD COLUMN,纯增量向后兼容)。
    
    - work-graph-db.js:createTask 新增 subtaskKind/status 参数 + 校验;
      claimSubtask/unclaimSubtask 是全仓库第一个真正的"认领(pull)"CAS 写入
      (之前 assignee_agent_id 全部写入点都是"派发 push",没有先例可抄,照抄
      claimQueuedTask 的 changes 判据);materializeClaimableSubtasks 把
      subtask-planning 的 steps[] 物化成可认领子任务,两阶段建法处理
      depends_on_hint 的前向引用,幂等,与 acceptance_item 物化互不干扰。
    - graph-engine.js:settleNode 的 SUBTASKS_INCOMPLETE 硬闸门收窄成只查
      subtask_kind='acceptance_item'——这是必须同步改的一步,不收窄的话新物化
      的 claimable_work 行会让所有开了 subtask-planning 的 task 验收判定失控
      (已用回归测试固定住这个场景)。maybeMaterializeItemizedSubtasks 的
      createTask 调用同步打上 subtask_kind='acceptance_item'。
    - subtask-planning.js:triggerSubtaskPlanning 写完 artifact 后立刻物化
      claimable_work 子任务,不等 org-planner 跑完。
    - company.js:taskView 补 subtaskKind/claimedAt;GET /tasks 加 subtaskKind
      过滤(?subtaskKind=claimable_work&status=claimable 就是可认领池查询);
      新增 POST .../claim、POST .../unclaim 两个端点,角色白名单先写死
      ['org-coder'](不做能力匹配,系统里没有结构化角色能力字段)。认领成功
      同时把 executor 改成 'agent',复用既有的 executor='agent' 派单闭环
      (/task/report 上报生命周期),不需要碰 graph-engine 的调度模型。
    
    新增 task-pool-claim.test.js(23 条:DB 层校验/CAS/物化 + 路由层白名单/
    并发/放弃);task-decomposition-validator-gate.test.js 补一条回归测试
    (claimable_work 子任务不影响 acceptance_item 的验收判定)+ 更新既有用例
    显式打 subtaskKind;task-decomposition-materialize.test.js 补
    subtask_kind 落库断言。全量单测 1202 条,1200 passed(唯二失败与本次改动
    无关、改动前即存在)。
    
    本轮明确不做:依赖顺序强制、能力匹配、graph-engine 调度模型改动、
    Phase 4 的独立开发验证循环、Phase 5 看板 UI。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01H85SS5hVC4u1PbVhjmrhH8
    yibo authored
     
    Browse Dir »
  • 47号:itemized 子任务物化在当前 workgraph-planning 常见子图形状下因 acceptance.md
    永远产不出来而形同虚设;maybeMaterializeItemizedSubtasks 新增兜底,缺失时改读
    已被 44 号文档证实稳定产出的 goal-benchmark.json 的 dimensions[]。
    
    46号 Phase 1:新 agent subtask-planning,钩在 enqueueTaskRun(显式 opt-in,只在
    goal 批量下达/单条 task 下达两处触发,保住 quick task 零 LLM 调用的既有保证);
    产物写共享 artifacts/subtask-plan.json,复用 R-19.3 任务级共享索引对接下游。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »

  • Snapshot-then-stop race in stopRun() left child runs created after the
    snapshot outside the stop scope; add auto-mode guard rejecting manual
    /next on execution_mode='auto' runs (source of the concurrent-advance race).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 之前排队等待(上条改动)期间前端只有通用的"思考中",最长可能等到 60s 都看不出
    是在排队还是在正常生成,容易让人以为卡死了。
    
    - agent-session-lock.js 新增 isLockBusy(agentId):判断此刻是不是真被占着(僵尸锁
      不算),供路由层在真正排队前 peek 一眼。
    - agent-chat.js 的 /:agentId/chat:锁被占用时提前建立 SSE 连接、先发一个 queued
      事件(activeConvId + waitBudgetMs),前端能精确显示"排队等待中";没被占用的
      常见路径完全跳过这段,不引入多余的建连时机差异。头一旦按 200 SSE 发出去就不能
      再改状态码,排队超时也改用 queue_timeout 事件收尾,不再是独立的 429 JSON 响应
      (没被占用、一开始就抢到锁的路径不受影响,仍是普通 SSE)。
    - agentChatEvents.js 的 handleEvent 认识这两个新事件类型:queued 把 thinkingText
      换成带等待预算的提示;queue_timeout 跟 error 分支一样落一条可见的助手消息,
      不会像原来的静默 429 那样挂在界面上没反应。
    - 相应更新 agent-chat-interrupt.test.js(断言 SSE 里真的有 queued/queue_timeout
      事件,超时分支状态码从 429 改为 200)+ agentChatEvents.test.js 新增两条用例。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01CwGcvMfUcgCcweN5vjhTkn
    yibo authored
     
    Browse Dir »
  • yibo authored
     
    Browse Dir »
  • - agent-session-lock.js 新增 acquireLockWithWait:锁被占用时按 pollMs 轮询排队等待
      AGENT_CHAT_QUEUE_WAIT_MS 预算(默认 60s),check-and-set 之间无 await,天然原子;
      等到锁释放就正常执行,等满预算仍抢不到才 429,不再"撞上就立刻拒绝"。
      内部自唤醒(TaskCallback/TaskDispatch 等 selfChat 调用)是 fire-and-forget,之前
      429 等于这次唤醒直接消失,只能靠别处巡检兜底;排队等待让大多数几秒到几十秒的
      短暂碰撞能自然衔接上。
    - AgentChat.vue 的 handleSend() 之前拿到 fetch 响应后不检查 r.ok 就直接当 SSE 流读,
      非 200 响应体是普通 JSON,逐行解析全部匹配不上 event/data 前缀,被静默当空流吞掉——
      用户发的消息挂在界面上,没有任何回复也没有报错。现在先判 r.ok,非 200 时读 JSON
      错误体,429 显示"Agent 正忙,请稍后重试",其他状态显示带状态码的错误提示。
    - agent-chat-interrupt.test.js 相应更新:原来断言"并发第二个请求立刻 429"的用例
      改为断言"预算内排队等待、等到就正常执行、两次调用不重叠",另加一条"预算耗尽仍
      抢不到锁时返回 429"的回归。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01CwGcvMfUcgCcweN5vjhTkn
    yibo authored
     
    Browse Dir »
  • 实现路径:task-planning 判断是否 itemized(硬规则 5 开例外让 LLM 写这
    一个字段)→ org-planner 收尾把 acceptance.md 物化成子任务 → coder-validator
    用新工具 task subtask-result 逐条上报,settleNode() 新增闸门强制核对
    子任务全部 completed 才认这一轮 success。
    
    - work-graph-db.js: tasks 加 acceptance_mode 列,createTask 支持
      parentTaskId(复用既有列),新增 settleSubtaskResult 条件更新
    - graph-engine.js: maybeMaterializeItemizedSubtasks(org-planner 结算
      success 后物化子任务,agent_call/subgraph 两种形态都认,subgraph 场景
      扫子 run 全节点找 acceptance.md,找不到优雅降级不阻塞主产出);
      coder-validator 判 success 前新增子任务完整性闸门
      (ENGINE_FAIL_REASONS.SUBTASKS_INCOMPLETE)
    - task-planning.js / company.js: acceptance_mode 字段透传链路
    - bin/lemon.js: 新增 task subtasks / task subtask-result 两个子命令
    - coder-validator/agent.md、task-planning/agent.md: 对应提示词更新
    
    真实回归验证过(新建公司走完整链路),过程中发现两个跟本次改动无关的
    预存在问题:一次瞬时的 produces_missing 误判(重试即愈)、一个结构性的
    writesArtifacts 产出闸门不匹配 bug(另立 44 号文档记录,未修复)。
    
    全量单测 1120/1123 通过,3 个失败与本次改动无关(git stash 验证过)。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01U7xYyvv3pzuV9diMVUH5QL
    yibo authored
     
    Browse Dir »
  • 原来的 /task-board 挂的是 GoalTaskPane(折叠式目标→任务列表),字段少、
    没有搜索筛选排序、点任务是整页跳转。GoalTaskPane 同时被 GraphConsole
    左栏复用,原地重写会连带改掉那边,所以新做 TaskTable + TaskDetailModal
    两个组件只给 TaskBoard 用。
    
    - TaskTable:表格列(状态/描述/所属目标/负责Agent/优先级/创建·开始·结束
      时间),搜索框 + 状态/目标/负责人筛选 + 点表头排序
    - TaskDetailModal:点一行弹出详情,可编辑 + 增删改查(复用既有
      taskActions()/PATCH/POST/DELETE 端点,没加新接口)
    - taskView() 补上 assigneeAgentId 字段(DB 列早就有,只是没读侧暴露)
    
    已用真实登录态跑通端到端验证(建临时测试公司+任务,搜索/筛选/排序/
    增删改查全走了一遍,控制台零报错),确认 GraphConsole 侧栏未受影响。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01XUvQ2xSFk7YoavE5SQENdw
    yibo authored
     
    Browse Dir »

  • org-coder 等 9 个角色(含 _base 骨架模板)的四段式阶段流转靠调
    bin/lemon.js task transition,打到 agent-chat.js 的 /task/transition——
    这个端点靠自己的 sessions Map 找会话,work graph 节点场景下找不到,
    阶段流转静默跳过,中间产物不进任何跨节点共享的轨迹。
    
    调研确认范围后选了最保守的实现:不改 bin/lemon.js 的 CLI 签名、不碰
    agent-chat.js(当天正被另一个并行会话实时编辑,风险太高)。关键发现
    是 --artifact 参数本来就是 .../runs/{rootRunId}/nodes/{nodeRunId}/...
    形状的路径,两个 ID 已经现成编码在里面,不需要给子进程塞新环境变量,
    不需要改一个字的 agent.md 提示词。
    
    bin/lemon.js 在原有调用之外 best-effort 追加一次 POST
    /api/graph-runs/phase-note,新端点从 artifact 路径解出两个 ID,
    校验 run 真实存在后调 emitEvent() 追加 timeline 记录,只追加不碰
    graph_run_nodes/graph_runs 任何一行——不影响 needs/produces 契约。
    P0 拦截/OKR 更新两段旧逻辑维持原样,新端点是独立的第二条路径,不是
    迁移。
    
    新增测试 4 例,全量测试 1503/1506,零回归(剩余失败全部来自另一个
    并行会话进行中、还未提交的工作)。42 号文档 Phase 1-4 全部完工。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01BwG4b7Nq1NLR9eeE3M9Us9
    yibo authored
     
    Browse Dir »
  • 原来场景 C 固定只回放最近 20 条、单条截断到 500 字,ACP 进程重启/空闲超时
    后早于窗口的历史会静默丢失。改成按 token 预算(默认按 1M 上下文的一半,
    AGENT_MODEL_CONTEXT_TOKENS 可覆盖)从最新往最老收,装得下就整段注入。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01XUvQ2xSFk7YoavE5SQENdw
    yibo authored
     
    Browse Dir »
  • 39 号文档 P5 落地的是"配了 company.json.target_dir 就生效"这一半,
    "人怎么把这两个字段填进去"此前只能手工编辑 company.json。
    
    复用已存在的 PUT /api/company/:companyId(companyService.update()),
    没有新建端点。新增 target_dir/target_mode 分支,校验直接调
    path-resolver.validateLinkedTarget(39 号文档 P5 抽出来的共享函数,
    和 graph-engine.js 运行时校验同一份判据):合法路径写入成功、敏感
    路径黑名单拒绝、linked-new 非空目录拒绝、linked-existing 不存在拒绝,
    校验失败路由层转 400;传空字符串清空配置;不传 target_mode 默认
    linked-existing,与 graph-engine.js 的 readCompanyTargetConfig 同口径。
    
    前端:CompanyDashboard.vue 概览 tab 加一张配置卡片,本地表单状态
    不直接双向绑定 selectedCompany,保存失败不会冲掉用户刚打的字。
    
    新增测试 8 例,npm run web:build 编译通过。**未做真实浏览器点击
    验证**——服务器上有其它并行会话真实在跑的 agent 进程,这次没有重启
    服务器(42 号文档 §二记过一次"重启杀掉别人真实进程"的教训),文档
    里标记了这个已知缺口。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01BwG4b7Nq1NLR9eeE3M9Us9
    yibo authored
     
    Browse Dir »
  • 冲突在 docs/README.md:并行会话在真实端到端测试里发现 boss agent(39 号文档)还有
    三个单测没覆盖的真问题,把 39 号重新留在 todo/ 并拆出 42 号跟进文档;与我这边把
    39 号挪进 done/ 的判断冲突——采纳对方判断(39 号文档物理路径挪回 todo/system-design/,
    докс/README.md 用对方更新的行 + 补 42 号一行),因为他们是基于更新的真实测试结果。
    
    顺带把 29-dream-design.md 这次会话移到 done/ 后遗漏的引用修掉:
    system/agents/{dream-distiller,dream-screener}/agent.md、
    server/{routes/dream.js,services/dream-service.js}、
    server/tests/unit/dream-routes.test.js 六处 docs/todo/system-design/29-dream-design.md
    路径改成 docs/done/system-design/29-dream-design.md(no-dead-path-references.test.js /
    agent-md-refs.test.js 两条守卫测试之前没扫到这几处,这次真跑测试才暴露)。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 服务器重启会杀死正在跑的 graph 节点背后的真实进程,但 graph_run_nodes.
    status 永远停在 running(ceo 的两条唤醒路径都只认 blocked,对这个状态
    完全不可见)——39 号文档真跑复盘撞出来的坑,这次补上。
    
    1a:graph-engine.js 新增 sweepOrphanedRunningNodes(cutoffIso),复用
    routeFailure 的 infra_flake 分支(不发明新状态),server/index.js 在
    本次进程启动时刻挂一次开机扫描,排在其余「补一次全量扫描」钩子之前。
    
    1b:新增 forceFailNode() + POST /:id/nodes/:nodeId/force-fail,补上
    retry 端点管不到 running 节点这个缺口——人工/ceo 判定"这个真的救不
    回来了"时的手动出口,转 fail 后交给既有失败路由处理,不发明新路径。
    
    新增测试 12 例(graph-engine-orphan-sweep.test.js 4 例、
    graph-run-force-fail.test.js 8 例),全量测试 1483/1487,零回归。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01BwG4b7Nq1NLR9eeE3M9Us9
    yibo authored
     
    Browse Dir »
  • 39 号文档地基 0 当时只改了 company.js(goals/tasks 端点),graph-run.js
    (run 的 start/stop/retry/budget 控制端点)漏了同一处修复,还是纯
    requireAuth。真实端到端测试里 ceo 被唤醒后能读写 goal/task 板,但调
    /api/graph-runs/* 处理卡住的 run 时精确撞上 401——ceo 自己在对话记录里
    诊断出这一点并写出了人工需要跑的 curl 命令,间接验证了这条修复的必要性。
    
    两个挂载点(router + companyRouter)都改成 requireAuthOrInternal,两处
    :companyId/:id 的 param 中间件加了与 company.js 一致的 req.user?.internal
    旁路。route-auth-gate.test.js 同步更新断言。全量测试 1469/1473,零回归。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01BwG4b7Nq1NLR9eeE3M9Us9
    yibo authored
     
    Browse Dir »
  • 排查「前端刷新不及时/没有打字机效果」发现现状已是 SSE,不需要换 WebSocket,
    真正缺的是三件独立的事:
    
    - 打字机平滑:后端把 ACP chunk 按 10 字符切片再发 SSE,前端用定时器队列
      (agentChatEvents.js 的 createTypewriterQueue)匀速吐出,抹平网络突发。
    - 打断并发送:执行中解锁输入框,新增 ⤴ 按钮与 ⏹ Stop 视觉区分;stop 端点
      改为轮询确认 agentChatLocks 真正释放(3 秒预算)再返回 cleaned 字段,
      前端据此决定直接发送还是走"强制发送"兜底。顺手修了一个真实并发漏洞:
      同一 agentId + 同一 convId 的并发请求过去完全没有闸门,只是一直靠前端
      textarea 的 disabled 属性侥幸没被触发。
    - 串台加固:把 handleEvent 分发逻辑抽成不依赖组件实例的纯函数
      (agentChatEvents.js),补上 9b1c3f1 那条不变式的回归测试。
    
    已用真实 Claude CLI 会话(非 mock)在浏览器里验证打字机效果、打断并发送
    全流程、以及纯 Stop 无回归。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01HDtmdnE6pay9BhwZDAKK6M
    yibo authored
     
    Browse Dir »
  • 按 39 号设计文档落地 ceo 常驻监督:company.js 的 goals/tasks 端点
    放行内部令牌自环调用,POST /tasks 支持 draft 模式(agent 自主提任务
    不再立刻烧预算),ceo 补雇进公司创建流程;task-settle 新增事件驱动
    唤醒 + 30 分钟兜底轮询;graph-engine 的回退熔断收敛时顺手回写一条
    draft task,形成"卡住 → 自动/ceo 判断 → 重新规划"闭环。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01BwG4b7Nq1NLR9eeE3M9Us9
    yibo authored
     
    Browse Dir »

  • router.use(requireAuthOrInternal) 之后,agent-chat.js 自己的 selfChat() helper
    (TaskDispatch/TaskCallback/TaskTransition ×2/TaskReject/HumanRequest 共 7 处
    调用)一直在用裸 http.request 自己打自己的 /api/agent/:id/chat,没带
    x-internal-token,全部被 401 挡住——而且 chatRes 回调之前不看 statusCode,
    401 也照样打印"✅ 已唤醒",症状是静默的(跟 claude-cli-session-leak.md 一个模式)。
    实测复现:真实 dispatch 一个任务给 org-innovator,目标 agent 从未收到消息,
    任务卡在 dispatched 状态没人执行。
    
    修法:selfChat 加 x-internal-token header(跟 company.js/task-settle.js 的
    自环调用同一套机制),并且真的检查 chatRes.statusCode,非 2xx 打日志说明失败,
    不再假装成功。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 之前 invoke 端点只把结果落 job(.data/jobs-openapi.json),从不生成 conv-*
    会话、也不写 meta.json/history.json,导致网页 AgentChat 看不到任何 API
    调用产生的对话记录。现在复用 agent-chat.js 的会话存储函数,写到同一份
    system/agents/{agentId}/_global/conversations/{convId}/ 下。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »

  • POST /api/open/agents/:agentId/invoke —— 复用现有 company-scoped invoke
    (/api/open/companies/:companyId/agents/:agentId/invoke)那套 jobManager
    异步 job + 轮询/webhook 机制,但不查 company_agents 雇佣关系(全局 agent
    如 master/agent-creator/h3-video-prompt 本来就不挂在任何公司下,套雇佣
    校验只会无条件 404)。只查本体是否存在(system/agents/{id}/config.json)。
    
    两条 invoke 路由共用的执行逻辑(附件处理 + AgentManager 调用 + 写 job
    结果)提到 _executeAgentInvocation,避免整段复制。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 验证阶段真查出一个 Phase 1/2 都没顾上的真实漏洞:agent-chat.js(/api/agent/*)一直靠
    :agentId 里的 "companyId:role" 命名空间前缀(locator.companyStateDir 解析)区分会话
    属于哪家公司,但这个文件从只挂了 requireAuthOrInternal(登录即可),没有归属校验。
    curl 验证:任意登录用户对 GET /api/agent/{别人的companyId}:{role}/conversations
    依然 200,能跨租户读别家公司的 agent 会话。
    
    修法跟 company.js 同一套思路:router.param('agentId', ...) 解析 : 前的 companyId 段,
    过 canAccessCompany。三个例外:①没有 : 前缀的全局 agent(master/org-innovator 等,
    落 _global)不做归属校验,本来就全系统共享;②内部自环调用(req.user.internal)直接
    放行,招聘/训练自动流水线要能打到任意公司名下;③admin 不受限制。
    
    新增 agent-chat-company-gate.test.js:直接拿 Express 暴露的 router.params.agentId[0]
    单测这个 param handler,覆盖同公司放行/跨公司404/admin不受限/公司不存在404/全局agent
    放行/内部自环放行六种情况。顺带修好一个连带失败的既有测试文件
    (agent-chat-tenant-isolation.test.js,zz-tenant-alpha/beta 同样是从没真正建过的
    虚构公司 id)。
    
    验证:curl 复测跨租户读会话从 200 变 404,owner/admin 依然 200,全局 agent 不受影响;
    真实浏览器用全新账号确认 CompanyDashboard.vue 对"没有可用公司"已有现成空状态,不需要
    新写前端代码。全量 vitest 1433 passed,3 个无关既有失败不受影响。
    
    至此 38 号文档 Phase 0/1/2/3 全部完成。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • company-service.js:company.json 加 ownerId(无 SQL 表,纯 JSON 字段);create() 接收
    ownerId;list({ user }) 按传入用户过滤,不传就不过滤(company-heartbeat-service.js 等
    系统内部巡检要看全部公司);新增 canAccessCompany(company, user) 作为全仓库唯一一份
    归属判据(admin 不过滤,判负一律 404 不是 403,不暴露"这个 id 存在只是不是你的")。
    
    company.js 用 router.param('companyId', ...) 统一收口(约 50 条路由,含 okrPath() 系列
    动态路由)。graph-run.js 的两个挂载点参数形状不一样,设计阶段的描述不准确,实现时才
    发现:companyRouter 唯一路由是 /:companyId/graph-runs 直接查;主 router 全部路由是
    /:id/...,:id 指的是 run 不是公司,改成先查 run 的 company_id 再判断,run 不存在或
    公司不是你的统一回"run 不存在"。devloop.js 用 :pid 反查 company_id;POST /projects
    单独校验 body 里的 company_id(放在基本字段校验之后,避免归属检查打乱输入校验的错误
    优先级);GET /projects 按归属过滤列表。
    
    okr.js 最终没有按原计划接入 companyId 归属校验——实现前查证发现 OkrDashboard.vue
    挂载的 /okr 页面全站没有任何导航入口指向它,是个孤儿页面,给访问不到的页面建"选公司"
    前端交互不划算,改成 requireAdmin(跟 dream.js 同款),先堵住直接拼 URL 看到跨公司
    聚合数据的口子。真正在用的公司级 OKR(company.js 的 /:id/okr)已随 company.js 一起隔离。
    
    存量数据迁移:scripts/backfill-company-owner.js(npm run backfill:company-owner),
    两家存量公司已划给种子账号,幂等验证过。
    
    8 个既有测试文件用的公司 id 都是从没真正建过的字符串标签,挂锁后集体 404——新增共享
    helper server/tests/helpers/ensure-test-company.js 修复;两处原有的"跳过已有
    company.json 的目录,那是真公司"防御逻辑,判据从"有没有 company.json"改成"ownerId
    是不是 -1 这个测试专用标记",否则新出现的测试夹具会被误判成真公司永远不清理。
    
    验证:真实 curl 走过(admin 看到全部存量公司、新用户看到空列表、新用户拼 URL 访问
    别人的公司 404、新建公司 ownerId 精确落在创建者、admin 和创建者都看得到其他人看不到);
    全量 vitest 1427 passed,3 个无关既有失败不受影响。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 给此前完全零防护的 company.js/okr.js/tasks.js/devloop.js/graph-run.js(两个挂载点)/
    arena.js/config.js/evolution.js/whatsapp-auth.js 挂 requireAuth;dream.js 挂 requireAdmin
    (系统级经验汇流视图,只有 admin 能看)。open-api.js(自己一套 OPEN_API_KEYS)/auth.js
    自身/health 端点按计划排除。这一步只挡"没登录",公司归属过滤留到 Phase 2。
    
    agent-chat.js 不能直接挂 requireAuth:company.js(自动建公司/招聘后自动训练)、
    task-settle.js(goal 汇报回调)三处服务端自环调用会 fetch 自己的 /api/agent/* 端点,
    拿不到任何用户 cookie,直接挂锁会打断这几条核心自动化链路。新增 requireAuthOrInternal +
    jwt-auth.js 的进程内 INTERNAL_TOKEN(启动时随机生成,只活在内存里):自环调用带上
    x-internal-token 头放行,其余情况仍要求真实登录;三处调用点同步改造。
    
    17 个既有测试文件、67+ 处用例直接拿 supertest 打这些路由,从没设过登录 cookie(测的是
    业务逻辑不是鉴权)。没有逐个补登录态,改为 requireAuth/requireAdmin 在 NODE_ENV=test
    时默认放行,除非显式 FORCE_REAL_AUTH=1;新增 route-auth-gate.test.js 在放行关闭状态下
    做结构检查(router.stack 里中间件是否真的排在所有路由前面),确保"锁真的挂上了"这件事
    有测试兜底,不被业务测试的默认放行悄悄掩盖。
    
    验证:真实 curl 走过全部路由(无 cookie 401、admin 登录全通、非 admin 能看公司但
    dream.js 403);手工删过一行 requireAuth 确认守卫测试真的会红;全量 vitest 1427 passed,
    另 3 个失败与本次改动无关(stash 验证过改前改后一致,系仓库并发工作遗留)。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »

  • run_b962ea87f58f 卡在 wallclock_ms 耗尽的锚点未结清问题上,需要人工追加预算
    才能继续。extendBudget 只增不减、只加到已配额的维度上,追加时顺带结清卡死的
    墙钟锚点(否则 startRunSync 一启动就把挂起时长记进新预算里)。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 招聘自动训练与「重新训练」按钮都是给 org-innovator 发消息、由它去执行训练,
    所以对话记录一直落在 org-innovator 的会话库里,而不是被训练的那个 agent 自己——
    conversationId 里带目标 agent 的名字只是命名巧合,容易让人在错的 agent 页面下找不到记录
    (今天实测:手工模板训练页删除前产生的一条记录就复现过这个问题)。
    
    - company.js /hire:训练 conversationId 提前算好并通过 res.json 带回给前端
      (不再等 setTimeout 回调里才生成,那时响应早发完了)
    - CompanyDashboard.vue 重新训练按钮:发完请求立即 window.open 到正确的
      /agent/org-innovator/{convId}
    - CompanyDashboard.vue 招聘弹窗:雇佣触发自动训练时,弹窗给出可复制的正确链接
      (不自动开新标签页——这条训练要等 agent-creator 先写完定义,立刻打开大概率是空的)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • POST /:cid/tasks -> enqueueTaskRun 入队时从未写过 assignee_agent_id,
    COO 心跳巡检靠 tasks.assignee_agent_id(the "to" field) 判断任务有没有
    人接手,读到永远的 NULL 就把正常跑图的任务误报成孤儿任务
    (本周已发生两次:T-mt25asz4、T-mt5d3mi9-57cf)。
    
    - enqueueTaskRun 入队时补写占位哨兵 'org-graph'(GRAPH_TAKEOVER_ASSIGNEE)
    - settleTaskFromRun 收敛时用 COALESCE 兜底,只在仍为空时补,不覆盖
      派单式任务(agent-chat dispatch 流程)已经写好的真实 assignee
    - TASK_PATCH_COLUMNS 补上 assigneeAgentId,否则 updateTask 写不进这列
    
    T-mt5dkvjm
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • system/templates/ 已在 28 号文档的合并里并入 system/agents/,「模板」作为独立
    数据概念已经消失;确认后手工 CRUD 页面(创建/改名/删除骨架、触发生成与训练、
    传 skill 文件、provision)也已经没人用,一并下线:
    
    - 前端:删 AppNav「Templates」菜单项、/templates 路由、TemplateAgents.vue
    - 后端:company.js 删掉只服务于该页面的写接口(PATCH/DELETE/POST /templates、
      generate/train/skills-upload/provision/files 系列),保留 GET /templates
      (CompanyDashboard 招聘弹窗和 CLI `company templates` 命令仍依赖它列出可招角色)
    - CLI:bin/lemon.js 删 `template create`/`template train`(调用的是被删接口)
    - 修正两处过时注释(company-service.js / graph-service.js):不再声称
      buildAgentDefinitionBrief 有两个调用方,/hire 是唯一派单点
    - work-layer-body-paths.test.js 同步更新断言(2 处调用方 → 1 处)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »

  • 35 号文档剩下的三个提案,彼此直接相关(C 产出 D 消费的证据,B 展示 D 算出的结果),
    一次做完。
    
    提案 D(goal-contract.js 引擎级证据闸门):
      `runBenchmark` 新增 evidence 检测——`evidence_hint` 命中"浏览器/视口/开发者工具/
      console/network/截图"类关键词的维度,必须在 evidence.<dim> 有真实字符串支撑,
      否则该维度判不过(哪怕分数达标)。证据来源两处都认:`--target` 文件自带的
      `evidence.<dim>`,或同目录姊妹判词文件 `attempt-<n>.json` 的 `dimensions[].evidence`
      ——后者是 benchmark-validator/agent.md 早就在教的既有写法,不用 agent 多写一份。
      新增 `dimensions[].evidence_required` 字段随判决落盘。6 条新单测。
    
    提案 C(可复用视觉验收工具):
      新增 scripts/visual-acceptance-check.mjs,四个子命令(overflow/style/console/
      contrast),封装最常见的几类真实浏览器检查(Playwright),agent 通过 bash 直接
      调用,不用每次自己临时想办法(真实撞见过两个极端:一次真写了 Playwright 认真测,
      一次只写了正则解析文本)。配一份不进 vitest 快速循环的独立冒烟脚本(起真
      Chromium,几秒量级)。benchmark-validator/agent.md Step 2 加了引用说明。
    
    提案 B(控制台证据可见性):
      GET /api/graph-runs/:id 新增 benchmarkVerdicts 字段(只读 goal-contract.js 已落盘
      的判决,不重新计算)。NodeDetailPanel 概览 tab 新增"验收证据"小节:逐维度显示
      分数 + 是否有证据支撑,evidence_required 且缺证据的标 ⚠️。纯展示,不改判定逻辑。
      2 条新单测。
    
    全量单测 1012/1012 通过;控制台功能用 Chrome 实测过(截图确认"验收证据"小节正确
    渲染)。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 用户反馈:控制台原来只能逐个节点点「输入输出」tab 看产物,缺一个全局入口。
    新增 TaskFilesPanel.vue,挂在页头,跨节点/跨 run 一次性列出这个任务的全部产物。
    
    做的时候发现分两类、且第二类之前完全没覆盖:
      · 节点产出索引(graph_files,按 company+task 检索)——规划笔记/日志/契约快照
      · 项目根(W2 ensureProjectRoot,data/companies/{cid}/projects/{pid}/)——
        真实交付物代码写的地方,跨节点共享,indexNodeOutputs 从不扫它
    
    后端补 readProjectRootRecord 导出 + GET /api/graph-runs/:id 挂 projectRoot 字段;
    项目根的文件列表复用已有的 GET /:companyId/files(此前零调用方),不新开列目录端点。
    新增 graph-run-route-io.test.js 两例(分配前 null、分配后原样透传 + 幂等)。
    
    顺带订正 34 号文档:当时断言"index.html 从未被写出来"是错的——真实网站文件
    其实在项目根里,只是没查那个目录。34 号文档 §零 记录了订正过程,问题收窄成
    "validator 有没有真的做过浏览器级检查"(仍未解决,留待下次)。
    
    全量单测 1000/1000 通过。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »

  • company.js 的 hire 路由原来用一发 fetch(...).catch(...) 回环自己去触发 agent-creator
    填新角色的定义文件——fire-and-forget,.catch 只接网络层错误,agent-creator 撞车时
    (另一次调用正占着它)/api/agent/:agentId/chat 返回 429,被原样当成"成功"吞掉。
    新角色因此永久停在 base 空壳 + status=training,唯一痕迹是一行 console.error(且这行
    在 429 场景下根本不会触发)。
    
    新增两个模块:
    - agent-session-lock.js:从 agent-chat.js 抽出的共享互斥锁存储(纯数据结构,Map +
      超时常量),agent-chat.js 的路由与 hire 路由现在用同一把锁。不能各起一把——
      SessionPool.getOrCreate() 不检查上一次调用有没有跑完,两条路径分开维护锁的话,
      各自都以为自己没有并发,会把第二轮 prompt 发进一个还在处理中的 CLI 会话
      (未定义行为,比现在的"被拒绝丢弃"更危险,见 32 号文档 §二方案 B)。
    - agent-body-filler.js:同进程直接调 AgentManager.callAgent,替掉 HTTP 回环。
      拿到锁才发起调用,锁被占用时等一等重试(最多 3 次,5s/15s 退避),重试耗尽在
      本体目录写一个哨兵文件 .body_fill_failed.json(不动 system_agents.status /
      config.json.status——那两个字段还有 draft→active 毕业逻辑在读,改取值域风险
      大于收益)。
    
    company.js 的 hire 路由第 1 步从 fire-and-forget 换成 await,顺带让第 2/3 步
    (技能注入、5 秒后触发 Innovator 训练)拿到"agent-creator 真的写完了"这个保证——
    第 3 步原来的 setTimeout(5000) 靠的是"假设 5 秒内写完",现在这个假设不再需要,
    但没有顺手删掉那个 setTimeout(不在这次修复范围内,留着无害)。hire 接口本身
    仍是异步的,调用方还是立即拿到 training 状态,没有改成同步等待完成。
    
    agent-chat.js 的锁声明改成从共享模块 require,行为逐字不变——用现有 5 个
    agent-chat 相关测试文件(36 条)验证过零回归。
    
    新增 agent-body-filler.test.js(6 条):首次成功、重试后成功、锁被占用时等待
    重试、重试耗尽写失败标记、成功后清掉上一轮的失败标记、只释放自己抢到的锁
    (不误删第三方在途期间抢到的新锁)。用 vi.useFakeTimers() 控制重试等待,
    不用真等 5s/15s。
    
    全量 npx vitest run:151 文件 / 1384 用例,零回归。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »
  • 只做了这一件事,没有跟着做「dream 改成 org/work graph 来跑」——两者被
    单独评估过:org/work graph 是单公司作用域(org graph=公司级 workflow,
    work graph=公司内某任务的节点图),dream 却是跨公司扫描,套进去要么破坏
    作用域假设要么得新开图类型;且 graph 引擎自带的验收机制(goal-contract
    benchmark/test-script)判的是「产物过没过验收脚本」,对「经验提炼得对
    不对」这种自由文本判断没有对应机制——不管套不套 graph 都得单独设计
    rubric,是同一件事。用户明确只要 REST/控制台这一半。
    
    ## REST(server/routes/dream.js,挂 /api/dream)
    
    路由本身没有新业务逻辑,全部委托给已测过的 dream-service.js:
    GET /agents(总览)、GET /agents/:id(详情,含定义文件可写性 + 经验
    审计)、POST /agents/:id/run(手动跑一次,dryRun 只看有没有新原料不
    起 LLM)、POST /sweep + GET /sweep/status(全量扫描后台跑 + 轮询,内存
    态并发锁防止连点两下)。
    
    ## 控制台(web/src/views/DreamConsole.vue,路由 /dream)
    
    一个表格 + 两个弹窗:本体列表(详情/跑一次/dry-run)+ 全量扫描按钮
    (15 秒轮询状态)。导航栏加 🌙 Dream 入口。
    
    ## 验证
    
    server/tests/unit/dream-routes.test.js 4 例,monkeypatch 掉
    dream-service 导出对象上的六个函数(不走真文件系统,不误扫真仓库
    76 个本体),只验路由层自己的逻辑:端点接线、sweep 并发锁状态机。
    
    真起服务器打了真数据:GET /agents/innovator-evolver 正确报
    SKILL.md 因没进 git 历史而不可写、经验审计吐出上次真跑沉淀的 4 条
    经验及原文;dry-run 正确报 no-new-material,没多花一次 LLM 调用。
    浏览器里走了一遍列表→详情→dry-run,与直接打 API 结果一致。
    
    955 个单测全绿。
    
    文档:29-dream-design.md §5.7 记录实现与「为什么不做 graph 化」的
    理由;28-followups.md / docs/README.md 同步更新 dream 现状。
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Dir »

  • 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>
    yibo authored
     
    Browse Dir »
  • 主体由另一个会话完成(其会话已关闭,由我接手核对并提交),我补了两处修复
    与一条守卫。
    
    ## 它修的是什么
    六处「服务端自己给某个 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>
    yibo authored
     
    Browse Dir »
  • ## 第 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>
    yibo authored
     
    Browse Dir »
  • **本文件除了 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>
    yibo authored
     
    Browse Dir »

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

  • 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 Dir »
  • 把「人类提的一个需求」与「机器执行的一件活」拆成两级。这不是新概念:
    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 Dir »