# 02-main-session-delegation.md — Main Session + Delegation 架构 **日期**: 2026-05-30 **作者**: 庞统 **状态**: 已修订 v1.1(根据司马懿 2026-05-30 评审意见) **前置**: `01-four-phase-loop.md`(四相循环 E2E 验证暴露 session 爆炸问题) --- ## 一、问题:现行 spawn 方案的 session 爆炸 ### 现状 spawner 通过 `openclaw agent --session-id UUID --message "..." --json` spawn Agent: ``` spawner → 新 UUID session → Agent 执行 → 进程退出 → session 残留 ``` ### 问题 | 问题 | 证据 | 影响 | |------|------|------| | **Session 爆炸** | 今天 E2E 测试 6 个 Agent 产生 68 个 session(16~484 每个),其中 56 个 `gateway-fallback-*` | 文件系统堆积、磁盘浪费 | | **空 payloads** | 绝大部分 spawn 返回 `status: None, payloads: []` | Daemon 以为任务完成了但实际没有产出 | | **上下文丢失** | 每次 spawn 新 session,Agent 无历史、无 workspace bootstrap | Agent 不知道自己是谁、不知道之前的决策 | | **续杯浪费** | 续杯时 `reuse_session_id` 复用 UUID,但 Gateway 可能又创建 fallback | round_count 空转、Agent busy 但不产出 | ### 根因 `openclaw agent --session-id UUID` 的语义是"在指定 session 里执行 turn",但 Gateway 的实际行为是 find-or-create: - session 不存在 → 创建 `gateway-fallback-` session - 每次都是全新上下文 - Agent 没有历史,没有 SOUL.md 等身份注入 **这是结构性问题,不是 bug。** 在这个方案上继续修 bug 没有意义。 --- ## 二、方案:Main Session + Delegation ### 核心思路 ``` 现行:Daemon 硬编码步骤 → spawner 直接 spawn 新 session → Agent 被动执行 改为:Daemon 投递任务 → Agent main session 收到 → Agent 自主分析 → sessions_spawn(subagent) 执行 → sub 完成 → Agent review → 写回黑板 ``` ### 架构对比 ``` ┌─ 现行 ──────────────────────────────────────────┐ │ │ │ Daemon/Ticker │ │ ├── broadcast → spawn_full_agent(UUID session) │ │ │ → Agent 执行固定步骤 prompt │ │ │ → 空 payloads / session 残留 │ │ ├── mention → spawn_full_agent(UUID session) │ │ └── review → spawn_full_agent(UUID session) │ │ │ └──────────────────────────────────────────────────┘ ┌─ 改为 ───────────────────────────────────────────┐ │ │ │ Daemon/Ticker │ │ ├── broadcast → 投递到 Agent main session │ │ ├── mention → 投递到 Agent main session │ │ └── review → 投递到庞统 main session │ │ │ │ Agent Main Session(有完整身份+上下文) │ │ ├── 收到任务消息 │ │ ├── 分析黑板(API 读任务详情/上下文/依赖) │ │ ├── sessions_spawn(subagent, cleanup: "delete") │ │ │ └── sub 执行具体工作 → 完成后自动清理 │ │ ├── sessions_yield → 等待 sub 完成 │ │ ├── review sub 结果 │ │ └── 写回黑板(产出/状态/评论) │ │ │ └───────────────────────────────────────────────────┘ ``` ### 三条路径的变化 | 路径 | 现行 | 改为 | |------|------|------| | **Broadcast dispatch** | `spawn_full_agent(session_id=UUID)` 新 session | `spawn_full_agent(use_main_session=True)` main session | | **Mention** | `spawn_full_agent(session_id=UUID)` 新 session | `spawn_full_agent(use_main_session=True)` main session | | **Review**(庞统) | `spawn_full_agent(session_id=UUID)` 新 session | `spawn_full_agent(use_main_session=True)` main session | --- ## 三、关键机制 ### 3.1 投递到 Main Session **方式**:`spawn_full_agent(use_main_session=True)`(不传 `--session-id`) 底层命令行:`openclaw agent --agent --message "..." --json --timeout N` Gateway 行为(已确认): - 不传 `--session-id` → 投递到 Agent 的 main session(`agent::main`) - main session 正忙 → 消息排队,Agent 当前 turn 完成后处理 - Agent 有完整上下文(SOUL.md / MEMORY.md / workspace bootstrap) **调用链路**: ``` spawner.spawn_full_agent(use_main_session=True) → session_id = None(不传 --session-id) → counter.acquire(agent_id, "main") → asyncio.create_subprocess_exec("openclaw", "agent", "--agent", "xxx", ...) → asyncio.create_task(_monitor_process(...)) # 异步不阻塞 → 返回 "main" _monitor_process() → await proc.wait() + 读 stdout # 异步等子进程退出 → 子进程退出 → _handle_exit() → 解析 --json 输出 → on_complete 回调(幻觉门控、标状态等) → counter.release(agent_id, "main") ``` **关键:`openclaw agent` 子进程内部是同步阻塞的**——它会同步等 Gateway 投递消息到 main session → Agent 执行 turn → 回复 → 子进程退出。所以 `on_complete` 只在 Agent 真正完成 turn 后才触发。 ### 3.2 Delegation(subagent-delegation skill) Agent 收到任务后,用 `sessions_spawn` 创建 subagent 执行具体工作: ```javascript sessions_spawn({ task: "任务目标\n\n上下文信息", cleanup: "delete", // 完成后自动清理 session model: "zhipu/glm-5.1", label: "T1-01" }) ``` **关键特性**: - `cleanup: "delete"` → sub 执行完 session 立即删除(transcript 保留可查) - main session 通过 `sessions_yield` 等待 sub 完成(**非阻塞**,可被新消息打断,Gateway 还能投递新消息) - sub 的结果通过完成事件回传给 main session - subagent 是 Agent 自己 spawn 的,**不经过 Daemon counter** ### 3.3 续杯机制 改用 main session 后,续杯逻辑简化: | 维度 | 现行 | 改为 | |------|------|------| | 续杯 session | `reuse_session_id=UUID` 复用原 session | `use_main_session=True` 重新投递到 main session | | 上下文 | 新 session 无历史 | main session 有完整历史 | | counter | acquire 新 session key | acquire 同一个 "main" key | | 失败处理 | 重试时可能创建新的 fallback session | 直接重投 main session,无 fallback 问题 | **改动**:`_do_retry` 中把 `reuse_session_id` 改为 `use_main_session=True`,移除 `reuse_session_id` 参数。 ### 3.4 Counter(无需改动) 改用 main session 后 counter 逻辑不变: - 所有 spawn 走同一个 session key(`"main"`) - `max_per_session=1` 保证同一时间只有一个 active turn - `max_concurrent_sessions=3` 限制同一 Agent 最多 3 个不同 session(main + 可能的 sub) - `max_global=5` 限制全局并发 **注意**:subagent 是 Agent 自己 spawn 的,不经过 Daemon counter。Daemon 只控制 main session 的投递。 --- ## 四、Prompt 设计 ### 4.1 投递消息格式 Daemon 投递到 main session 的消息包含足够的上下文,让 Agent 自主决策: ```markdown 📋 新任务到达 **任务**: {title} **描述**: {description} **Project**: {project_id} **Task ID**: {task_id} **类型**: {task_type} | **风险**: {risk_level} **验收标准**: {must_haves} **依赖产出**: {depends_on_outputs_summary} **黑板 API**: http://localhost:8083/api/projects/{project_id} - 读任务详情: GET /api/projects/{project_id}/tasks/{task_id}?expand=all - 写产出: POST /api/projects/{project_id}/tasks/{task_id}/outputs - 写评论: POST /api/projects/{project_id}/tasks/{task_id}/comments - 更新状态: POST /api/projects/{project_id}/tasks/{task_id}/status **约束**: - 完成后必须写产出 + 标 review - 失败了标 failed 并写明原因 - 优先使用 subagent-delegation skill 进行具体执行 - 禁止使用 sessions_send 直接发消息 ``` ### 4.2 Agent 自主决策 Agent 收到消息后的自主行为(不是硬编码步骤): 1. **分析上下文**:读黑板 API 获取任务详情、依赖、历史讨论 2. **决定执行策略**: - 简单任务 → 自己直接做 - 复杂任务 → `sessions_spawn` 拆分 sub task - 需要协作 → @mention 其他 Agent 3. **执行**:通过 subagent 或直接执行 4. **Review**:检查 sub 结果质量 5. **写回**:产出 + 状态 + handoff comment(handoff comment 必须 ≥ 50 字符,用于幻觉门控第三信号检测) ### 4.3 消息优先级与中断策略 Agent main session 可能同时收到多种消息(新 task / mention / review),按以下优先级处理: **优先级顺序**(Prompt 中明确告知 Agent): 1. 🔍 **Review 消息**(庞统的打回重做 / GOAL_ACHIEVED)— 最高优先级,先处理再继续当前工作 2. 💬 **Mention 消息**(其他 Agent 的协作请求)— 高优先级,完成当前 sub task 后立即处理 3. 📋 **新任务消息**(ticker 投递的广播任务)— 普通优先级,排队处理 **中断策略**(Prompt 中明确告知 Agent): - 收到 review 消息时 → 暂停当前 sub task,先完成 review,再决定是否继续 - 收到 mention 消息时 → 完成当前 sub task(如果 sub 正在执行),然后处理 mention - 收到新任务消息时 → 排队等待,当前任务完成后再处理 - 任何时候一个任务完成后,检查是否有更高优先级消息排队 **队列管理**: - Agent 自己维护任务队列(在对话中记忆),不需要 Daemon 管理 - 每一步开始前先检查新消息,如果有高优先级消息就调整顺序 - 完成当前任务后,从队列取下一个 ### 4.4 Subagent 背压控制 Agent 同时 spawn 的 subagent 数量有硬性上限: | 参数 | 值 | |------|-----| | 单轮同时 sub 上限 | 3 | | 单任务累计 sub 上限 | 8 | 超限时 subagent-delegation skill 自动拒绝,并在 prompt 中提醒 Agent:"已超出 subagent 数量限制,请等待现有 sub 完成后再创建新 sub。" 在 subagent-delegation skill 中通过计数器维护(Daemon 不参与 subagent 计数)。 ### 4.5 庞统 Review 消息格式 ```markdown 🔍 一轮结束,需要你的 review **Parent Task**: {title}(ID: {task_id}) **Project**: {project_id} **Round**: {round_num}/{max_rounds} **本轮状态**: - 完成: {done} | 失败: {failed} | 取消: {cancelled} - 总计: {total} **三问**: 1. Goal 还清晰吗?(是否有 goal drift) 2. 成果物覆盖 goal 了吗?(逐条检查验收标准) 3. 下一轮需要做什么?(创建新 sub tasks / 标记完成 / 调整方向) **你可以**: - 创建新 sub task: curl -X POST http://localhost:8083/api/projects/{project_id}/tasks ... - 调整 goal - 标记 GOAL_ACHIEVED 回复 GOAL_ACHIEVED 表示完成,否则系统会等待你创建新 sub tasks。 ``` --- ## 五、Daemon 侧改动 ### 5.1 改动范围 **不需要新增方法**。直接复用 `spawn_full_agent(use_main_session=True)`,Mail 路径已验证此模式。 改动集中在调用侧(把参数改对)和回调侧(把完成检测从邮件改为黑板): | 文件 | 改动点 | 改动量 | |------|--------|-------| | `ticker.py` | 3 处 `spawn_full_agent` 调用加 `use_main_session=True`;移除 `new_session=True` | ~15 行 | | `dispatcher.py` | 确定性路由改 `use_main_session=True`;新增 `_task_auto_complete` | ~40 行 | | `spawner.py` | `_do_retry` 续杯改 `use_main_session=True`;标注 `new_session`/`reuse_session_id` 废弃 | ~10 行 | | `ticker.py` | 新增 `_task_verify_completion`(第一层幻觉门控);review 前跑 `_verify_output_files`(第二层) | ~50 行 | **总改动量**:~115 行,涉及 3 个文件。不涉及 DB schema 变更、不涉及前端。 ### 5.2 Ticker 改动 | 方法 | 改动 | |------|------| | `_broadcast_and_dispatch` | `spawn_full_agent(..., use_main_session=True)` 替代当前的默认参数 | | `_process_mentions` | 同上 | | `_spawn_pangtong_review` | 同上 | | `_check_round_complete` | 保留 `reviewing` 中间态逻辑(SYS-BUG-01 修复) | | `_handle_review_conclusion` | 保留,从 `on_complete` 回调中的黑板状态变化触发 | | 新增 `_task_verify_completion` | 第一层幻觉门控(三信号检查) | | 新增 `_verify_output_files` | 第二层幻觉门控(文件存在性检查) | ### 5.3 Dispatcher 改动 - 确定性路由改 `use_main_session=True` - 新增 `_task_auto_complete`(复用 Mail 的 `_mail_auto_complete` 模式) - `new_session` 参数标注废弃 ### 5.4 废弃代码 - `spawn_full_agent` 的 `new_session` 参数 → 标注废弃(保留接口兼容,变更前 `grep -rn 'new_session'` 确认零调用方后标废弃) - `reuse_session_id` 参数 → 标注废弃(续杯改用 `use_main_session=True`;变更前 `grep -rn 'reuse_session_id'` 确认零调用方) - `gateway-fallback-*` session 的产生路径 → 自然消失(不再传 `--session-id UUID`) ### 5.5 `on_complete` 回调路由 `on_complete` 回调中,根据 `task_type` 分支选择检测逻辑: ```python def _handle_completion(self, task, db_path): if task["type"] == "mail": return self._mail_auto_complete(task, db_path) else: return self._task_auto_complete(task, db_path) ``` `_task_auto_complete` 检查黑板三信号(status/outputs/comments),`_mail_auto_complete` 检查是否有回复邮件。两者共享超时兜底逻辑(ticker 轮询 working 状态)。 --- ## 六、Session 生命周期对比 ### 现行 ``` 每次 spawn → 创建新 session → 执行 → 残留 → 手动清理 ↓ gateway-fallback-* 堆积 ``` ### 改为 ``` main session(常驻,有完整上下文) ├── 收到任务 A → spawn sub(A1, cleanup: delete) → yield → review → 写回 ├── 收到任务 B → spawn sub(B1, cleanup: delete) → yield → review → 写回 └── 收到 mention → 自主处理 → 写回 sub session(临时,完成即删) ├── sub(A1) → 执行 → 完成 → session 删除(transcript 保留) └── sub(B1) → 执行 → 完成 → session 删除(transcript 保留) ``` **Session 数量**:6 个 main session(常驻)+ 临时 sub session(用完即删) **对比现行**:6 个 main + N 个 `gateway-fallback-*`(无限增长) --- ## 七、收益与风险 ### 收益 | 维度 | 改善 | |------|------| | **Session 数量** | 从无限增长 → 6 个 main + 临时 sub | | **Agent 上下文** | 从无历史 → 完整身份 + 记忆 + workspace | | **代码简化** | `new_session` / `reuse_session_id` 不再有调用方 | | **Prompt 灵活度** | 从硬编码步骤 → Agent 自主决策 | | **空 payloads** | 消除(main session 有完整生命周期管理) | | **续杯可靠性** | 不再创建 fallback session,直接重投 main session | ### 风险与缓解 | 风险 | 缓解 | |------|------| | **main session 上下文膨胀** | OpenClaw 自带的 compact 机制处理;sub session 不膨胀(用完删) | | **main session 被阻塞** | `sessions_yield` 非阻塞,可被新消息打断;Gateway 排队机制保证不丢消息 | | **sub 质量不可控** | main session review sub 结果;reviewing 中间态保证 round 不重复触发 | | **Prompt 依赖** | 投递消息中明确告诉 Agent "优先使用 subagent-delegation skill";skill 已调优 | | **main session 排队延迟** | Daemon 投递后 `on_complete` 异步等结果;ticker 轮询黑板状态兜底 | | **main session prompt 污染** | 每次投递消息中明确标注当前任务 ID;Agent 每步开始前确认当前处理的任务;compact 机制清除旧消息 | | **sub 失败传导** | 投递 prompt 中明确告诉 Agent:sub 失败时不要标 done,先分析失败原因;可重试 ≤ 2 次,然后标 failed 并写原因到 comment | | **openclaw agent 子进程超时** | `--timeout` 超时后子进程退出,但 main session 中的 Agent turn 可能还在进行。Daemon 遇到超时:不立即标 failed,留 working 等 ticker 下一次轮询检查 | --- ## 八、Mail 路径已验证的完整模式(可直接复用) Mail 路径(`dispatcher.py` `_mail_on_checks_passed` + `_mail_auto_complete`)已经完整实现了 main session + 完成检测的全链路: ``` openclaw agent --agent xxx(不传 --session-id)→ Gateway 投递到 main session → Agent 处理 → 子进程退出 → _monitor_process → _handle_exit → on_complete 回调 → _mail_auto_complete → 幻觉门控:检查黑板是否有回复 → 有回复 → 标 done → 无回复 → 留 working,等 ticker 超时兜底再查 → counter.release(agent_id, "main") ``` ### 已验证的机制 | 机制 | Mail 实现 | 普通 Task 复用 | |------|----------|--------------| | Main session 投递 | `use_main_session=True` | ✅ 直接复用 | | `on_complete` 回调 | `_mail_on_complete` → `_mail_auto_complete` | ✅ 改为 `_task_auto_complete`(检查产出/状态而非邮件回复) | | 幻觉门控 | `_mail_check_reply` 检查回复邮件 | ⚠️ 改为三信号检查(status + outputs + comments) | | 超时兜底 | ticker 轮询 working 状态 + 再次检查 | ✅ 直接复用 | | auto-working | `on_checks_passed` 回调中标 working | ✅ 直接复用 | | Counter acquire/release | `"main"` session key | ✅ 直接复用 | ### 普通 Task 的完成检测 Mail 的 `_mail_auto_complete` 检查"是否有回复邮件"。普通 Task 改为 `_task_auto_complete`,检查黑板三信号(详见第九节幻觉门控方案)。 --- ## 九、幻觉门控方案(三层) ### 第一层:Daemon 确定性检查(on_complete 触发) **触发时机**:`on_complete` 回调(`openclaw agent` 子进程退出时)。 Agent 在 main session 里执行时子进程还活着,不会触发。不存在长任务误判。 **三信号检查**:status / outputs / comments 任一存在即视为有效完成。 ```python def _task_verify_completion(self, task_id, db_path) -> bool: """普通 Task 完成验证(泛化 Mail 幻觉门控)""" conn = get_connection(db_path) try: # 信号 1:Agent 已自行更新状态(review/done/failed) row = conn.execute( "SELECT status FROM tasks WHERE id=?", (task_id,) ).fetchone() TERMINAL_STATES = {"review", "done", "failed", "cancelled"} if not row or row["status"] in TERMINAL_STATES: return True # Agent 自己标了终态 # 信号 2:outputs 表有产出 output_count = conn.execute( "SELECT COUNT(*) FROM outputs WHERE task_id=?", (task_id,) ).fetchone()[0] if output_count > 0: return True # 信号 3:有评论/handoff(Agent 至少写了交接文档,且 ≥ 50 字符) comment_count = conn.execute( "SELECT COUNT(*) FROM comments WHERE task_id=? AND author != 'system' AND LENGTH(content) >= 50", (task_id,) ).fetchone()[0] if comment_count > 0: return True # 三无 → 可能是幻觉 return False finally: conn.close() ``` **行为**: - 通过 → 标 review(等待庞统 review) - 不通过 → 留 working,ticker 重查(最多 3 次,然后标 failed) **重试计数器存储**:在 tasks 表中复用 `retry_count` 字段(现有字段,用于续杯计数)。Daemon 在 `_task_verify_completion` 返回 False 后递增该字段,达到 3 次后标 failed。不走内存维护(重启丢失)。 ### 第二层:Daemon 产出物文件验证(review 阶段触发) 对 `outputs` 表中有 `content_path` 的产出,验证文件是否真实存在。放在 Daemon(确定性代码,成本低、不依赖 AI): ```python def _verify_output_files(self, task_id, db_path) -> list[str]: """验证产出物文件是否真实存在""" conn = get_connection(db_path) outputs = conn.execute( "SELECT content_path FROM outputs WHERE task_id=? AND content_path IS NOT NULL", (task_id,) ).fetchall() missing = [] for o in outputs: if not Path(o["content_path"]).exists(): missing.append(o["content_path"]) return missing ``` **触发时机**:review 阶段(庞统 review 前),Daemon 先跑一次文件验证。 **行为**:missing 非空 → 在黑板 comment 中标注缺失文件,供庞统 review 时参考。 ### 第三层:AI 验证(review prompt 中要求) 庞统/司马懿 review 时,在 prompt 中要求: 1. 检查产出是否覆盖 must_haves 中的验收标准 2. 如果产出不可验证,在评论中标注 这一层由 AI 做,成本高但准确度高,只在 review 时触发。 ### 三层关系 ``` openclaw agent 子进程退出 → on_complete 回调 → 第一层(三信号:status / outputs / comments) → 有信号 → 标 review → 三无 → 留 working(ticker 重查,最多 3 次 → failed) 庞统 review → 第二层(文件存在性:content_path 验证) → 第三层(AI 验证:产出是否覆盖 must_haves) → 通过 → GOAL_ACHIEVED 或创建新 sub task → 不通过 → 打回重做 ``` --- ## 十、实施计划 ### Phase 1:统一投递到 main session + 幻觉门控 - 所有 `spawn_full_agent` 调用改为 `use_main_session=True`(3 处) - 续杯 `_do_retry` 改为 `use_main_session=True` - 新增 `_task_verify_completion`(第一层幻觉门控) - 新增 `_verify_output_files`(第二层幻觉门控) - 单元测试覆盖 ### Phase 2:E2E 测试适配 + 验证 - 适配新的投递机制(不再依赖 `--session-id UUID`) - 验证 session 不爆炸 - 验证 Agent 自主决策行为 - 重跑 01-four-phase-loop 的 E2E 测试 ### Phase 3:迁移方案 **迁移策略**:新旧路径并行,通过 feature flag 切换。 ```python # spawner.py USE_MAIN_SESSION = os.environ.get("MOZIPLUS_USE_MAIN_SESSION", "0") == "1" def spawn_full_agent(self, agent_id, ..., use_main_session=False): if USE_MAIN_SESSION or use_main_session: # 新路径:投递到 main session return self._spawn_to_main_session(agent_id, ...) else: # 旧路径:UUID session(现有逻辑,保留) return self._spawn_uuid_session(agent_id, ...) ``` **迁移步骤**: 1. 代码部署(新旧路径并存,feature flag 默认关闭) 2. 手动测试一个任务走新路径 → 验证 session 不爆炸、产出物正确 3. 设置 `MOZIPLUS_USE_MAIN_SESSION=1` 开启全量迁移 4. 监控 24h,检查:session 数量、任务完成率、幻觉误判率 5. 确认稳定后删除旧路径代码 **进行中任务处理**: - feature flag 切换前已创建但未完成的任务 → 继续走旧路径(`reuse_session_id=UUID`) - 切换后新创建的任务 → 走新路径(`use_main_session=True`) - `task_type=mail` 不受影响(Mail 路径已在 v1.0 使用 main session) **回滚方案**: - 设置 `MOZIPLUS_USE_MAIN_SESSION=0` → 立即切回旧路径 - 新路径已产生的 session 保留不清理(不影响旧路径) - 回滚不会丢失数据(黑板数据独立于 session 路径) **回滚触发条件**: - session 数量未减少(新路径也有 session 爆炸) - 任务完成率下降超 30% - 幻觉误判(标 failed 但实际有产出)超过 10% - Agent main session 频繁超时(>5 次/小时`openclaw agent` 子进程超时) --- ## 十一、参考文件 | 文件 | 说明 | |------|------| | `01-four-phase-loop.md` | 四相循环设计(现行方案) | | `v2.8-direction-notes.md` | v2.8/v2.9 方向(Prompt 进化 + Daemon 退化) | | `subagent-delegation/SKILL.md` | Delegation skill(Agent 侧执行指南) | | `counter.py` | 并发控制(per session 粒度) | | `spawner.py` `_do_retry` | 续杯机制 | | `dispatcher.py` `_mail_auto_complete` | Mail 幻觉门控(已验证,可复用) |