chore(i18n): refresh uk translations

This commit is contained in:
openclaw-docs-i18n[bot] 2026-04-15 20:08:32 +00:00
parent 24a7c301f5
commit e98a56b19d

View File

@ -0,0 +1,132 @@
---
x-i18n:
generated_at: "2026-04-15T20:07:47Z"
model: gpt-5.4
provider: openai
source_hash: 95e56c5411204363676f002059c942201503e2359515d1a4b409882cc2e04920
source_path: refactor/async-exec-duplicate-completion-investigation.md
workflow: 15
---
# Розслідування дубльованого завершення Async Exec
## Обсяг
- Сесія: `agent:main:telegram:group:-1003774691294:topic:1`
- Симптом: те саме асинхронне завершення exec для session/run `keen-nexus` було двічі зафіксоване в LCM як ходи користувача.
- Мета: визначити, що є найімовірнішим — дубльоване інʼєктування сесії чи звичайний повтор вихідної доставки.
## Висновок
Найімовірніше, це **дубльоване інʼєктування сесії**, а не просто повтор вихідної доставки.
Найсильніша прогалина з боку Gateway — у **шляху завершення node exec**:
1. Завершення exec на боці node емітить `exec.finished` із повним `runId`.
2. Gateway `server-node-events` перетворює це на системну подію й запитує Heartbeat.
3. Запуск Heartbeat інʼєктує злитий блок системних подій у prompt агента.
4. Вбудований runner зберігає цей prompt як новий хід користувача в транскрипті сесії.
Якщо той самий `exec.finished` з якоїсь причини двічі доходить до gateway для того самого `runId` (replay, дубль після reconnect, повторне надсилання згори, дубльований producer), OpenClaw наразі **не має перевірки ідемпотентності з ключем `runId`/`contextKey`** на цьому шляху. Друга копія стане другим повідомленням користувача з тим самим вмістом.
## Точний шлях у коді
### 1. Producer: подія завершення node exec
- `src/node-host/invoke.ts:340-360`
- `sendExecFinishedEvent(...)` емітить `node.event` з подією `exec.finished`.
- Payload містить `sessionKey` і повний `runId`.
### 2. Приймання події в Gateway
- `src/gateway/server-node-events.ts:574-640`
- Обробляє `exec.finished`.
- Будує текст:
- `Exec finished (node=..., id=<runId>, code ...)`
- Ставить його в чергу через:
- `enqueueSystemEvent(text, { sessionKey, contextKey: runId ? \`exec:${runId}\` : "exec", trusted: false })`
- Одразу запитує wake:
- `requestHeartbeatNow(scopedHeartbeatWakeOptions(sessionKey, { reason: "exec-event" }))`
### 3. Слабке місце дедуплікації системних подій
- `src/infra/system-events.ts:90-115`
- `enqueueSystemEvent(...)` пригнічує лише **послідовні дублікати тексту**:
- `if (entry.lastText === cleaned) return false`
- Воно зберігає `contextKey`, але **не** використовує `contextKey` для ідемпотентності.
- Після drain пригнічення дублікатів скидається.
Це означає, що повторно відтворений `exec.finished` із тим самим `runId` може бути знову прийнятий пізніше, хоча код уже мав стабільного кандидата для ідемпотентності (`exec:<runId>`).
### 4. Обробка wake не є основним джерелом дублювання
- `src/infra/heartbeat-wake.ts:79-117`
- Wake-сповіщення коалесцюються за `(agentId, sessionKey)`.
- Дубльовані запити wake для тієї самої цілі згортаються в один pending wake entry.
Через це **саме дублювання обробки wake** виглядає слабшим поясненням, ніж дубльоване приймання подій.
### 5. Heartbeat споживає подію і перетворює її на вхід для prompt
- `src/infra/heartbeat-runner.ts:535-574`
- Preflight переглядає pending system events і класифікує запуск exec-event.
- `src/auto-reply/reply/session-system-events.ts:86-90`
- `drainFormattedSystemEvents(...)` зливає чергу для сесії.
- `src/auto-reply/reply/get-reply-run.ts:400-427`
- Злитий блок системних подій додається на початок тіла prompt агента.
### 6. Точка інʼєкції в транскрипт
- `src/agents/pi-embedded-runner/run/attempt.ts:2000-2017`
- `activeSession.prompt(effectivePrompt)` надсилає повний prompt у вбудовану PI-сесію.
- Саме тут prompt, похідний від завершення, стає збереженим ходом користувача.
Отже, щойно ту саму системну подію двічі перебудовано в prompt, дубльовані повідомлення користувача в LCM є очікуваними.
## Чому простий повтор вихідної доставки менш імовірний
У heartbeat runner справді є шлях вихідної помилки:
- `src/infra/heartbeat-runner.ts:1194-1242`
- Спочатку генерується відповідь.
- Вихідна доставка відбувається пізніше через `deliverOutboundPayloads(...)`.
- Помилка там повертає `{ status: "failed" }`.
Однак для того самого запису в черзі системних подій цього **самого по собі недостатньо**, щоб пояснити дубльовані ходи користувача:
- `src/auto-reply/reply/session-system-events.ts:86-90`
- Черга системних подій уже злита до вихідної доставки.
Тому сам по собі повтор channel send не відтворив би той самий запис у черзі. Це може пояснити відсутню/невдалу зовнішню доставку, але саме по собі не пояснює друге ідентичне повідомлення користувача в сесії.
## Другорядна, менш упевнена можливість
У runner агента є цикл повного повтору запуску:
- `src/auto-reply/reply/agent-runner-execution.ts:741-1473`
- Деякі тимчасові збої можуть повторити весь запуск і повторно подати той самий `commandBody`.
Це може продублювати збережений prompt користувача **в межах того самого виконання reply**, якщо prompt уже був доданий до того, як спрацювала умова повтору.
Я оцінюю це як менш імовірне, ніж дубльоване приймання `exec.finished`, тому що:
- спостережуваний розрив становив близько 51 секунди, що більше схоже на другий wake/хід, ніж на повтор усередині процесу;
- у звіті вже згадуються повторні збої надсилання повідомлень, а це більше вказує на окремий пізніший хід, ніж на негайний повтор model/runtime.
## Гіпотеза щодо першопричини
Гіпотеза з найвищою впевненістю:
- Завершення `keen-nexus` пройшло через **шлях подій node exec**.
- Той самий `exec.finished` було двічі доставлено до `server-node-events`.
- Gateway прийняв обидва, тому що `enqueueSystemEvent(...)` не виконує дедуплікацію за `contextKey` / `runId`.
- Кожна прийнята подія запускала Heartbeat і інʼєктувалася як хід користувача в PI-транскрипт.
## Запропоноване маленьке точкове виправлення
Якщо потрібне виправлення, найменша, але цінна зміна така:
- зробити так, щоб ідемпотентність exec/system event враховувала `contextKey` протягом короткого горизонту, принаймні для точних повторів `(sessionKey, contextKey, text)`;
- або додати окрему дедуплікацію в `server-node-events` для `exec.finished` з ключем `(sessionKey, runId, kind події)`.
Це безпосередньо блокувало б повторно відтворені дублікати `exec.finished` до того, як вони стануть ходами сесії.