Files
sanguo_moziplus_v2/docs/design/02-main-session-delegation.md
T
2026-05-30 21:32:20 +08:00

23 KiB
Raw Blame History

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 个 session16~484 每个),其中 56 个 gateway-fallback-* 文件系统堆积、磁盘浪费
空 payloads 绝大部分 spawn 返回 status: None, payloads: [] Daemon 以为任务完成了但实际没有产出
上下文丢失 每次 spawn 新 sessionAgent 无历史、无 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 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 <id> --message "..." --json --timeout N

Gateway 行为(已确认):

  • 不传 --session-id → 投递到 Agent 的 main sessionagent:<id>: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 Delegationsubagent-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 完成(非阻塞,可被新消息打断,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 个不同 sessionmain + 可能的 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 收到消息后的自主行为(不是硬编码步骤):

  1. 分析上下文:读黑板 API 获取任务详情、依赖、历史讨论
  2. 决定执行策略
    • 简单任务 → 自己直接做
    • 复杂任务 → sessions_spawn 拆分 sub task
    • 需要协作 → @mention 其他 Agent
  3. 执行:通过 subagent 或直接执行
  4. Review:检查 sub 结果质量
  5. 写回:产出 + 状态 + handoff commenthandoff 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 消息格式

🔍 一轮结束,需要你的 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_agentnew_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 分支选择检测逻辑:

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 中明确告诉 Agentsub 失败时不要标 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 任一存在即视为有效完成。

def _task_verify_completion(self, task_id, db_path) -> bool:
    """普通 Task 完成验证(泛化 Mail 幻觉门控)"""
    conn = get_connection(db_path)
    try:
        # 信号 1Agent 已自行更新状态(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 自己标了终态

        # 信号 2outputs 表有产出
        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,详见 #01 §4.6
  • 不通过 → 留 working,ticker 重查(最多 3 次,然后标 failed)

重试计数器存储:在 tasks 表中复用 retry_count 字段(现有字段,用于续杯计数)。Daemon 在 _task_verify_completion 返回 False 后递增该字段,达到 3 次后标 failed。不走内存维护(重启丢失)。

第二层:Daemon 产出物文件验证(review 阶段触发)

outputs 表中有 content_path 的产出,验证文件是否真实存在。放在 Daemon(确定性代码,成本低、不依赖 AI):

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
      → 三无 → 留 workingticker 重查,最多 3 次 → failed

司马懿 review_dispatch_reviews 确定性路由)
  → review 通过 → 标 done
  → review 不通过 → 打回 working

庞统 review_check_round_complete → _spawn_pangtong_review
  → 第二层(文件存在性:content_path 验证)
  → 第三层(AI 验证:产出是否覆盖 must_haves
  → GOAL_ACHIEVED → parent done
  → 创建新 sub task → 新一轮执行
  → 不通过 → 打回重做

十、实施计划

Phase 1:统一投递到 main session + 幻觉门控

  • 所有 spawn_full_agent 调用改为 use_main_session=True3 处)
  • 续杯 _do_retry 改为 use_main_session=True
  • 新增 _task_verify_completion(第一层幻觉门控)
  • 新增 _verify_output_files(第二层幻觉门控)
  • 单元测试覆盖

Phase 2E2E 测试适配 + 验证

  • 适配新的投递机制(不再依赖 --session-id UUID
  • 验证 session 不爆炸
  • 验证 Agent 自主决策行为
  • 重跑 01-four-phase-loop 的 E2E 测试

Phase 3:已完成

用户确认一步到位直接使用 use_main_session=True,不需要 feature flag 灰度切换。 所有 spawn_full_agent 调用均已硬编码 use_main_session=True

  • 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 skillAgent 侧执行指南)
counter.py 并发控制(per session 粒度)
spawner.py _do_retry 续杯机制
dispatcher.py _mail_auto_complete Mail 幻觉门控(已验证,可复用)