Get started
Рефакторинг життєвого циклу 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 продовжують працювати під час міграції.
- Runtime залишається у власності Plugin; ядро не дізнається деталей пакета ACPX.
Нецілі
- Заміна ACPX або зміна публічної поверхні команди
/acp. - Перенесення поведінки ACP-адаптерів, специфічної для постачальника, у ядро.
- Вимога до користувачів вручну очищати стан перед оновленням.
- Перетворення
cancelна закриття повторно використовуваних ACP-сесій.
Цільова модель
Ідентичність екземпляра Gateway
Кожен процес Gateway повинен мати стабільний ідентифікатор runtime-екземпляра:
type GatewayInstanceId = string;
Його можна генерувати під час запуску Gateway і зберігати у стані на весь строк життя цього встановлення. Це не секрет безпеки; це розрізнювач належності, який використовується, щоб не плутати ACP-процеси одного Gateway з процесами іншого 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";
};
Процес обгортки повинен отримувати ідентифікатор лізу та ідентифікатор екземпляра Gateway у своєму середовищі:
OPENCLAW_ACPX_LEASE_ID=...
OPENCLAW_GATEWAY_INSTANCE_ID=...
Коли платформа це дозволяє, перевірка має надавати перевагу живим метаданим процесу, які не можна сплутати через лапки в командах:
- кореневий PID досі існує
- живий шлях обгортки розташований під
wrapperRoot - група процесів відповідає лізу, коли доступна
- середовище містить очікуваний ідентифікатор лізу, коли його можна прочитати
- хеш команди або шлях виконуваного файла відповідає лізу
Якщо живий процес неможливо перевірити, очищення відмовляє в закритому режимі.
Контролер життєвого циклу
Запровадьте один контролер життєвого циклу 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 та ідентифікатор групи процесів назад контролеру життєвого циклу, коли це доступно
Обгортки не повинні визначати політику сесій. Вони лише забезпечують локальне очищення дерева процесів для власної групи адаптера.
Контракт видимості сесій
Видимість має використовувати нормалізовану належність рядків:
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: Додати ідентичність і лізи
- Додати
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.- Очищення під час запуску керується лізами, а не широким скануванням рядків команд.
- Цільові тести матриці процесів і видимості покривають кожен крайовий випадок, який раніше вимагав одноразових виправлень під час рев’ю.