Commit 32227f12f44ceef9d5e23586add4e57ead592278
1 parent
01267e8a
fix(tasks): 人类任务被 graph-engine 接管跑图时补写 assignee_agent_id
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>
Showing
3 changed files
with
36 additions
and
1 deletions
server/routes/company.js
| ... | ... | @@ -1076,6 +1076,10 @@ function enqueueTaskRun({ companyId, taskRow, budget }) { |
| 1076 | 1076 | startedAt: null, |
| 1077 | 1077 | finishedAt: null, |
| 1078 | 1078 | blockedReason: null, |
| 1079 | + // 人类任务从这一刻起被 graph-engine 接管跑图,不再是「无人认领」(T-mt5dkvjm): | |
| 1080 | + // 没有真实派单对象就写占位哨兵,COO 心跳巡检靠 to 字段判断时才不会把它当孤儿误报。 | |
| 1081 | + // 只在还没有真实值时才写,避免盖掉别的派单流程已经写好的 assignee。 | |
| 1082 | + assigneeAgentId: taskRow.assignee_agent_id || wdb.GRAPH_TAKEOVER_ASSIGNEE, | |
| 1079 | 1083 | }); |
| 1080 | 1084 | return { runId, run: wdb.getRun(runId) || {}, artifact, task }; |
| 1081 | 1085 | } catch (err) { | ... | ... |
server/services/work-graph-db.js
| ... | ... | @@ -1925,6 +1925,7 @@ const TASK_PATCH_COLUMNS = { |
| 1925 | 1925 | budgetJson: 'budget_json', |
| 1926 | 1926 | currentRunId: 'current_run_id', |
| 1927 | 1927 | attempt: 'attempt', |
| 1928 | + assigneeAgentId: 'assignee_agent_id', | |
| 1928 | 1929 | reportTo: 'report_to', |
| 1929 | 1930 | blockedReason: 'blocked_reason', |
| 1930 | 1931 | queuedAt: 'queued_at', |
| ... | ... | @@ -2009,6 +2010,14 @@ const TASK_TERMINAL_STATUS = ['completed', 'failed', 'cancelled']; |
| 2009 | 2010 | const GOAL_TERMINAL_STATUS = ['done', 'failed', 'cancelled', 'archived']; |
| 2010 | 2011 | |
| 2011 | 2012 | /** |
| 2013 | + * 人类任务被 graph-engine 接管跑图时,`assignee_agent_id` 没有真实派单对象可写—— | |
| 2014 | + * 占位成这个哨兵值,让 COO 心跳巡检的「to 是否为 null」判据不再把「有图在跑」误判成「没人认领」 | |
| 2015 | + * (T-mt5dkvjm)。`enqueueTaskRun` 入队时写一次,这里(run 收敛)做兜底 COALESCE: | |
| 2016 | + * 只在该列仍是 NULL 时才补,不覆盖派单式任务(agent-chat 的 dispatch 流程)已经写好的真实值。 | |
| 2017 | + */ | |
| 2018 | +const GRAPH_TAKEOVER_ASSIGNEE = 'org-graph'; | |
| 2019 | + | |
| 2020 | +/** | |
| 2012 | 2021 | * run 收敛 → task 回写的**条件更新**(27-P3C §五.1.1 的钩子用,缺陷 F1)。 |
| 2013 | 2022 | * |
| 2014 | 2023 | * 两个 WHERE 条件各挡一类事故,缺一不可: |
| ... | ... | @@ -2024,7 +2033,8 @@ function settleTaskFromRun(taskId, runId, status) { |
| 2024 | 2033 | const now = new Date().toISOString(); |
| 2025 | 2034 | return getDb() |
| 2026 | 2035 | .prepare(` |
| 2027 | - UPDATE tasks SET status = @status, blocked_reason = NULL, finished_at = @now, updated_at = @now | |
| 2036 | + UPDATE tasks SET status = @status, blocked_reason = NULL, finished_at = @now, updated_at = @now, | |
| 2037 | + assignee_agent_id = COALESCE(assignee_agent_id, '${GRAPH_TAKEOVER_ASSIGNEE}') | |
| 2028 | 2038 | WHERE id = @id |
| 2029 | 2039 | AND current_run_id = @runId |
| 2030 | 2040 | AND status NOT IN (${TASK_TERMINAL_STATUS.map((s) => `'${s}'`).join(', ')}) |
| ... | ... | @@ -2650,6 +2660,7 @@ module.exports = { |
| 2650 | 2660 | taskProgress, |
| 2651 | 2661 | maxTaskSeq, |
| 2652 | 2662 | settleTaskFromRun, |
| 2663 | + GRAPH_TAKEOVER_ASSIGNEE, | |
| 2653 | 2664 | // 批 3 排队:三条都是条件更新,调用方靠 changes 判「是不是我做的」 |
| 2654 | 2665 | claimQueuedTask, |
| 2655 | 2666 | blockQueuedTask, | ... | ... |
server/tests/unit/goal-task-acceptance.test.js
| ... | ... | @@ -152,6 +152,26 @@ describe('27-P3C 批 1 验收(§八 4 / 5 / 6 / 10 / 11 / 12b)', () => { |
| 152 | 152 | }); |
| 153 | 153 | |
| 154 | 154 | // ────────────────────────────────────────────────────────────── |
| 155 | + // T-mt5dkvjm · assignee_agent_id 不再是永远的 NULL | |
| 156 | + // ────────────────────────────────────────────────────────────── | |
| 157 | + | |
| 158 | + it('POST /:cid/tasks 入队后 assignee_agent_id 不是 NULL —— COO 心跳巡检靠 to 字段判「有没有人接」,一直是 null 会被误报成孤儿任务(T-mt5dkvjm)', async () => { | |
| 159 | + const cid = newCompany(); | |
| 160 | + const [status, body] = await call('POST', `/api/company/${cid}/tasks`, { task: '起个图跑任务' }); | |
| 161 | + expect(status).toBe(201); | |
| 162 | + | |
| 163 | + // 入队当下就该有占位 assignee —— 这正是 task-service.getAggregatedTasks 回给 COO 的 `to` 字段 | |
| 164 | + expect(wdb.getTask(body.taskId).assignee_agent_id).toBe(wdb.GRAPH_TAKEOVER_ASSIGNEE); | |
| 165 | + | |
| 166 | + // run 收敛之后也不能被冲回 null:settleTaskFromRun 的 COALESCE 只在为空时兜底,不覆盖已有值 | |
| 167 | + const sse = require('../../services/graph-run-sse'); | |
| 168 | + expect(sse.transitionRun(body.runId, 'running', 'completed', {}, { summary: '用例直推终态' })).toBeTruthy(); | |
| 169 | + const settled = wdb.getTask(body.taskId); | |
| 170 | + expect(settled.status).toBe('completed'); | |
| 171 | + expect(settled.assignee_agent_id).toBe(wdb.GRAPH_TAKEOVER_ASSIGNEE); | |
| 172 | + }); | |
| 173 | + | |
| 174 | + // ────────────────────────────────────────────────────────────── | |
| 155 | 175 | // 验收 12b · 写侧与读侧落在同一个 run 目录(路径缺陷 A) |
| 156 | 176 | // ────────────────────────────────────────────────────────────── |
| 157 | 177 | ... | ... |