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,6 +1076,10 @@ function enqueueTaskRun({ companyId, taskRow, budget }) { | ||
| 1076 | startedAt: null, | 1076 | startedAt: null, |
| 1077 | finishedAt: null, | 1077 | finishedAt: null, |
| 1078 | blockedReason: null, | 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 | return { runId, run: wdb.getRun(runId) || {}, artifact, task }; | 1084 | return { runId, run: wdb.getRun(runId) || {}, artifact, task }; |
| 1081 | } catch (err) { | 1085 | } catch (err) { |
server/services/work-graph-db.js
| @@ -1925,6 +1925,7 @@ const TASK_PATCH_COLUMNS = { | @@ -1925,6 +1925,7 @@ const TASK_PATCH_COLUMNS = { | ||
| 1925 | budgetJson: 'budget_json', | 1925 | budgetJson: 'budget_json', |
| 1926 | currentRunId: 'current_run_id', | 1926 | currentRunId: 'current_run_id', |
| 1927 | attempt: 'attempt', | 1927 | attempt: 'attempt', |
| 1928 | + assigneeAgentId: 'assignee_agent_id', | ||
| 1928 | reportTo: 'report_to', | 1929 | reportTo: 'report_to', |
| 1929 | blockedReason: 'blocked_reason', | 1930 | blockedReason: 'blocked_reason', |
| 1930 | queuedAt: 'queued_at', | 1931 | queuedAt: 'queued_at', |
| @@ -2009,6 +2010,14 @@ const TASK_TERMINAL_STATUS = ['completed', 'failed', 'cancelled']; | @@ -2009,6 +2010,14 @@ const TASK_TERMINAL_STATUS = ['completed', 'failed', 'cancelled']; | ||
| 2009 | const GOAL_TERMINAL_STATUS = ['done', 'failed', 'cancelled', 'archived']; | 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 | * run 收敛 → task 回写的**条件更新**(27-P3C §五.1.1 的钩子用,缺陷 F1)。 | 2021 | * run 收敛 → task 回写的**条件更新**(27-P3C §五.1.1 的钩子用,缺陷 F1)。 |
| 2013 | * | 2022 | * |
| 2014 | * 两个 WHERE 条件各挡一类事故,缺一不可: | 2023 | * 两个 WHERE 条件各挡一类事故,缺一不可: |
| @@ -2024,7 +2033,8 @@ function settleTaskFromRun(taskId, runId, status) { | @@ -2024,7 +2033,8 @@ function settleTaskFromRun(taskId, runId, status) { | ||
| 2024 | const now = new Date().toISOString(); | 2033 | const now = new Date().toISOString(); |
| 2025 | return getDb() | 2034 | return getDb() |
| 2026 | .prepare(` | 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 | WHERE id = @id | 2038 | WHERE id = @id |
| 2029 | AND current_run_id = @runId | 2039 | AND current_run_id = @runId |
| 2030 | AND status NOT IN (${TASK_TERMINAL_STATUS.map((s) => `'${s}'`).join(', ')}) | 2040 | AND status NOT IN (${TASK_TERMINAL_STATUS.map((s) => `'${s}'`).join(', ')}) |
| @@ -2650,6 +2660,7 @@ module.exports = { | @@ -2650,6 +2660,7 @@ module.exports = { | ||
| 2650 | taskProgress, | 2660 | taskProgress, |
| 2651 | maxTaskSeq, | 2661 | maxTaskSeq, |
| 2652 | settleTaskFromRun, | 2662 | settleTaskFromRun, |
| 2663 | + GRAPH_TAKEOVER_ASSIGNEE, | ||
| 2653 | // 批 3 排队:三条都是条件更新,调用方靠 changes 判「是不是我做的」 | 2664 | // 批 3 排队:三条都是条件更新,调用方靠 changes 判「是不是我做的」 |
| 2654 | claimQueuedTask, | 2665 | claimQueuedTask, |
| 2655 | blockQueuedTask, | 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,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 | // 验收 12b · 写侧与读侧落在同一个 run 目录(路径缺陷 A) | 175 | // 验收 12b · 写侧与读侧落在同一个 run 目录(路径缺陷 A) |
| 156 | // ────────────────────────────────────────────────────────────── | 176 | // ────────────────────────────────────────────────────────────── |
| 157 | 177 |