---
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-05-07T13:25:53Z"
    model: gpt-5.5
    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 ライフサイクルコントローラーを 1 つ導入します:

```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` に制限される
- list、history、send、status アクション

重要な不変条件: リクエスター所有の生成された子は、設定された可視性がリクエスターのセッションツリーを含む場所では可視であり、
`all` は `tree` より能力が低くなってはいけません。

## 互換性メモ

古いセッションレコードには `leaseId` がない場合があります。これらはレガシーの
失敗時クローズのクリーンアップパスを使うべきです:

- ライブのルートプロセスを必須とする
- 生成ラッパーが期待される場合、ラッパールートの所有権を必須とする
- 非ラッパールートではコマンドの一致を必須とする
- 古い保存済み PID メタデータのみに基づいてシグナルを送らない

レガシーレコードを検証できない場合は、そのままにします。起動時のリースクリーンアップと
次のリリース期間によって、最終的にフォールバックは廃止されるべきです。

## 成功基準

- 古い、または古くなった ACPX セッションを閉じても、別の Gateway のプロセスを終了できない。
- 親の終了後も、しぶといアダプターの孫プロセスが実行中のまま残らない。
- `cancel` は、再利用可能なセッションを閉じることなくアクティブなターンを中止する。
- `sessions_list` は、`tree` と `all` の両方で、リクエスター所有のクロスエージェント ACP 子を表示できる。
- 起動時のクリーンアップは広範なコマンド文字列スキャンではなく、リースによって駆動される。
- 焦点を絞ったプロセスと可視性マトリックスのテストが、以前は個別レビュー修正を必要としていたすべてのエッジケースを網羅している。
