快速开始
ACP 生命周期重构
ACP 生命周期目前可以正常工作,但其中太多信息是在事后推断出来的。
进程清理根据 PID、命令字符串、包装器路径和实时进程表重建所有权。
会话可见性根据会话键字符串以及辅助 sessions.list({ spawnedBy }) 查询重建所有权。
这使得进行针对性修复成为可能,但也很容易遗漏边缘情况:
PID 重用、带引号的命令、适配器的孙进程、多 Gateway 网关状态根目录、
cancel 与 close,以及 tree 与 all 的可见性,都成为
需要重新推导相同所有权规则的独立位置。
此重构将所有权提升为一等概念。目标不是新增 ACP 产品 界面,而是为现有 ACP 和 ACPX 行为提供更安全的内部契约。
目标
- 除非当前实时证据与 OpenClaw 所有的租约匹配,否则清理绝不向进程发送信号。
cancel、close和启动时回收具有不同的生命周期意图。sessions_list、sessions_history、sessions_send和状态检查使用 相同的请求方所有会话模型。- 多 Gateway 网关安装不能回收彼此的 ACPX 包装器。
- 旧 ACPX 会话记录在迁移期间继续工作。
- 运行时仍归插件所有;核心无需了解 ACPX 软件包细节。
非目标
- 替换 ACPX 或更改公共
/acp命令界面。 - 将供应商特定的 ACP 适配器行为移入核心。
- 要求用户在升级前手动清理状态。
- 让
cancel关闭可复用的 ACP 会话。
目标模型
Gateway 网关实例身份
每个 Gateway 网关进程都应具有稳定的运行时实例 ID:
type GatewayInstanceId = string;它可以在 Gateway 网关启动时生成,并在该安装的生命周期内持久保存于状态中。 它不是安全机密,而是用于区分所有权的标识符, 以避免将一个 Gateway 网关的 ACP 进程与另一个 Gateway 网关的进程混淆。
ACP 会话所有权
每个生成的 ACP 会话都应具有规范化的所有权元数据:
type AcpSessionOwner = { sessionKey: string; spawnedBy?: string; parentSessionKey?: string; ownerSessionKey: string; agentId: string; backend: "acpx"; gatewayInstanceId: GatewayInstanceId; createdAt: number;};Gateway 网关应在已知这些字段的会话行中返回它们。 可见性筛选应是对行元数据执行的纯检查:
canSeeSessionRow({ row, requesterSessionKey, visibility, a2aPolicy,});这会从可见性检查中移除隐藏的辅助 sessions.list({ spawnedBy }) 调用。
生成的跨智能体 ACP 子会话归请求方所有,是因为该行明确如此记录,
而不是因为第二次查询恰好找到了它。
ACPX 进程租约
每次生成的包装器启动都应创建一条租约记录:
type AcpxProcessLease = { leaseId: string; gatewayInstanceId: GatewayInstanceId; sessionKey: string; wrapperRoot: string; wrapperPath: string; rootPid: number; processGroupId?: number; commandHash: string; startedAt: number; state: "open" | "closing" | "closed" | "lost";};包装器进程通过可移植参数接收租约 ID 和 Gateway 网关实例 ID:
--openclaw-acpx-lease-id ... --openclaw-gateway-instance-id ...当平台允许时,验证应优先使用不会因命令引号而产生混淆的实时进程元数据:
- 根 PID 仍然存在
- 实时包装器路径位于
wrapperRoot下 - 进程组在可用时与租约匹配
- 参数包含预期的租约 ID
- 命令哈希或可执行文件路径与租约匹配
如果无法验证实时进程,清理将以失败关闭方式终止。
生命周期控制器
引入一个负责进程租约和清理策略的统一 ACPX 生命周期控制器:
interface AcpxLifecycleController { ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>; cancelTurn(handle: AcpRuntimeHandle): Promise<void>; closeSession(input: { handle: AcpRuntimeHandle; discardPersistentState?: boolean; reason?: string; }): Promise<void>; reapStartupOrphans(): Promise<void>; verifyOwnedTree(lease: AcpxProcessLease): Promise<OwnedProcessTree | null>;}cancelTurn 仅请求取消当前轮次。它绝不能回收可复用的包装器
或适配器进程。
closeSession 可以执行回收,但必须先加载会话记录、
加载租约,并验证实时进程树仍属于该租约。
reapStartupOrphans 从状态中的开放租约开始。它可以使用进程表
查找后代进程,但不应先扫描任意看似 ACP 的命令,
再判断它们可能属于我们。
包装器契约
生成的包装器应保持精简。它们应:
- 在支持的情况下,在进程组中启动适配器
- 将常规终止信号转发给进程组
- 检测父进程死亡
- 父进程死亡时发送 SIGTERM,然后保持包装器运行,直到执行 SIGKILL 后备操作
- 在可用时,将根 PID 和进程组 ID 报告给生命周期控制器
包装器不应决定会话策略。它们仅对自身适配器组实施本地进程树 清理。
会话可见性契约
可见性应使用规范化的行所有权:
type SessionVisibilityInput = { requesterSessionKey: string; row: { key: string; agentId: string; ownerSessionKey?: string; spawnedBy?: string; parentSessionKey?: string; }; visibility: "self" | "tree" | "agent" | "all"; a2aPolicy: AgentToAgentPolicy;};规则:
self:仅请求方会话。tree:请求方会话,以及归请求方所有或由请求方生成的行。all:所有同智能体行、a2a 允许的跨智能体行,以及归请求方所有的 已生成跨智能体行,即使常规 a2a 已禁用。agent:仅限同一智能体,除非明确的所有者关系表明该行 归请求方所有。
这使 tree 和 all 保持单调性:all 绝不能隐藏
tree 会显示的自有子会话。
迁移计划
阶段 1:添加身份和租约
- 向 Gateway 网关状态添加
gatewayInstanceId。 - 在 ACPX 状态目录下添加 ACPX 租约存储。
- 在生成包装器之前写入租约。
- 在新的 ACPX 会话记录中存储
leaseId。 - 为旧记录保留现有 PID 和命令字段。
阶段 2:租约优先清理
- 更改关闭清理流程,使其先加载
leaseId。 - 发送信号前,根据租约验证实时进程所有权。
- 仅为旧记录保留当前的根 PID 和包装器根目录后备逻辑。
- 完成经验证的清理后,将租约标记为
closed。 - 如果进程在清理前已经消失,则将租约标记为
lost。
阶段 3:租约优先的启动时回收
- 启动时回收扫描开放租约。
- 对每个租约验证根进程并收集后代进程。
- 按子进程优先的顺序回收经验证的进程树。
- 使用有界保留窗口使旧
closed和lost租约过期。 - 仅将命令标记扫描作为临时旧版后备逻辑保留,并尽可能使用 包装器根目录和 Gateway 网关实例进行保护。
阶段 4:会话所有权行
- 向 Gateway 网关会话行添加所有权元数据。
- 让 ACPX、子智能体、后台任务和会话存储写入方填充
ownerSessionKey或spawnedBy。 - 将会话可见性检查转换为使用行元数据。
- 移除可见性检查期间的辅助
sessions.list({ spawnedBy })查询。
阶段 5:移除旧版启发式逻辑
经过一个发布周期后:
- 停止依赖存储的根命令字符串进行非旧版 ACPX 清理
- 移除启动时的命令标记扫描
- 移除可见性后备列表查询
- 对缺失或无法验证的租约保留防御性的失败关闭行为
测试
添加两个表驱动测试套件。
进程生命周期模拟器:
- PID 被无关进程重用
- PID 被另一个 Gateway 网关的包装器根进程重用
- 存储的包装器命令经过 shell 引号处理,而实时
ps命令没有 - 适配器子进程退出,但孙进程仍留在进程组中
- 父进程死亡时的 SIGTERM 后备操作最终执行 SIGKILL
- 进程列表不可用
- 进程缺失的过期租约
- 包含包装器、适配器子进程和孙进程的启动孤儿进程
会话可见性矩阵:
self、tree、agent、all- 启用和禁用 a2a
- 同智能体行
- 跨智能体行
- 归请求方所有的已生成跨智能体 ACP 行
- 沙箱隔离的请求方被限制为
tree - 列表、历史记录、发送和状态操作
重要的不变量是:只要配置的可见性包含请求方会话树,
归请求方所有的已生成子会话就应可见,而且 all 的能力
不得弱于 tree。
兼容性说明
旧会话记录可能没有 leaseId。它们应使用旧版
失败关闭清理路径:
- 要求存在实时根进程
- 预期使用生成的包装器时,要求包装器根目录所有权匹配
- 对于非包装器根进程,要求命令一致
- 绝不只根据过期的已存储 PID 元数据发送信号
如果无法验证旧记录,则不要对其执行任何操作。启动时租约清理和 下一个发布周期最终应淘汰该后备逻辑。
成功标准
- 关闭旧的或过期的 ACPX 会话不能终止另一个 Gateway 网关的进程。
- 父进程死亡后不会留下顽固运行的适配器孙进程。
cancel中止当前轮次,但不关闭可复用会话。sessions_list可以在tree和all下显示归请求方所有的 跨智能体 ACP 子会话。- 启动清理由租约驱动,而不是依赖宽泛的命令字符串扫描。
- 针对进程和可见性矩阵的专项测试覆盖此前需要逐项审查修复的 每个边缘情况。