WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-380 · Задача · — · web

P1 [монетизация/чат]: конфликт идемпотентности кошелька рушит ответ — цитаты 16->0, HTTP 200, пользователю ложная причина

Закрыт P1 · важно ведёт: —
Суть
Найдено при разборе батареи ролей (WEB-293) на стенде посадки 57.

ЧТО ПРОИСХОДИТ: девять последовательных запросов chat-global отработали retrieval (chunksDelivered=16), провайдер отработал и был затарифицирован (записи в TokenUsage есть), а затем пост-генерационный дебет упал с WalletIdempotencyConflictError: 'wallet: semantic idempotency mismatch for op_dc61352c9666361121296c5573929b11'. Итог: ответ деградирован, цитаты схлопнуты 16->0 (citationMarkerIntegrity: citationsIn 16, citationsOut 0), клиенту отдан HTTP 200 с общим текстом.

ГДЕ В КОДЕ: walletService.ts:714-743 сверяет по одному idempotencyKey tenant/user/wallet/kind/provider/usage event/accounting day/сумму и бросает конфликт при любом расхождении; llmBilling.ts:684-719 ловит всё, кроме OverdraftLimitError, и помечает billing_debit_failed.

ПОЧЕМУ ВАЖНО: (1) деньги за работу провайдера СПИСАНЫ/учтены ($0.041984 за девять попыток), а пользователь ответа не получил; (2) один и тот же ключ операции переиспользовался на СЕМАНТИЧЕСКИ РАЗНЫХ запросах — это дефект генерации ключа, а не защита; (3) деградация приходит в поле HTTP 200, то есть внешне выглядит успехом.

ДОКАЗАТЕЛЬСТВА: server.log:524-529 и далее восемь идентичных блоков (532-537, 540-545, 548-553, 556-561, 564-569, 572-577, 580-585, 588-593); cases.jsonl:9-17; battery-all.json:851,1028,1205,1382,1559,1736,1913,2090,2267. Кошелёк стенда userId cmsm2q8em0000124iy6czij5j: cashBalanceUsd -0.205977, версия 124.

НЕ ДЕНЬГИ: лимит овердрафта -5, запас был около $4.79 — средств хватало. Пополнение проблему НЕ решает.
Доказательства
[2026-08-26 12:05Z] ПОДТВЕРЖДЕНО ЭКСПЕРИМЕНТОМ: при свежих уникальных ключах операций те же девять запросов проходят нормально — billing.status=billed во всех девяти, degraded нет, цитаты возвращаются (3..16 вместо 0), фактическое списание $0.082499. Это доказывает, что причина деградации — именно переиспользование одного ключа операции на семантически разных запросах, а не нехватка средств и не отказ провайдера. Дефект в генерации ключа идемпотентности.
[2026-08-27 16:05Z] ПРИНЯТ acc380 (строгая монетарная приёмка): conflict-after-success доставляет ответ с цитатами при РОВНО ОДНОМ списании (ledger-пересчёт), replay no-op, genuine debit_failed деградирует как раньше, конфликт ДО провайдера fail-closed (без бесплатных ответов), негативный контроль ловит. -> done, ветка fix380wallet в набор l59.1/l60.
Починено в
cdd6ef3
Воркер
не привязан — привязать: curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \ -d '{"issueId":"WEB-380","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-26T21:03:45.562Z