---
read_when:
    - Рефакторинг жизненного цикла сеанса ACP или очистки процесса ACPX
    - Отладка осиротевших процессов ACPX, повторного использования PID или безопасности очистки нескольких Gateway
    - Изменение видимости `sessions_list` для порожденных сеансов ACP или субагентов
    - Проектирование метаданных владения для фоновых задач, сеансов ACP или аренд процессов
sidebarTitle: ACP lifecycle refactor
summary: План миграции для явного указания владения сеансом ACP и процессом ACPX
title: Рефакторинг жизненного цикла ACP
x-i18n:
    generated_at: "2026-06-28T23:42:13Z"
    model: gpt-5.5
    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

У каждого процесса Gateway должен быть стабильный идентификатор экземпляра
среды выполнения:

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

Он может генерироваться при запуске Gateway и сохраняться в состоянии на весь
срок жизни этой установки. Это не секрет безопасности; это дискриминатор
принадлежности, используемый, чтобы не спутать ACP-процессы одного Gateway с
процессами другого 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-потомок между агентами принадлежит
запрашивающему, потому что так указано в строке, а не потому что второй запрос
случайно его нашел.

### Записи аренды процессов 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";
};
```

Процесс обертки должен получать идентификатор аренды и идентификатор экземпляра
Gateway в своем окружении:

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

Когда платформа это позволяет, проверка должна предпочитать живые метаданные
процесса, которые невозможно спутать из-за кавычек в команде:

- корневой PID все еще существует
- живой путь обертки находится внутри `wrapperRoot`
- группа процессов совпадает с записью аренды, когда она доступна
- окружение содержит ожидаемый идентификатор аренды, когда его можно прочитать
- хеш команды или путь исполняемого файла совпадает с записью аренды

Если живой процесс нельзя проверить, очистка завершается закрытым отказом.

## Контроллер жизненного цикла

Введите единый контроллер жизненного цикла 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 и идентификатор группы процессов обратно контроллеру
  жизненного цикла, когда это доступно

Обертки не должны определять политику сеанса. Они только обеспечивают локальную
очистку дерева процессов для собственной группы адаптера.

## Контракт видимости сеансов

Видимость должна использовать нормализованную принадлежность строки:

```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 строки между агентами и
  принадлежащие запрашивающему порожденные ACP-строки между агентами, даже если
  общий a2a отключен.
- `agent`: только тот же агент, если только явная связь принадлежности не
  говорит, что строка принадлежит запрашивающему.

Это делает `tree` и `all` монотонными: `all` не должен скрывать принадлежащего
потомка, которого показал бы `tree`.

## План миграции

### Фаза 1: добавить идентификатор и записи аренды

- Добавить `gatewayInstanceId` в состояние Gateway.
- Добавить хранилище записей аренды ACPX в каталог состояния ACPX.
- Записывать аренду перед порождением сгенерированной обертки.
- Сохранять `leaseId` в новых записях сеансов ACPX.
- Оставить существующие поля 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` может показывать принадлежащих запрашивающему ACP-потомков
  между агентами как в `tree`, так и в `all`.
- Очистка при запуске управляется записями аренды, а не широким сканированием
  строк команд.
- Сфокусированные тесты матрицы процессов и видимости покрывают каждый
  граничный случай, который ранее требовал разовых исправлений по результатам
  ревью.
