16 KiB
02-main-session-delegation.md — Main Session + Delegation 架构
日期: 2026-05-29
作者: 庞统
状态: 设计中
前置: 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-<UUID>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 |
投递到 main session,Agent 自己决定执行方式 |
| Mention | spawn_full_agent(session_id=UUID) 新 session |
投递到 main session,Agent 自主处理 |
| Review(庞统) | spawn_full_agent(session_id=UUID) 新 session |
投递到庞统 main session,庞统自主 review |
三、关键机制
3.1 投递到 Main Session
方式:openclaw agent --agent <id> --message "..."(不传 --session-id)
Gateway 行为(已确认):
- 不传
--session-id→ 投递到 Agent 的 main session(agent:<id>:main) - main session 正忙 → 消息排队,Agent 当前 turn 完成后处理
- Agent 有完整上下文(SOUL.md / MEMORY.md / workspace bootstrap)
spawner 改动:
- 新增
deliver_to_main()方法,用openclaw agent --agent <id> --message "..." --json投递 - 不走
spawn_full_agent的 session 管理,改用 Gateway 的原生投递机制 - counter 仍然生效(per main session 粒度,
max_per_session=1保证不并发)
3.2 Delegation(subagent-delegation skill)
Agent 收到任务后,用 sessions_spawn 创建 subagent 执行具体工作:
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 完成(非阻塞,可被新消息打断) - sub 的结果通过完成事件回传给 main session
3.3 续杯机制(已有,无需改动)
Agent 在 main session 中执行,如果超时/中断:
- 续杯机制(
_do_retry)已经实现了重新投递逻辑 - counter 的 acquire/release 在
spawn_full_agent内部管理 - 改用 main session 后,续杯变成"重新投递到 main session",逻辑更简单
3.4 Counter 调整
改用 main session 后:
- 所有 spawn 走同一个 session key(
agent:main) max_per_session=1保证同一时间只有一个 active turnmax_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 自主决策:
📋 新任务到达
**任务**: {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 收到消息后的自主行为(不是硬编码步骤):
- 分析上下文:读黑板 API 获取任务详情、依赖、历史讨论
- 决定执行策略:
- 简单任务 → 自己直接做
- 复杂任务 →
sessions_spawn拆分 sub task - 需要协作 → @mention 其他 Agent
- 执行:通过 subagent 或直接执行
- Review:检查 sub 结果质量
- 写回:产出 + 状态 + handoff comment
4.3 庞统 Review 消息格式
🔍 一轮结束,需要你的 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 Spawner 新增方法
复用 Mail 的 use_main_session=True 模式,将三条路径统一:
async def deliver_to_main(self, agent_id: str, message: str,
task_id: str = None,
on_complete: Callable = None) -> str:
"""投递到 Agent main session
复用 spawn_full_agent(use_main_session=True),不传 --session-id。
Gateway 自动路由到 main session。
"""
return await self.spawn_full_agent(
agent_id=agent_id,
message=message,
use_main_session=True, # 关键:走 main session
task_id=task_id,
on_complete=on_complete,
)
不需要新建方法,只需调用 spawn_full_agent(use_main_session=True)。Mail 路径已验证此模式。
5.2 Ticker 改动
| 方法 | 改动 |
|---|---|
_broadcast_and_dispatch |
spawn_full_agent(..., use_main_session=True) |
_process_mentions |
spawn_full_agent(..., use_main_session=True) |
_spawn_pangtong_review |
spawn_full_agent(..., use_main_session=True) |
_check_round_complete |
保留 reviewing 中间态逻辑 |
_handle_review_conclusion |
改为从黑板检查状态变化(复用 Mail 的 _task_auto_complete 模式) |
5.3 Dispatcher 改动
- 所有路由统一走
use_main_session=True - 移除
new_session参数(不再需要) - 新增
_task_auto_complete(复用 Mail 的_mail_auto_complete模式,检查 outputs + status) - counter 逻辑不变(main session 粒度,per key =
agent:main)
5.4 废弃代码
spawn_full_agent的new_session参数标注废弃(所有调用改为use_main_session=True)gateway-fallback-*session 的产生路径消失- 续杯简化:main session 的续杯由 Gateway 排队 + ticker 超时兜底处理
六、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 / use_main_session / reuse_session_id 逻辑 |
| Prompt 灵活度 | 从硬编码步骤 → Agent 自主决策 |
| 空 payloads | 消除(main session 有完整生命周期管理) |
风险
| 风险 | 缓解 |
|---|---|
| main session 上下文膨胀 | OpenClaw 自带的 compact 机制处理;sub session 不膨胀(用完删) |
| main session 被阻塞 | sessions_yield 非阻塞,可被新消息打断;Gateway 排队机制保证不丢消息 |
| sub 质量不可控 | main session review sub 结果;reviewing 中间态保证 round 不重复触发 |
| Prompt 依赖 | Agent 需要正确的 prompt 来知道怎么 delegation;subagent-delegation skill 已调优 |
| Gateway 投递延迟 | main session 排队是异步的,Daemon 不同步等结果;改为"投递后等黑板状态变化" |
八、Mail 路径已验证的完整模式(可直接复用)
Mail 路径(dispatcher.py _mail_on_checks_passed + _mail_auto_complete)已经完整实现了 main session + 完成检测的全链路:
openclaw agent --agent xxx(不传 --session-id)→ Gateway 投递到 main session
→ Agent 处理 → 进程退出
→ on_complete 回调触发 → _mail_auto_complete
→ 幻觉门控:检查黑板是否有回复
→ 有回复 → 标 done
→ 无回复 → 留 working,等 ticker 超时兜底再查
已验证的机制
| 机制 | Mail 实现 | 普通 Task 复用 |
|---|---|---|
| Main session 投递 | use_main_session=True |
✅ 直接复用 |
on_complete 回调 |
_mail_on_complete → _mail_auto_complete |
✅ 改为检查产出/状态而非邮件回复 |
| 幻觉门控 | _mail_check_reply 检查回复邮件 |
⚠️ 改为检查 outputs 表 + status 字段 |
| 超时兜底 | ticker 轮询 working 状态 + 再次检查 | ✅ 直接复用 |
| auto-working | on_checks_passed 回调中标 working |
✅ 直接复用 |
普通 Task 的完成检测(复用 Mail 模式)
Mail 的 _mail_auto_complete 检查的是"是否有回复邮件"。普通 Task 改为检查:
def _task_auto_complete(self, task_id, agent_id, db_path):
"""普通 Task 完成检测(复用 Mail 模式)
检查逻辑:
1. outputs 表是否有产出(非空)
2. status 是否已由 Agent 自行更新(review/done/failed)
3. 如果都没有 → 留 working,等 ticker 超时兜底
"""
conn = get_connection(db_path)
try:
row = conn.execute("SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
if not row or row["status"] != "working":
return # Agent 已自行更新状态
# 检查是否有产出
output_count = conn.execute(
"SELECT COUNT(*) FROM outputs WHERE task_id=?", (task_id,)
).fetchone()[0]
if output_count > 0:
# 有产出 → Agent 忘了标状态,帮它标 review
conn.execute("UPDATE tasks SET status='review' WHERE id=?", (task_id,))
conn.commit()
else:
# 无产出 → 留 working 等 ticker 超时兜底
pass
finally:
conn.close()
待确认
| # | 问题 | 说明 |
|---|---|---|
| 1 | 投递消息的 prompt 模板 | 需要和 v2.8 方向的 Prompt 进化对齐(身份+能力+约束 vs 固定步骤) |
| 2 | E2E 测试适配 | 现有 E2E 测试依赖 spawner 的 --session-id UUID 机制,需要适配 |
| 3 | 幻觉门控策略 | 普通 Task 用 outputs 表 + status 字段检查,还是其他方式? |
九、实施计划
Phase 1:统一投递到 main session
- 所有
spawn_full_agent调用改为use_main_session=True - 新增
_task_auto_complete(复用 Mail 模式) - 单元测试覆盖
Phase 2:E2E 测试适配 + 验证
- 适配新的投递机制(不再依赖
--session-id UUID) - 验证 session 不爆炸
- 验证 Agent 自主决策行为
- 重跑 01-four-phase-loop 的 E2E 测试
十、参考文件
| 文件 | 说明 |
|---|---|
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 |
续杯机制 |