chore(i18n): refresh uk translations
This commit is contained in:
parent
24a7c301f5
commit
e98a56b19d
@ -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` до того, як вони стануть ходами сесії.
|
||||
Loading…
Reference in New Issue
Block a user