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。
迁移阶段
- 添加身份和 Lease:添加
gatewayInstanceId,在生成 wrapper 前写入 lease - Lease 优先清理:close 清理先加载
leaseId,验证活跃进程所有权后再发信号 - Lease 优先启动回收:启动回收扫描 open leases,而非扫描任意 ACP 命令
- Session 所有权行:向 Gateway session 行添加所有权元数据,转换可见性检查
- 移除遗留启发式:一个发布窗口后,停止依赖存储的 root 命令字符串
兼容性
旧 session 记录可能没有 leaseId,使用遗留 fail-closed 清理路径:
- 要求活跃 root 进程
- 要求 wrapper-root 所有权(预期有 generated wrapper 时)
- 永不仅基于陈旧存储的 PID 元数据发信号