From d6cb854f68089a24782bfcbe67b4c70804e9f18a Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 15 Jun 2026 09:48:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20mention=20=E9=87=8D=E5=A4=8D=E6=8A=95?= =?UTF-8?q?=E9=80=92=20+=20mail=20=E5=A4=B1=E8=B4=A5=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=AB=9E=E6=80=81=E4=BF=9D=E6=8A=A4=20+=20=C2=A714=20=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: spawn_full_agent use_main_session 返回 None 导致 mention 重复投递 - 根因: use_main_session=True 时 session_id=None, return None 被 ticker _process_posts 误判为 spawn 失败, 每次 tick 都重试 - 修复: 引入 effective_sid = session_id or 'main', 统一用于 _register_session / _monitor_process / return value Bug 2: _mark_task failed 时未检查已完成状态导致误发投递失败通知 - 根因: spawner 标 failed 和 handler 标 done 竞态条件下, 已完成的 mail task 被误发投递失败通知 - 修复: notify_mail_failed 调用前加防御性检查, 若 task 已 done 则跳过 设计文档: §13 三个 handler sections 列表同步 DeliveryChecklistSection 及 GiteaConventionSection / WikiGuideSection, 更新 section 复用分析表 及文件结构 section 计数 --- docs/design/14-task-type-architecture.md | 39 ++++++++++++++++++++++-- src/daemon/spawner.py | 26 +++++++++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/design/14-task-type-architecture.md b/docs/design/14-task-type-architecture.md index 7ca28c7..f47398b 100644 --- a/docs/design/14-task-type-architecture.md +++ b/docs/design/14-task-type-architecture.md @@ -585,6 +585,18 @@ class PromptComposer: | 50-59 | 硬约束 | 安全红线、禁止行为 | | 60-69 | 扩展段 | 保留给未来使用 | +## 共性 Section(三 handler 共享) + +以下三个 Section 在 `prompt_composer.py` 中统一定义,被 Task/Mail/Toolchain 三个 handler 共同注入: + +| Section | priority | 用途 | +|---------|----------|------| +| `GiteaConventionSection` | 55 | Gitea Issue/PR 标题规范、分支命名、提交格式 | +| `DeliveryChecklistSection` | 55 | 交付前检查清单(产出格式、验证项、必读文档) | +| `WikiGuideSection` | 60 | Wiki 知识库检索指引(检索路径、优先级、知识缺口记录) | + +设计意图:将跨 handler 的共性约束从各 handler 的 ConstraintsSection 中抽离,避免重复维护。 + --- # §13 三个 Handler 的 Section 注册 @@ -601,6 +613,9 @@ def get_sections(self) -> list[PromptSection]: RoleSkillSection(priority=30), # BootstrapBuilder 段 3(Skill 全文) TaskApiSection(priority=40), # API 操作指令,success_status="review" TaskConstraintsSection(priority=50), # 硬约束 + GiteaConventionSection(priority=55), # Gitea 协作规范(共性) + WikiGuideSection(priority=60), # Wiki 知识库检索指引(共性) + DeliveryChecklistSection(priority=55), # 交付检查清单(共性) ] ``` @@ -611,6 +626,9 @@ def get_sections(self) -> list[PromptSection]: | RoleSkillSection | BootstrapBuilder 段 3 | 个性:只有 task 读 Skill 全文 | | TaskApiSection | spawner `_build_api_section` | **共性基础 + 个性参数**(success_status) | | TaskConstraintsSection | BootstrapBuilder 段 4 | 个性:每种 task 约束不同 | +| GiteaConventionSection | prompt_composer.py | **共性**:Gitea Issue/PR 规范 | +| WikiGuideSection | prompt_composer.py | **共性**:Wiki 检索指引 | +| DeliveryChecklistSection | prompt_composer.py | **共性**:交付前检查清单 | ## MailHandler sections @@ -620,6 +638,9 @@ def get_sections(self) -> list[PromptSection]: MailContextSection(priority=10), # from/to/title/text,区分 inform/request MailApiSection(priority=40), # API 操作指令,success_status="done" MailConstraintsSection(priority=50), # 硬约束(禁止状态转换命令等) + GiteaConventionSection(priority=55), # Gitea 协作规范(共性) + WikiGuideSection(priority=60), # Wiki 知识库检索指引(共性) + DeliveryChecklistSection(priority=55), # 交付检查清单(共性) ] ``` @@ -628,6 +649,9 @@ def get_sections(self) -> list[PromptSection]: | MailContextSection | MAIL_INFORM_TEMPLATE / MAIL_REQUEST_TEMPLATE | 个性:邮件格式 | | MailApiSection | spawner `_build_api_section` 变体 | **共性基础 + 个性参数**(success_status="done",含 Mail API 指令) | | MailConstraintsSection | 模板中的 ⚠️ 约束 | 个性 | +| GiteaConventionSection | prompt_composer.py | **共性**:Gitea Issue/PR 规范 | +| WikiGuideSection | prompt_composer.py | **共性**:Wiki 检索指引 | +| DeliveryChecklistSection | prompt_composer.py | **共性**:交付前检查清单 | ## ToolchainHandler sections @@ -637,6 +661,9 @@ def get_sections(self) -> list[PromptSection]: ToolchainContextSection(priority=10), # 事件类型 + 事件详情 ToolchainApiSection(priority=40), # API 操作指令,success_status="done" ToolchainConstraintsSection(priority=50), # 硬约束 + GiteaConventionSection(priority=55), # Gitea 协作规范(共性) + WikiGuideSection(priority=60), # Wiki 知识库检索指引(共性) + DeliveryChecklistSection(priority=55), # 交付检查清单(共性) ] ``` @@ -645,6 +672,9 @@ def get_sections(self) -> list[PromptSection]: | ToolchainContextSection | toolchain_templates.py + md 文件 | 个性:事件格式 | | ToolchainApiSection | spawner `_build_api_section` 变体 | **共性基础 + 个性参数** | | ToolchainConstraintsSection | 新增 | 个性 | +| GiteaConventionSection | prompt_composer.py | **共性**:Gitea Issue/PR 规范 | +| WikiGuideSection | prompt_composer.py | **共性**:Wiki 检索指引 | +| DeliveryChecklistSection | prompt_composer.py | **共性**:交付前检查清单 | ## Section 复用分析 @@ -655,6 +685,9 @@ def get_sections(self) -> list[PromptSection]: | *ConstraintsSection | ✅ | ✅ | ✅ | ❌ 约束内容不同,各自实现 | | PriorOutputsSection | ✅ | ❌ | ❌ | 仅 task | | RoleSkillSection | ✅ | ❌ | ❌ | 仅 task | +| GiteaConventionSection | ✅ | ✅ | ✅ | **共性**:三 handler 共享,prompt_composer.py 定义 | +| WikiGuideSection | ✅ | ✅ | ✅ | **共性**:三 handler 共享,prompt_composer.py 定义 | +| DeliveryChecklistSection | ✅ | ✅ | ✅ | **共性**:三 handler 共享,prompt_composer.py 定义 | **结论**:ApiSection 可以抽一个 BaseApiSection(curl 模板 + success_status 参数),其余 section 各自实现。 @@ -667,9 +700,9 @@ src/daemon/ ├── task_type_registry.py # §3 + §4:Protocol + Registry ├── prompt_composer.py # §12 PromptSection + PromptContext + PromptComposer ├── base_task_handler.py # §16 BaseTaskHandler 基类 -├── task_handler.py # §13 TaskHandler(继承 BaseTaskHandler)+ 5 sections -├── mail_handler.py # §13 MailHandler(继承 BaseTaskHandler)+ 3 sections -├── toolchain_handler.py # §13 ToolchainHandler(继承 BaseTaskHandler)+ 3 sections +├── task_handler.py # §13 TaskHandler(继承 BaseTaskHandler)+ 8 sections +├── mail_handler.py # §13 MailHandler(继承 BaseTaskHandler)+ 6 sections +├── toolchain_handler.py # §13 ToolchainHandler(继承 BaseTaskHandler)+ 6 sections ├── dispatcher.py # §6 改动 ├── spawner.py # §6 改动 ├── ticker.py # §6 改动 diff --git a/src/daemon/spawner.py b/src/daemon/spawner.py index 28451bb..ec6bcce 100644 --- a/src/daemon/spawner.py +++ b/src/daemon/spawner.py @@ -625,19 +625,24 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) - self._register_session(session_id, agent_id, task_id, proc.pid, + # use_main_session=True 时 session_id 为 None,但 _register_session 和 + # _monitor_process 需要一个非 None 的 key;同时 ticker 等调用方用 + # `result is not None` 判断 spawn 是否成功,返回 None 会被误判为失败。 + # 统一用 "main" 作为占位标识。 + effective_sid = session_id or "main" + self._register_session(effective_sid, agent_id, task_id, proc.pid, broadcast_task_ids=broadcast_task_ids) logger.info("Spawned agent %s (session=%s, pid=%d)", - agent_id, session_id, proc.pid) + agent_id, effective_sid, proc.pid) # Schedule monitor(传 wrapped_on_complete) asyncio.create_task( - self._monitor_process(session_id, proc, agent_id, task_id, + self._monitor_process(effective_sid, proc, agent_id, task_id, on_complete=_wrapped_on_complete, db_path=task_db_path or self.db_path) ) - return session_id + return effective_sid except Exception as e: # spawn 失败也要 release counter @@ -1949,6 +1954,19 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ try: from src.daemon.mail_notify import _is_mail_project, notify_mail_failed if _is_mail_project(db_path): + # 防御性检查:如果 task 已经 done,不触发失败通知(竞态保护) + # 场景:spawner 标 failed 和 handler 标 done 同时发生 + try: + conn2 = get_connection(db_path) + current_status = conn2.execute( + "SELECT status FROM tasks WHERE id=?", (task_id,) + ).fetchone() + conn2.close() + if current_status and current_status["status"] == "done": + logger.info("Task %s already done, skipping mail failure notification", task_id) + return + except Exception: + pass # Mail 失败:通知发件人,不 @pangtong notify_mail_failed(db_path, task_id, reason, detail) else: -- 2.45.4