ACP 生命周期目前可以工作,但过多依赖事后推断:进程清理从 PID、命令字符串、wrapper 路径和活跃进程表重建所有权;session 可见性从 session-key 字符串加二次 sessions.list({ spawnedBy }) 查询重建所有权。

本重构目标是让所有权成为一等公民,而非新的 ACP 产品面。

目标

  • 清理操作只在活跃证据匹配 OpenClaw 拥有的 lease 时才发信号
  • cancel、close、启动时孤儿回收有明确的生命周期意图
  • sessions_list、sessions_history、sessions_send、状态检查使用同一个请求者拥有的 session 模型
  • 多 gateway 安装不能回收彼此的 ACPX wrapper
  • 旧 ACPX session 记录在迁移期间继续工作

核心数据结构

Gateway 实例身份:

type GatewayInstanceId = string;

ACP Session 所有权:

type AcpSessionOwner = {
  sessionKey: string;
  ownerSessionKey: string;
  agentId: string;
  backend: "acpx";
  gatewayInstanceId: GatewayInstanceId;
  createdAt: number;
};

ACPX 进程 Lease:

type AcpxProcessLease = {
  leaseId: string;
  gatewayInstanceId: GatewayInstanceId;
  sessionKey: string;
  wrapperRoot: string;
  rootPid: number;
  commandHash: string;
  state: "open" | "closing" | "closed" | "lost";
};

Wrapper 进程通过环境变量接收 lease 信息:

OPENCLAW_ACPX_LEASE_ID=...
OPENCLAW_GATEWAY_INSTANCE_ID=...

生命周期控制器接口

interface AcpxLifecycleController {
  ensureSession(input): Promise<AcpRuntimeHandle>;
  cancelTurn(handle): Promise<void>;      // 仅取消当前 turn,不回收 reusable session
  closeSession(input): Promise<void>;     // 验证 lease 后才回收
  reapStartupOrphans(): Promise<void>;    // 从 open leases 扫描,不扫描任意 ACP 命令
  verifyOwnedTree(lease): Promise<OwnedProcessTree | null>;
}

Session 可见性规则

visibility可见范围
self仅请求者 session
tree请求者 session + 其拥有或派生的 session
all同 agent 所有 session + a2a 允许的跨 agent + 请求者拥有的派生跨 agent
agent仅同 agent,除非有明确所有权关系

all 必须不少于 tree:all 不能隐藏 tree 会显示的拥有子 session。

迁移阶段

  1. 添加身份和 Lease:添加 gatewayInstanceId,在生成 wrapper 前写入 lease
  2. Lease 优先清理:close 清理先加载 leaseId,验证活跃进程所有权后再发信号
  3. Lease 优先启动回收:启动回收扫描 open leases,而非扫描任意 ACP 命令
  4. Session 所有权行:向 Gateway session 行添加所有权元数据,转换可见性检查
  5. 移除遗留启发式:一个发布窗口后,停止依赖存储的 root 命令字符串

兼容性

旧 session 记录可能没有 leaseId,使用遗留 fail-closed 清理路径:

  • 要求活跃 root 进程
  • 要求 wrapper-root 所有权(预期有 generated wrapper 时)
  • 永不仅基于陈旧存储的 PID 元数据发信号

相关页面