fix(task-flow): QC/PM 驳回加重试上限,打破「空交付件→驳回→重做」死循环

根因:develop agent 产出空交付件 → QC 驳回(qc_reject)直接 qc_review→submitted,
不递增 retry_count、不检查 task_max_retry 上限 → 无限重做循环;3 个 running 任务
占满 max_concurrent_agents=3 全局名额 → agent poller avail=0 不再派发 6 个 submitted
任务 → 任务整体饿死,只有手动 start_agents(绕过并发限制)才走一步。

修复:
1. task_capability.py 新增 _reject_with_retry_limit:退回时 retry_count+1,超
   task_max_retry(默认3)则转 failed(释放并发名额,交 failed poller 报故障给人工)。
   qc_reject_task / reject_task 统一走它。
2. agent_loop.py qc_review_run 驳回时感知超限转 failed,跳过建 qc_reject 问题
   (failed poller 会报 fault_report),返回 failed 而非 rejected。
This commit is contained in:
ymq 2026-08-19 18:15:39 +08:00
parent ccc7baafbd
commit 3ffe39e885
2 changed files with 52 additions and 8 deletions

View File

@ -1674,6 +1674,12 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
return {"status": "approved", "task_id": task_id, "comment": decision.get('comment', '')}
else:
comment = decision.get('comment', '')
from .task_capability import qc_reject_task
ok, msg = await qc_reject_task(task_id, project_id, who="agent.qc", agent_id=agent_id, comment=comment)
if not ok and "failed" in msg:
# 重复退回达上限 → 已转 failed不再建 qc_reject 问题failed poller 会报 fault_report 给人工)
logger.info(f"qc_review_run reject->failed: task={task_id} {msg}")
return {"status": "failed", "task_id": task_id, "comment": msg}
from .communication import raise_problem
rejection_q = decision.get("questions") or comment or "交付件不合规"
await raise_problem("qc_reject", rejection_q, "agent.qc",
@ -1681,8 +1687,6 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
first_handler_role=task_role,
context={"qc_comment": comment, "deliverable_type": deliverable_type},
suspend_task=False)
from .task_capability import qc_reject_task
await qc_reject_task(task_id, project_id, who="agent.qc", agent_id=agent_id, comment=comment)
logger.info(f"qc_review_run reject: task={task_id} -> submitted")
return {"status": "rejected", "task_id": task_id, "comment": comment, "question": rejection_q}

View File

@ -117,12 +117,52 @@ async def qc_approve_task(task_id, tenant_id, who=None, agent_id=None, comment=N
who=who, agent_id=agent_id, detail=comment)
async def qc_reject_task(task_id, tenant_id, who=None, agent_id=None, comment=None):
"""QC 退回qc_review → submitted清 claimed_by被退角色重新认领重做"""
return await _transition(task_id, tenant_id, S_QC_REVIEW, S_SUBMITTED, 'qc_reject',
async def _reject_with_retry_limit(task_id, tenant_id, from_state, action,
who=None, agent_id=None, comment=None):
"""退回重做 + 重试上限retry_count+1超 task_max_retry 则转 failed打破死循环
未超限from_state submitted正常退回重做
超限from_state failed释放并发名额 failed poller 报故障给人工
返回 (ok, message)
"""
if not task_id or not tenant_id:
return False, "缺少 task_id 或 tenant_id"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
# 重复计数 +1CAS仅当任务仍在 from_state
await sor.sqlExe(
"UPDATE pipeline_tasks SET retry_count=retry_count+1 "
"WHERE id=${tid}$ AND tenant_id=${tn}$ AND state=${from}$",
{"tid": task_id, "tn": tenant_id, "from": from_state})
recs = await sor.sqlExe(
"SELECT retry_count FROM pipeline_tasks WHERE id=${tid}$", {"tid": task_id})
await sor.sqlExe("COMMIT", {})
rc = int(getattr(recs[0], 'retry_count', 0) or 0) if recs else 0
from .workspace import get_max_task_retry
max_retry = await get_max_task_retry(sor)
if rc >= max_retry:
err = f"{action} 退回 {rc} 次已达上限({max_retry}){comment or '交付件不合规'}"
await sor.sqlExe(
"UPDATE pipeline_tasks SET state='failed', claimed_by=NULL, "
"last_error=${err}$, updated_at=NOW() "
"WHERE id=${tid}$ AND state=${from}$",
{"tid": task_id, "from": from_state, "err": err})
await record_audit(tenant_id, 'pipeline_tasks', task_id, action,
from_state=from_state, to_state=S_FAILED,
who=who, agent_id=agent_id, detail=err, sor=sor)
logger.info("reject retry limit hit: task=%s rc=%s -> failed", task_id, rc)
return False, f"退回 {rc} 次已达上限({max_retry}),任务已标记 failed等待人工介入"
# 未超限:正常退回 submitted
return await _transition(task_id, tenant_id, from_state, S_SUBMITTED, action,
who=who, agent_id=agent_id, detail=comment)
async def qc_reject_task(task_id, tenant_id, who=None, agent_id=None, comment=None):
"""QC 退回qc_review → submitted重复计数+1超上限转 failed 打破死循环)。"""
return await _reject_with_retry_limit(task_id, tenant_id, S_QC_REVIEW, 'qc_reject',
who=who, agent_id=agent_id, comment=comment)
async def approve_task(task_id, tenant_id, who=None, agent_id=None, comment=None):
"""审核通过review → approved。"""
return await _transition(task_id, tenant_id, S_REVIEW, S_APPROVED, 'approve',
@ -130,9 +170,9 @@ async def approve_task(task_id, tenant_id, who=None, agent_id=None, comment=None
async def reject_task(task_id, tenant_id, who=None, agent_id=None, comment=None):
"""审核退回review → submitted清 claimed_by被退角色重新认领)。"""
return await _transition(task_id, tenant_id, S_REVIEW, S_SUBMITTED, 'reject',
who=who, agent_id=agent_id, detail=comment)
"""审核退回review → submitted重复计数+1超上限转 failed 打破死循环)。"""
return await _reject_with_retry_limit(task_id, tenant_id, S_REVIEW, 'reject',
who=who, agent_id=agent_id, comment=comment)
async def complete_task(task_id, tenant_id, who=None, agent_id=None):