fix(lint): 修复 PR #14 引入的 lint 回退 (119→0)
PR #14 从旧分支复制文件导致回退了 PR #10 的 lint 修复。 修复内容: - autoflake 移除未使用导入/变量 - autopep8 修复缩进/空格 - 手动修复 F821(pathlib→Path), F541(f-string), F841(未使用变量) - 所有修复均通过 flake8 --max-line-length=120 --extend-ignore=E501 检查 (0 errors)
This commit is contained in:
+162
-71
@@ -14,7 +14,6 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -22,7 +21,7 @@ from typing import Any, Dict, List, Optional
|
||||
from src.blackboard.models import Task
|
||||
from src.blackboard.db import get_connection
|
||||
from src.daemon.spawner import AgentBusyError
|
||||
from src.daemon.router import AgentRouter, RouteDecision
|
||||
from src.daemon.router import AgentRouter
|
||||
|
||||
logger = logging.getLogger("moziplus-v2.dispatcher")
|
||||
|
||||
@@ -64,7 +63,8 @@ class Dispatcher:
|
||||
if self._legacy_mode:
|
||||
self.registered_agents = set(registered_agents or [])
|
||||
self.capability_map = capability_map or {}
|
||||
logger.warning("Dispatcher running in legacy mode (no AgentRouter)")
|
||||
logger.warning(
|
||||
"Dispatcher running in legacy mode (no AgentRouter)")
|
||||
|
||||
def decide(self, task: Task, action_type: str = "") -> Dict[str, Any]:
|
||||
"""调度决策(委托给 Router)
|
||||
@@ -124,16 +124,21 @@ class Dispatcher:
|
||||
"""
|
||||
# 安全红线检查(调度前拦截)
|
||||
# Mail 是 Agent 间通信,不做 guardrail 检查
|
||||
is_mail = project_config.get("project_id") == "_mail" if project_config else False
|
||||
is_mail = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if self.guardrails and not is_mail:
|
||||
violations = self.guardrails.check_task(task)
|
||||
critical = [v for v in violations if v.action in ("block_and_notify", "terminate_and_escalate")]
|
||||
critical = [
|
||||
v for v in violations if v.action in (
|
||||
"block_and_notify",
|
||||
"terminate_and_escalate")]
|
||||
if critical:
|
||||
v = critical[0]
|
||||
logger.warning("Task '%s' blocked by guardrail: %s - %s",
|
||||
task.title, v.rule_id, v.message)
|
||||
# 写入黑板事件
|
||||
_routing_db = Path(project_config["db_path"]) if project_config and "db_path" in project_config else self.db_path
|
||||
_routing_db = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else self.db_path
|
||||
if _routing_db:
|
||||
self._record_routing(task, {"level": DispatchLevel.BLOCKED, "agent_id": "none",
|
||||
"reason": v.message}, "blocked", v.message, _routing_db)
|
||||
@@ -152,7 +157,8 @@ class Dispatcher:
|
||||
decision = self.decide(task, action_type)
|
||||
level = decision["level"]
|
||||
# 从 project_config 获取项目级 DB 路径(路由审计日志写入项目 DB)
|
||||
_routing_db = Path(project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
_routing_db = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
agent_id = decision["agent_id"]
|
||||
|
||||
# v2.7.2: counter 检查移到 spawn_full_agent 内部
|
||||
@@ -160,7 +166,8 @@ class Dispatcher:
|
||||
|
||||
# 本地执行
|
||||
if level == DispatchLevel.LOCAL:
|
||||
self._record_routing(task, decision, "dispatched", None, _routing_db)
|
||||
self._record_routing(
|
||||
task, decision, "dispatched", None, _routing_db)
|
||||
return {
|
||||
"level": level.value,
|
||||
"agent_id": "daemon",
|
||||
@@ -172,7 +179,8 @@ class Dispatcher:
|
||||
# Full Agent / Escalate spawn
|
||||
if level in (DispatchLevel.FULL_AGENT, DispatchLevel.ESCALATE):
|
||||
if not self.spawner:
|
||||
self._record_routing(task, decision, "error", "No spawner", _routing_db)
|
||||
self._record_routing(
|
||||
task, decision, "error", "No spawner", _routing_db)
|
||||
return {
|
||||
"level": level.value,
|
||||
"agent_id": agent_id,
|
||||
@@ -183,9 +191,11 @@ class Dispatcher:
|
||||
|
||||
try:
|
||||
# [v2.7.1] Mail: 标 working 移到 spawn_full_agent 内部(check 通过后、subprocess 前)
|
||||
is_mail = project_config.get("project_id") == "_mail" if project_config else False
|
||||
is_mail = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if is_mail:
|
||||
db_path = Path(project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
db_path = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
|
||||
# on_checks_passed: 所有检查通过后才标 working,检查失败不标
|
||||
on_checks_passed = None
|
||||
@@ -194,6 +204,7 @@ class Dispatcher:
|
||||
_task_id = task.id
|
||||
_mail_db = db_path
|
||||
_disp = self
|
||||
|
||||
def _mail_on_checks_passed():
|
||||
nonlocal _mail_marked_working
|
||||
if not _disp._mail_auto_working(_task_id, _mail_db):
|
||||
@@ -203,8 +214,9 @@ class Dispatcher:
|
||||
|
||||
# 构建 spawn message
|
||||
message = self._build_spawn_message(task, agent_id, project_config,
|
||||
mode=decision.get("mode", ""),
|
||||
spawn_type=action_type or "executor")
|
||||
mode=decision.get(
|
||||
"mode", ""),
|
||||
spawn_type=action_type or "executor")
|
||||
|
||||
# v2.7.2: on_complete 只含业务逻辑,不含 counter.release
|
||||
# counter.release 由 spawn_full_agent 内部的 wrapped_on_complete 保证
|
||||
@@ -218,14 +230,17 @@ class Dispatcher:
|
||||
def _mail_on_complete(aid, outcome):
|
||||
# 幻觉门控:检查是否有回复,自动标 done/failed
|
||||
try:
|
||||
_dispatcher._mail_auto_complete(_task_id, aid, _mail_db, _must_haves, outcome=outcome)
|
||||
_dispatcher._mail_auto_complete(
|
||||
_task_id, aid, _mail_db, _must_haves, outcome=outcome)
|
||||
except Exception as e:
|
||||
logger.error("Mail %s: on_complete error: %s", _task_id, e)
|
||||
logger.error(
|
||||
"Mail %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _mail_on_complete
|
||||
else:
|
||||
# #02: Task 路径也加 on_complete(幻觉门控)
|
||||
_task_id = task.id
|
||||
_task_db = Path(project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
_task_db = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
_dispatcher = self
|
||||
_is_review = action_type == "review"
|
||||
|
||||
@@ -239,10 +254,12 @@ class Dispatcher:
|
||||
try:
|
||||
# #07.2: 统一 crash 回退——executor 和 review 都回退 current_agent
|
||||
if outcome in ROLLBACK_CURRENT_AGENT_OUTCOMES and _task_db:
|
||||
_dispatcher._rollback_current_agent(_task_db, _task_id, aid)
|
||||
_dispatcher._rollback_current_agent(
|
||||
_task_db, _task_id, aid)
|
||||
|
||||
if _is_review:
|
||||
if _task_db and outcome in ("completed", "session_revived"):
|
||||
if _task_db and outcome in (
|
||||
"completed", "session_revived"):
|
||||
# #09: 读 verdict 决定后续动作
|
||||
conn = get_connection(_task_db)
|
||||
try:
|
||||
@@ -254,14 +271,18 @@ class Dispatcher:
|
||||
conn.close()
|
||||
|
||||
if review and review["verdict"] == "approved":
|
||||
_dispatcher._mark_task_status(_task_db, _task_id, "done")
|
||||
logger.info("Task %s: review approved, marking done", _task_id)
|
||||
_dispatcher._mark_task_status(
|
||||
_task_db, _task_id, "done")
|
||||
logger.info(
|
||||
"Task %s: review approved, marking done", _task_id)
|
||||
else:
|
||||
# 非 approved → @mention 被审 agent(assignee,非 current_agent)
|
||||
# 非 approved → @mention 被审
|
||||
# agent(assignee,非 current_agent)
|
||||
verdict_str = review["verdict"] if review else "未知"
|
||||
conn2 = get_connection(_task_db)
|
||||
try:
|
||||
task_row = conn2.execute("SELECT assignee FROM tasks WHERE id=?", (_task_id,)).fetchone()
|
||||
task_row = conn2.execute(
|
||||
"SELECT assignee FROM tasks WHERE id=?", (_task_id,)).fetchone()
|
||||
finally:
|
||||
conn2.close()
|
||||
|
||||
@@ -269,18 +290,21 @@ class Dispatcher:
|
||||
from src.blackboard.blackboard import Blackboard
|
||||
bb = Blackboard(_task_db)
|
||||
bb.add_comment(_task_id, "daemon",
|
||||
f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
||||
comment_type="review")
|
||||
f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
||||
comment_type="review")
|
||||
logger.info("Task %s: review verdict=%s, notified assignee=%s",
|
||||
_task_id, verdict_str, task_row["assignee"] if task_row else "?")
|
||||
# 不标 done,保持 review 状态
|
||||
else:
|
||||
logger.warning("Task %s: review agent %s (%s), NOT marking done", _task_id, aid, outcome)
|
||||
logger.warning(
|
||||
"Task %s: review agent %s (%s), NOT marking done", _task_id, aid, outcome)
|
||||
else:
|
||||
# executor: 三信号验证 → 标 review
|
||||
_dispatcher._task_auto_complete(_task_id, _task_db)
|
||||
_dispatcher._task_auto_complete(
|
||||
_task_id, _task_db)
|
||||
except Exception as e:
|
||||
logger.error("Task %s: on_complete error: %s", _task_id, e)
|
||||
logger.error(
|
||||
"Task %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _task_on_complete
|
||||
|
||||
session_id = await self.spawner.spawn_full_agent(
|
||||
@@ -289,7 +313,8 @@ class Dispatcher:
|
||||
task_id=task.id,
|
||||
on_complete=on_complete,
|
||||
use_main_session=True, # #02: 统一投递到 main session
|
||||
task_db_path=Path(project_config["db_path"]) if project_config and "db_path" in project_config else None,
|
||||
task_db_path=Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None,
|
||||
on_checks_passed=on_checks_passed,
|
||||
)
|
||||
|
||||
@@ -312,9 +337,14 @@ class Dispatcher:
|
||||
else:
|
||||
log_level = logger.debug
|
||||
detail_msg = f"Agent busy: {reason}"
|
||||
log_level("Dispatch skipped %s for task %s: %s", agent_id, task.id, detail_msg)
|
||||
log_level(
|
||||
"Dispatch skipped %s for task %s: %s",
|
||||
agent_id,
|
||||
task.id,
|
||||
detail_msg)
|
||||
# on_checks_passed 未执行(check 失败在它之前),working 未标,无需回退
|
||||
self._record_routing(task, decision, "skipped", detail_msg, _routing_db)
|
||||
self._record_routing(
|
||||
task, decision, "skipped", detail_msg, _routing_db)
|
||||
return {
|
||||
"level": level.value,
|
||||
"agent_id": agent_id,
|
||||
@@ -326,7 +356,8 @@ class Dispatcher:
|
||||
# on_checks_passed 已执行但 subprocess 失败 → 回退 working → pending
|
||||
if _mail_marked_working:
|
||||
self._mail_revert_to_pending(task.id, db_path)
|
||||
self._record_routing(task, decision, "error", str(e), _routing_db)
|
||||
self._record_routing(
|
||||
task, decision, "error", str(e), _routing_db)
|
||||
return {
|
||||
"level": level.value,
|
||||
"agent_id": agent_id,
|
||||
@@ -385,9 +416,16 @@ class Dispatcher:
|
||||
def _build_delegate_prompt(self, task: Task,
|
||||
project_config: Optional[Dict]) -> str:
|
||||
"""构建 delegate 模式的 prompt(协调员分配任务)"""
|
||||
api_host = getattr(self.spawner, 'api_host', '127.0.0.1') if self.spawner else '127.0.0.1'
|
||||
api_port = getattr(self.spawner, 'api_port', 8083) if self.spawner else 8083
|
||||
project_id = project_config.get("project_id", "") if project_config else ""
|
||||
api_host = getattr(
|
||||
self.spawner,
|
||||
'api_host',
|
||||
'127.0.0.1') if self.spawner else '127.0.0.1'
|
||||
api_port = getattr(
|
||||
self.spawner,
|
||||
'api_port',
|
||||
8083) if self.spawner else 8083
|
||||
project_id = project_config.get(
|
||||
"project_id", "") if project_config else ""
|
||||
|
||||
return f"""你是任务协调员。请分析以下任务,决定最合适的执行者并分配。
|
||||
|
||||
@@ -478,7 +516,8 @@ class Dispatcher:
|
||||
|
||||
# ── Legacy 兼容(deprecated) ──
|
||||
|
||||
def _legacy_decide(self, task: Task, action_type: str = "") -> Dict[str, Any]:
|
||||
def _legacy_decide(
|
||||
self, task: Task, action_type: str = "") -> Dict[str, Any]:
|
||||
"""旧版三级决策树(兼容过渡用)"""
|
||||
LOCAL_ACTIONS = frozenset({
|
||||
"L1_guardrail", "format_check",
|
||||
@@ -518,7 +557,8 @@ class Dispatcher:
|
||||
return registered[0]
|
||||
return "pangtong-fujunshi"
|
||||
|
||||
async def _legacy_dispatch(self, task, action_type="", project_config=None):
|
||||
async def _legacy_dispatch(
|
||||
self, task, action_type="", project_config=None):
|
||||
"""旧版 dispatch(兼容过渡用)
|
||||
|
||||
v2.7.2: counter acquire/release 移到 spawn_full_agent 内部。
|
||||
@@ -541,15 +581,19 @@ class Dispatcher:
|
||||
# NOTE: _legacy_dispatch 仅在 router=None 时触发,当前配置不会进入。
|
||||
# Mail 永远走 dispatch() 主路径(on_checks_passed 方案),不走此路径。
|
||||
# 如果未来 legacy 路径被启用,需同步 on_checks_passed 逻辑。
|
||||
is_mail_legacy = project_config.get("project_id") == "_mail" if project_config else False
|
||||
is_mail_legacy = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if is_mail_legacy:
|
||||
db_path_legacy = Path(project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
if not db_path_legacy or not self._mail_auto_working(task.id, db_path_legacy):
|
||||
db_path_legacy = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
if not db_path_legacy or not self._mail_auto_working(
|
||||
task.id, db_path_legacy):
|
||||
return {"level": level.value, "agent_id": agent_id,
|
||||
"session_id": None, "status": "error",
|
||||
"reason": "mail_auto_working_failed"}
|
||||
|
||||
if hasattr(self.spawner, 'build_spawn_message') and project_config:
|
||||
if hasattr(self.spawner,
|
||||
'build_spawn_message') and project_config:
|
||||
retry_ctx = self._build_retry_context(task)
|
||||
message = self.spawner.build_spawn_message(
|
||||
task_id=task.id, title=task.title,
|
||||
@@ -576,9 +620,11 @@ class Dispatcher:
|
||||
|
||||
def _mail_oc_legacy(aid, outcome):
|
||||
try:
|
||||
_disp._mail_auto_complete(_t_id, aid, _m_db, _m_mh, outcome=outcome)
|
||||
_disp._mail_auto_complete(
|
||||
_t_id, aid, _m_db, _m_mh, outcome=outcome)
|
||||
except Exception as e:
|
||||
logger.error("Mail %s: legacy on_complete error: %s", _t_id, e)
|
||||
logger.error(
|
||||
"Mail %s: legacy on_complete error: %s", _t_id, e)
|
||||
on_complete_legacy = _mail_oc_legacy
|
||||
|
||||
session_id = await self.spawner.spawn_full_agent(
|
||||
@@ -586,14 +632,16 @@ class Dispatcher:
|
||||
task_id=task.id,
|
||||
on_complete=on_complete_legacy,
|
||||
use_main_session=True, # #02: 统一投递到 main session
|
||||
task_db_path=Path(project_config["db_path"]) if project_config and "db_path" in project_config else None,
|
||||
task_db_path=Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None,
|
||||
)
|
||||
return {"level": level.value, "agent_id": agent_id,
|
||||
"session_id": session_id, "status": "dispatched",
|
||||
"reason": decision["reason"]}
|
||||
except AgentBusyError as e:
|
||||
reason = getattr(e, 'reason', 'busy')
|
||||
detail_msg = f"Session busy: {reason}" if reason.startswith("session_") else f"Agent busy: {reason}"
|
||||
detail_msg = f"Session busy: {reason}" if reason.startswith(
|
||||
"session_") else f"Agent busy: {reason}"
|
||||
return {"level": level.value, "agent_id": agent_id,
|
||||
"session_id": None, "status": "skipped",
|
||||
"reason": detail_msg}
|
||||
@@ -618,9 +666,11 @@ class Dispatcher:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute("SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
if not row:
|
||||
logger.warning("Mail %s: cannot mark working (task not found)", task_id)
|
||||
logger.warning(
|
||||
"Mail %s: cannot mark working (task not found)", task_id)
|
||||
return False
|
||||
if row["status"] not in ("pending", "claimed"):
|
||||
logger.warning("Mail %s: cannot mark working (status=%s, expected pending/claimed)",
|
||||
@@ -631,7 +681,10 @@ class Dispatcher:
|
||||
(task_id,),
|
||||
)
|
||||
conn.commit()
|
||||
logger.info("Mail %s: auto-marked working (system, was %s)", task_id, row["status"])
|
||||
logger.info(
|
||||
"Mail %s: auto-marked working (system, was %s)",
|
||||
task_id,
|
||||
row["status"])
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -645,30 +698,40 @@ class Dispatcher:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute("SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
if row and row["status"] == "working":
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status='pending', updated_at=datetime('now') WHERE id=?",
|
||||
(task_id,),
|
||||
)
|
||||
conn.commit()
|
||||
logger.info("Mail %s: reverted working → pending (spawn failed)", task_id)
|
||||
logger.info(
|
||||
"Mail %s: reverted working → pending (spawn failed)", task_id)
|
||||
else:
|
||||
logger.debug("Mail %s: skip revert (status=%s, expected working)", task_id, row["status"] if row else "not_found")
|
||||
logger.debug(
|
||||
"Mail %s: skip revert (status=%s, expected working)",
|
||||
task_id,
|
||||
row["status"] if row else "not_found")
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error("Mail %s: failed to revert to pending: %s", task_id, e)
|
||||
logger.error(
|
||||
"Mail %s: failed to revert to pending: %s",
|
||||
task_id,
|
||||
e)
|
||||
|
||||
def _mail_auto_complete(self, task_id: str, agent_id: str,
|
||||
db_path: Path, must_haves: str, outcome=None) -> None:
|
||||
db_path: Path, must_haves: str, outcome=None) -> None:
|
||||
"""Mail 任务:on_complete 后自动标 done/failed(含幻觉门控)"""
|
||||
try:
|
||||
# 解析 performative
|
||||
performative = "request"
|
||||
try:
|
||||
meta = json.loads(must_haves) if must_haves else {}
|
||||
performative = meta.get("performative", meta.get("type", "request"))
|
||||
performative = meta.get(
|
||||
"performative", meta.get(
|
||||
"type", "request"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -677,13 +740,15 @@ class Dispatcher:
|
||||
has_reply = self._mail_check_reply(task_id, db_path)
|
||||
if not has_reply:
|
||||
# F3: 立刻标 failed(不等 ticker 30 分钟)
|
||||
logger.error("Mail %s: no reply found, marking failed (no_reply_found)", task_id)
|
||||
logger.error(
|
||||
"Mail %s: no reply found, marking failed (no_reply_found)", task_id)
|
||||
for attempt in range(3):
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute("SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
if not row:
|
||||
return
|
||||
if row["status"] == "working":
|
||||
@@ -697,19 +762,24 @@ class Dispatcher:
|
||||
json.dumps({"reason": "no_reply_found"}, ensure_ascii=False)),
|
||||
)
|
||||
conn.commit()
|
||||
logger.info("Mail %s: marked failed (no_reply_found)", task_id)
|
||||
logger.info(
|
||||
"Mail %s: marked failed (no_reply_found)", task_id)
|
||||
# Mail 失败通知:通知发件人
|
||||
try:
|
||||
from src.daemon.mail_notify import notify_mail_failed
|
||||
notify_mail_failed(db_path, task_id, "no_reply_found")
|
||||
notify_mail_failed(
|
||||
db_path, task_id, "no_reply_found")
|
||||
except Exception as ne:
|
||||
logger.warning("Mail %s: failed to send no_reply_found notification: %s", task_id, ne)
|
||||
logger.warning(
|
||||
"Mail %s: failed to send no_reply_found notification: %s", task_id, ne)
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.warning("Mail %s: failed attempt %d: %s", task_id, attempt + 1, e)
|
||||
logger.error("Mail %s: all 3 failed attempts failed, leaving for ticker", task_id)
|
||||
logger.warning(
|
||||
"Mail %s: failed attempt %d: %s", task_id, attempt + 1, e)
|
||||
logger.error(
|
||||
"Mail %s: all 3 failed attempts failed, leaving for ticker", task_id)
|
||||
return
|
||||
|
||||
# inform 类型:只对成功 outcome 标 done,失败 outcome 留 working 等 ticker 重投
|
||||
@@ -717,7 +787,10 @@ class Dispatcher:
|
||||
if performative == "inform":
|
||||
INFORM_DONE_OUTCOMES = {"completed", "claimed", "no_reply"}
|
||||
if outcome not in INFORM_DONE_OUTCOMES:
|
||||
logger.info("Mail %s: inform outcome=%s, skip auto-done", task_id, outcome)
|
||||
logger.info(
|
||||
"Mail %s: inform outcome=%s, skip auto-done",
|
||||
task_id,
|
||||
outcome)
|
||||
return
|
||||
|
||||
# 标 done(重试 3 次)
|
||||
@@ -726,7 +799,8 @@ class Dispatcher:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute("SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
if not row:
|
||||
return
|
||||
if row["status"] == "working":
|
||||
@@ -741,9 +815,15 @@ class Dispatcher:
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.warning("Mail %s: done attempt %d failed: %s", task_id, attempt + 1, e)
|
||||
logger.warning(
|
||||
"Mail %s: done attempt %d failed: %s",
|
||||
task_id,
|
||||
attempt + 1,
|
||||
e)
|
||||
# 3 次都失败,留 working 等 ticker 超时兜底
|
||||
logger.error("Mail %s: all 3 done attempts failed, leaving for ticker", task_id)
|
||||
logger.error(
|
||||
"Mail %s: all 3 done attempts failed, leaving for ticker",
|
||||
task_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Mail %s: auto-complete error: %s", task_id, e)
|
||||
@@ -788,7 +868,9 @@ class Dispatcher:
|
||||
logger.info("Task %s: verify passed, marking review", task_id)
|
||||
self._mark_task_status(db_path, task_id, "review")
|
||||
else:
|
||||
logger.info("Task %s: verify not passed (no signal), leaving working", task_id)
|
||||
logger.info(
|
||||
"Task %s: verify not passed (no signal), leaving working",
|
||||
task_id)
|
||||
except Exception as e:
|
||||
logger.error("Task %s: auto-complete error: %s", task_id, e)
|
||||
|
||||
@@ -823,7 +905,8 @@ class Dispatcher:
|
||||
logger.error("Task %s: verify error: %s", task_id, e)
|
||||
return True
|
||||
|
||||
def _rollback_current_agent(self, db_path: Path, task_id: str, agent_id: str) -> None:
|
||||
def _rollback_current_agent(
|
||||
self, db_path: Path, task_id: str, agent_id: str) -> None:
|
||||
"""#07.2: crash 后回退 current_agent 到 assignee,避免 exclude_current 卡死"""
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
@@ -837,11 +920,18 @@ class Dispatcher:
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
logger.info("Task %s: rolled back current_agent from %s to assignee", task_id, agent_id)
|
||||
logger.info(
|
||||
"Task %s: rolled back current_agent from %s to assignee",
|
||||
task_id,
|
||||
agent_id)
|
||||
except Exception as e:
|
||||
logger.warning("Task %s: failed to rollback current_agent: %s", task_id, e)
|
||||
logger.warning(
|
||||
"Task %s: failed to rollback current_agent: %s",
|
||||
task_id,
|
||||
e)
|
||||
|
||||
def _mark_task_status(self, db_path: Path, task_id: str, status: str) -> None:
|
||||
def _mark_task_status(self, db_path: Path,
|
||||
task_id: str, status: str) -> None:
|
||||
"""更新任务状态 + 写审计事件"""
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
@@ -857,7 +947,8 @@ class Dispatcher:
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO events (task_id, agent, event_type, payload) VALUES (?, 'dispatcher', 'status_change', ?)",
|
||||
(task_id, f'{{"from": "{old_status}", "to": "{status}", "source": "auto_complete"}}'),
|
||||
(task_id,
|
||||
f'{{"from": "{old_status}", "to": "{status}", "source": "auto_complete"}}'),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
@@ -866,7 +957,7 @@ class Dispatcher:
|
||||
logger.error("Task %s: mark status error: %s", task_id, e)
|
||||
|
||||
@staticmethod
|
||||
def _check_crash_limit(task_id: str, db_path: pathlib.Path, limit: int = 3,
|
||||
def _check_crash_limit(task_id: str, db_path: Path, limit: int = 3,
|
||||
window_minutes: int = 30) -> bool:
|
||||
"""v2.8.1 Fix-3c: 检查 task 最近 window_minutes 内的 crash 次数是否超限。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user