---
read_when:
    - ACPセッションのライフサイクルまたはACPXプロセスのクリーンアップのリファクタリング
    - ACPX の孤立プロセス、PID の再利用、または複数 Gateway 環境でのクリーンアップの安全性をデバッグする
    - 生成された ACP またはサブエージェントセッションに対する sessions_list の可視性の変更
    - バックグラウンドタスク、ACP セッション、またはプロセスリースの所有権メタデータを設計する
sidebarTitle: ACP lifecycle refactor
summary: ACP セッションと ACPX プロセスの所有権を明示するための移行計画
title: ACP ライフサイクルのリファクタリング
x-i18n:
    generated_at: "2026-07-11T22:38:33Z"
    model: gpt-5.6
    postprocess_version: locale-links-v1
    provider: openai
    source_hash: b7f4ee447e0b436601c68251c26c1b897a642f6a8b1886d18647b62817996792
    source_path: refactor/acp.md
    workflow: 16
---

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 を持たせる必要があります。

```ts
type GatewayInstanceId = string;
```

Gateway の起動時に生成し、そのインストール環境の存続期間中、状態に永続化できます。
これはセキュリティ上のシークレットではありません。ある Gateway の ACP プロセスと
別の Gateway のプロセスを取り違えないために使用する、所有権の識別子です。

### ACP セッションの所有権

生成されたすべての ACP セッションには、正規化された所有権メタデータを持たせる必要があります。

```ts
type AcpSessionOwner = {
  sessionKey: string;
  spawnedBy?: string;
  parentSessionKey?: string;
  ownerSessionKey: string;
  agentId: string;
  backend: "acpx";
  gatewayInstanceId: GatewayInstanceId;
  createdAt: number;
};
```

Gateway は、これらのフィールドが判明しているセッション行で返す必要があります。
可視性フィルタリングは、行メタデータのみを対象とする純粋な判定にする必要があります。

```ts
canSeeSessionRow({
  row,
  requesterSessionKey,
  visibility,
  a2aPolicy,
});
```

これにより、可視性チェックから暗黙的な追加の `sessions.list({ spawnedBy })` 呼び出しが
なくなります。生成されたエージェント間 ACP 子セッションがリクエスター所有になるのは、
2 回目のクエリで偶然見つかったからではなく、行にそのように記録されているからです。

### ACPX プロセスリース

生成されたラッパーを起動するたびに、リースレコードを作成する必要があります。

```ts
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 を受け取る必要があります。

```sh
OPENCLAW_ACPX_LEASE_ID=...
OPENCLAW_GATEWAY_INSTANCE_ID=...
```

プラットフォームで可能な場合、検証では、コマンドの引用方法によって取り違えられない
稼働中のプロセスメタデータを優先する必要があります。

- ルート PID がまだ存在する
- 稼働中のラッパーパスが `wrapperRoot` 配下にある
- 利用可能な場合、プロセスグループがリースと一致する
- 読み取り可能な場合、環境に期待されるリース ID が含まれている
- コマンドハッシュまたは実行可能ファイルのパスがリースと一致する

稼働中のプロセスを検証できない場合、クリーンアップは安全側に失敗します。

## ライフサイクルコントローラー

プロセスリースとクリーンアップポリシーを所有する、単一の ACPX ライフサイクルコントローラーを導入します。

```ts
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 をライフサイクルコントローラーへ
  報告する

ラッパーはセッションポリシーを決定するべきではありません。自身のアダプターグループに
対するローカルなプロセスツリーのクリーンアップのみを強制します。

## セッション可視性契約

可視性では、正規化された行の所有権を使用する必要があります。

```ts
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：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 子セッションを表示できる。
- 起動時のクリーンアップは、広範なコマンド文字列スキャンではなくリースによって駆動される。
- 対象を絞ったプロセスおよび可視性マトリクステストで、以前は個別のレビュー修正が
  必要だったすべてのエッジケースを網羅する。
