はじめに
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 セッションレコードは引き続き動作します。
- ランタイムは引き続き Plugin が所有し、コアは ACPX パッケージの詳細を認識しません。
目標外
- ACPX の置き換え、または公開
/acpコマンドサーフェスの変更。 - ベンダー固有の ACP アダプター動作をコアへ移動すること。
- アップグレード前にユーザーへ状態の手動クリーンアップを要求すること。
cancelに再利用可能な ACP セッションを閉じさせること。
目標モデル
Gateway インスタンス ID
各 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 は単調になります。tree で表示される所有済みの子を
all で非表示にしてはなりません。
移行計画
フェーズ 1: ID とリースの追加
- 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: レガシーヒューリスティックの削除
1 リリース期間後:
- 非レガシー ACPX のクリーンアップで、保存されたルートコマンド文字列への依存を停止する
- コマンドマーカーによる起動時スキャンを削除する
- 可視性のフォールバックリスト検索を削除する
- 欠落または検証不能なリースに対する防御的なフェイルクローズ動作を維持する
テスト
テーブル駆動のスイートを 2 つ追加します。
プロセスライフサイクルシミュレーター:
- 無関係なプロセスによって PID が再利用される
- 別の Gateway のラッパールートによって PID が再利用される
- 保存されたラッパーコマンドにはシェル引用符があり、実行中の
psコマンドにはない - アダプターの子プロセスが終了し、孫プロセスがプロセスグループ内に残る
- 親プロセス終了時の SIGTERM フォールバックが SIGKILL に到達する
- プロセス一覧を取得できない
- プロセスが存在しない古いリース
- ラッパー、アダプターの子プロセス、孫プロセスを含む起動時の孤児プロセス
セッション可視性マトリックス:
self、tree、agent、all- a2a が有効な場合と無効な場合
- 同一エージェントの行
- クロスエージェント行
- リクエスター所有の生成されたクロスエージェント ACP 行
- サンドボックス化されたリクエスターが
treeに制限される - 一覧、履歴、送信、ステータスの各アクション
重要な不変条件: リクエスター所有の生成された子は、設定された可視性に
リクエスターのセッションツリーが含まれるすべての箇所で表示され、all の能力が
tree を下回ることはありません。
互換性に関する注意事項
古いセッションレコードには leaseId が存在しない場合があります。その場合は、レガシーの
フェイルクローズ型クリーンアップパスを使用する必要があります。
- 実行中のルートプロセスを必須とする
- 生成されたラッパーが想定される場合、ラッパールートの所有権を必須とする
- ラッパー以外のルートではコマンドの一致を必須とする
- 古い保存済み PID メタデータだけを根拠にシグナルを送信しない
レガシーレコードを検証できない場合は、そのままにします。起動時のリースクリーンアップと 次のリリース期間によって、最終的にフォールバックを廃止する必要があります。
成功基準
- 古い、または期限切れの ACPX セッションを閉じても、別の Gateway のプロセスを終了させることはできない。
- 親プロセスが終了しても、終了しにくいアダプターの孫プロセスが実行中のまま残らない。
cancelは再利用可能なセッションを閉じることなく、アクティブなターンを中止する。sessions_listは、treeとallの両方で、リクエスター所有のクロスエージェント ACP 子セッションを表示できる。- 起動時のクリーンアップは、広範なコマンド文字列スキャンではなくリースによって駆動される。
- 対象を絞ったプロセスおよび可視性マトリックスのテストで、以前は個別のレビュー修正が必要だったすべてのエッジケースを網羅する。