Skip to content

Qwen reliability: Baxia anti-bot solver + tool-call JSON repair + context-file pipeline - #22

Open
firegoaway wants to merge 13 commits into
Staks-sor:mainfrom
firegoaway:fix/qwen-reliability-and-baxia
Open

Qwen reliability: Baxia anti-bot solver + tool-call JSON repair + context-file pipeline#22
firegoaway wants to merge 13 commits into
Staks-sor:mainfrom
firegoaway:fix/qwen-reliability-and-baxia

Conversation

@firegoaway

@firegoaway firegoaway commented Aug 20, 2026

Copy link
Copy Markdown

Сводка

Дополнение к #21 и исправлениям, уже включённым в v0.4.25. Данный PR добавляет оставшуюся часть работ по повышению надёжности поверх v0.4.25 — разрешается без конфликтов (npm test и CI-инварианты проходят).

1. Антибот Baxia: обнаружение punishment'ов, кулдаун, локальный решатель слайдера (feat(qwen))

Серия из ~10 последовательных агентских потоков (33 инструмента, 60–160 с каждый) повышает фактор риска, и /api/v2/chat/completions начинает возвращать страницу punish Baxia вместо SSE — это источник каскада «пустых потоков».

  • request-pacing.mjs: обнаружение наказания (HTML _____tmd_____/punish, JSON RGV587 + data.url), охлаждение на основе серий, минимальный интервал между POST-запросами /completions, экспоненциальная задержка при пустых потоках
  • baxia-solver.mjs: локальный решатель для слайдера перетаскивания AWSC nc — не требует внешнего сервиса капчи. Сканирует кадры на наличие span.nc_1_n1z, имитирует человеческое перетаскивание (easeOut + дрожание + перелёт), определяет успех по cookie x5sec и автоматически сбрасывает охлаждение; при неудаче переходит к охлаждению
  • browser-proxy.mjs запускает решатель в обоих ветвях наказания (текстовой и потоковой); client.mjs добавляет слот регулировки перед отправкой запроса и преобразует ошибки наказания/охлаждения
  • Переменные окружения: QWEN_BAXIA_AUTO_SOLVE=0 (отключение решателя), QWEN_PUNISH_COOLDOWN_MS

2. Восстановление JSON в вызовах инструментов (fix(api))

Qwen регулярно генерирует команды оболочки с аргументами в кавычках внутри поля "command" без экранирования JSON (наблюдалось 2026-08-21: "grep -n "pattern" file"). JSON.parse завершается ошибкой, и необработанный блок попадает в чат как [Error parsing tool call JSON from model].

  • escapeUnescapedInnerQuotes(): конечный автомат, который экранирует внутреннюю кавычку, если следующий непробельный символ не является структурным символом JSON; также дублирует неверные экранирования, например \| (регулярные выражения grep)
  • Встроено в parseCallsJson (путь с блоком кода) и в цепочку восстановления потоковой обработки: строгий режим → исправление скобок → исправление кавычек → восстановление обрезанных данных

3. Конвейер загрузки context.txt (feat(qwen))

Подсказки (промпты) размером более 32 КБ загружаются на chat.qwen.ai как context.txt (получение токена getstsToken + multipart-запрос), после чего в полезной нагрузке ссылаются на file_id — так промпты агента на 133 КБ символов проходят надёжно, тогда как встроенный текст обрезается.

4. Покрытие тестами (test(qwen))

Тайм-ауты TTFT и деградированных потоков, запас по пропускной способности fetch, восстановление вызовов инструментов без ограждения, исправление кавычек — все тесты используют полезные нагрузки, наблюдаемые в реальной работе.

Тесты

  • npm test — 531 тест, 0 ошибок (2 пропущено)
  • npm run check:ci — все инварианты пройдены
  • Синхронизированы зеркала для Desktop/плагинов (проверено через CI-инварианты)
  • E2E: промпт на 133 КБ через конвейер контекстного файла; наказание → решатель → x5sec → повторная отправка подтверждены в реальной среде

Дополняет #21


Summary

Follow-up to #21 and the fixes you already merged in 0.4.25 — this PR layers the remaining reliability work on top of v0.4.25, resolving cleanly (no conflicts, npm test + CI invariants green).

1. Baxia anti-bot: punish detection, cooldown, local slider solver (feat(qwen))

A burst of ~10 back-to-back agent streams (33 tools, 60-160s each) raises the risk score, and /api/v2/chat/completions starts returning a Baxia punish page instead of SSE — the source of the "empty stream" cascade.

  • request-pacing.mjs: punish detection (HTML _____tmd_____/punish, JSON RGV587 + data.url), streak-based cooldown, min-interval pacing between POST /completions, empty-stream backoff
  • baxia-solver.mjs: local solver for the AWSC nc drag slider — no external captcha service. Scans frames for span.nc_1_n1z, drives a human-like drag (easeOut + jitter + overshoot), detects success via the x5sec cookie and auto-clears the cooldown; falls back to the cooldown when unsolved
  • browser-proxy.mjs runs the solver on both punish branches (text + stream); client.mjs adds a pacing slot before completion and maps punish/cooldown errors
  • Env switches: QWEN_BAXIA_AUTO_SOLVE=0 (solver off), QWEN_PUNISH_COOLDOWN_MS

2. Tool-call JSON repair (fix(api))

Qwen regularly emits shell commands with quoted arguments inside "command" without JSON escaping (observed 2026-08-21: "grep -n "pattern" file"). JSON.parse fails and the raw block leaks into chat as [Error parsing tool call JSON from model].

  • escapeUnescapedInnerQuotes(): state machine that escapes an inner quote when the next non-space char is not a JSON structural char; also doubles invalid escapes like \| (grep regexes)
  • Wired into parseCallsJson (fenced path) and the streaming salvage chain: strict → brace fix → quote-escape fix → truncation repair

3. context.txt upload pipeline (feat(qwen))

Prompts >32KB upload to chat.qwen.ai as context.txt (getstsToken + multipart), then reference the file_id in the payload — 133K-char agent prompts pass reliably where inline text gets truncated.

4. Test coverage (test(qwen))

TTFT/degraded-stream timeouts, fetch headroom, unfenced tool-call salvage, quote repair — all tests use payloads observed in the wild.

Tests

  • npm test — 531 tests, 0 fail (2 skipped)
  • npm run check:ci — all invariants pass
  • Desktop/plugin mirrors synchronized (verified via CI invariants)
  • E2E: 133K prompt through context-file pipeline; punish → solver → x5sec → replay confirmed in the wild

Improves #21

…r solver

- request-pacing.mjs: detect punish responses (HTML _____tmd_____/punish,
  JSON RGV587 + data.url), streak-based cooldown, min-interval pacing
  between POST /completions, empty-stream backoff
- baxia-solver.mjs: local solver for the AWSC nc drag slider (no external
  captcha service): scan frames for span.nc_1_n1z, human-like drag path
  (easeOut + jitter + overshoot), success detection via x5sec cookie;
  auto-clears cooldown on solve, falls back to 600s cooldown when unsolved
- browser-proxy.mjs: run the solver on both punish branches (text + stream)
  before entering cooldown
- client.mjs: pacing slot before completion, punish/cooldown error mapping

Env: QWEN_BAXIA_AUTO_SOLVE=0 disables the solver (cooldown only),
QWEN_PUNISH_COOLDOWN_MS overrides cooldown duration.
- context-file.mjs: upload prompts >32KB to chat.qwen.ai as context.txt
  via /api/v1/files/getstsToken + multipart upload, then reference the
  file_id in the completion payload
- completion-payload.mjs: accept files[] and attach uploaded file ids
- observed in the wild: 133K-char agent prompts pass reliably through
  the file pipeline where inline text gets silently truncated
Qwen regularly emits shell commands with quoted arguments inside
"command" without JSON escaping (observed 2026-08-21, e.g.
"grep -n "pattern" file"). JSON.parse fails, the raw block leaks
into chat as [Error parsing tool call JSON from model].

- escapeUnescapedInnerQuotes(): state machine that escapes a quote
  inside a string when the next non-space char is not a JSON structural
  char (, : } ] or EOF); also doubles invalid escapes like \| which
  the model uses for grep regexes
- wired into parseCallsJson (fenced path) and the streaming salvage
  chain in openai-handler: strict -> brace fix -> QUOTE-ESCAPE FIX ->
  truncation repair (log: 'after quote-escape fix')
- tests use the exact payloads observed in the wild
…ector

- qwen-degraded-ttfp.test.mjs: firstContentMs default 240s tolerates
  degraded-Qwen TTFT (observed 118-186s), explicit overrides respected
- qwen-stream-timeouts.test.mjs: assert 600s fetch headroom for long
  agent generations (250-320s observed) + explicit override
- qwen-bare-toolcalls.test.mjs: unfenced tool-call JSON in prose is
  salvaged, prose without tool structure is passed through untouched
- session-errors: clarify that login-page HTML is authoritative
@firegoaway

firegoaway commented Aug 20, 2026

Copy link
Copy Markdown
Author

Было:

╭───────────────────────────────────── Previous Conversation ──────────────────────────────────────╮
│   ● You: Проверь доступность всех mcp серверов                                                   │
│   ◆ Hermes: Tool terminal does not exists.Tool terminal does not exists.Tool execute_code does   │
│ not exists.Tool tool_describe does not exists.Tool tool_search does not exists.Tool skill_view   │
│ does not exists.Tool m...                                                                        │
│   ● You: retry                                                                                   │
│   ◆ Hermes: Tool terminal does not exists.Tool terminal does not exists.Tool tool_search does    │
│ not exists.Tool web_extract does not exists.Tool browser_navigate does not exists.К сожалению, в │
│ этой сессии работает ТОЛЬКО инструмент web_search. Все остальные (terminal, execute_code,        │
│ browser, web_extract, tool_search) возвращают "Tool does not exists". Это ограничение текущего   │
│ рантайма, а не моя ошибка.                                                                       │
│                                                                                                  │
│             Поэтому проверить доступность MCP серверов в реальном времени я НЕ МОГУ — нет        │
│ доступа ни к терминалу, ни к HTTP-клиенту, ни к вызову MCP-инструментов.                         │
│                                                                                                  │
│             Что я могу сказать по данным из памяти и конфигурации:                               │
│                                                                                                  │
│             НАСТРОЕННЫЕ MCP СЕРВЕРЫ (по каталогу deferred tools):                                │
│             1. cua-driver     — 50 инструментов (desktop automation)                             │
│             2. ghidra-mcp     — 34 инструмента (reverse engineering)                             │
│             3. hexstrike-ai   — 155 инструментов (pentest/recon)                                 │
│             4. reva           — 90 инструментов (binary analysis)                                │
│             5. spider-mcp     — 4 инструмента (web scraping), путь D:\tmp\spider-mcp             │
│             6. video          — 1 инструмент (video_analyze)                                     │
│             7. zap-mcp        — 21 инструмент, адрес https://127.0.0.1:8282/mcp                  │
│                                                                                                  │
│             То, что они перечислены в каталоге, означает что Hermes их ЗАРЕГИСТРИРОВАЛ при       │
│ старте. Но живы ли процессы прямо сейчас — без терминала не проверить.                           │
│                                                                                                  │
│             Для самостоятельной проверки выполни в CLI:                                          │
│                                                                                                  │
│               hermes mcp status                                                                  │
│                                                                                                  │
│             Или проверь порты/процессы вручную:                                                  │
│                                                                                                  │
│               netstat -ano | findstr "8282"                                                      │
│               tasklist | findstr /i "node java python"                                           │
│                                                                                                  │
│             Для ZAP конкретно:                                                                   │
│               curl -sk https://127.0.0.1:8282/mcp                                                │
│                                                                                                  │
│             Если нужна полная диагностика — открой новую сессию где terminal доступен, или       │
│ перезапусти Hermes с полным набором инструментов. В текущей сессии я могу только искать          │
│ информацию в интернете.

Стало:

(та же сессия, я просто прошу его повторить попытку)

● retry and continue
Initializing agent...

────────────────────────────────────────
  ┊ ⚡ preparing tool_search…
  ┊ 💻 preparing terminal…
  ┊ 💻 preparing terminal…
  ┊ ⚡ tool_sear mcp  0.0s
  ┊ 💻 $         tasklist | grep -i "node\|python\|java"  1.3s
  ┊ 💻 $         netstat -ano | grep -E "8282|LISTENING"  0.2s
  ┊ 💻 preparing terminal…
  ┊ 💻 preparing terminal…
  ┊ ⚡ preparing tool_call…
  ┊ ⚡ preparing tool_call…
  ┊ 💻 $         tasklist | grep -i -E "node|python|java|zap|spider"  0.4s
  ┊ 💻 $         netstat -ano | grep 8282  0.2s
  ┊ ⚡ mcp__ghid   0.0s
  ┊ ⚡ mcp__spid   2.7s
  ┊ 💻 preparing terminal…
  ┊ ⚡ preparing tool_call…
  ┊ ⚡ preparing tool_call…
  ┊ ⚡ preparing tool_call…
  ┊ ⚡ preparing tool_call…
  ┊ 💻 $         cmd.exe //c "tasklist" | grep -i -E "node|python|java|zap|spider|cua|ghidra|reva|he
x"  0.4s
  ┊ ⚡ mcp__reva   0.0s
  ┊ ⚡ mcp__hexs   0.1s
  ┊ ⚡ mcp__ghid   2.1s
  ┊ ⚡ mcp__cua_   1.1s
  ⏳ waiting on qwen3.8-max — 30s with no output yet (provider may be slow or overloaded, or the m
del is thinking; auto-reconnect at 900s)
(тут небольшая пауза)
и дальше он спокойно продолжает как ни в чём ни бывало.

@firegoaway

Copy link
Copy Markdown
Author

Прокси успешно запихивает контекст свыше 100 тыс. токенов целиком в виде txt, заполняя основной input field чисто системным промптом.

image

…rsed as tool calls

Degraded Qwen emits reasoning as literal text with <think> tags directly
in the text channel (instead of a separate phase:"think" SSE field).
The reasoning contains draft tool-call JSON ('Wait, I'll use execute_code...'
+ {"name": ...}) which findBareToolStart and the fence detector picked
up as REAL calls — producing the 'Tool X does not exists' cascade in
Hermes and visible 'Thinking completed' separators in chat.

- think-filter.mjs: stripThinkBlocks() (non-stream) + createThinkTagFilter()
  (streaming wrapper with a small hold buffer so tags split across chunks
  do not leak; handles unclosed <think>, partial tag prefixes and the
  'Thinking completed' separator line)
- Qwen stream path: wrap parser.onText in the filter, flush before onEnd
- parseModelToolCallsSafe(): parseModelToolCalls(stripThinkBlocks(text)),
  used by all three non-stream call sites (responses, chat.completions,
  anthropic)
- tests use the exact payloads observed in the wild
Qwen regularly interleaves think/answer phases several times per response
(observed with dense contexts through the proxy): think -> short answer ->
think -> short answer -> ... -> final answer.

- the filter is a state machine, so interleaved phases pass through any
  number of switches in both normal and degraded (literal-tag) modes
- new: strip stray </think> closers that appear without an opening tag
  (degraded streams sometimes lose the opener); previously they leaked
  into the visible answer as literal '</think>' text
- streaming wrapper now holds partial prefixes of BOTH tags ('<th..' and
  '</th..') so tags split across chunks never leak
- tests: interleaved think/answer x3 phases, stray closers via strip and
  streaming paths, chunk-split closers
…ry harvest

- Track response_id + terminal markers ([DONE], finish_reason,
  delta.status=finished) in the incremental SSE parser; flag streams
  that die mid-generation as truncated instead of silently returning
  partial text as success.
- Add recoverTruncatedQwenStream: on truncation, re-attach via an
  empty POST /completions?chat_id=..&response_id=.. (server continues
  the same SSE response; verified from web-UI HAR) up to
  QWEN_RESUME_MAX_ATTEMPTS times, then harvest the finished answer
  from GET /api/v2/chats/{id} history (new harvest.mjs module).
- Disable blind re-POST of the full completion body: browser-proxy no
  longer retries a POST after stream chunks were received, and client
  no longer re-throws auth failure when recovery already collected
  text. Blind re-POSTs created sibling branches (2/2, 3/4) in the web
  UI and duplicated content.
- Treat function tools named web_search as CLIENT tools, not native
  provider search: matching them enabled auto_search + a lying
  'Web search is enabled' prompt prefix, making the model call its
  internal search tool and cascade 'Tool web search does not exist'.
  Hosted tool types (web_search_20250305 etc.) still enable provider
  search. Fixes tool-hallucination loops seen via Hermes Gateway.
- New proxyApiGet for same-origin GETs (chat history).
- Tests: parser lifecycle (terminal markers, truncation detection),
  continuation budget, harvest pagination/failure modes, end-to-end
  recovery pipeline with injected proxy, native-search detection
  (567 tests, 0 fail).
@firegoaway
firegoaway force-pushed the fix/qwen-reliability-and-baxia branch from 117a55f to 47422b5 Compare August 21, 2026 02:56
…ctions

Reasoning models (qwen3-max etc.) read the [TOOL INSTRUCTIONS] prompt
block and attempt to invoke the listed tools through their INTERNAL
tool-call mechanism during the thinking phase. The Qwen backend has no
such tools registered, so every attempt fails ('Tool execute_code does
not exists', 'Tool write_file does not exists', ...) and the model
cascades through every tool name until the turn dies. Reproduced in
the native web UI (2026-08-21): the model believes the tool-execution
system exists and keeps trying natively between reasoning phases.

- Add a CRITICAL 'HOW TOOLS ARE EXECUTED HERE' preamble to the tool
  instructions: no native/internal tool execution exists, the only way
  to run a tool is a tool_calls markdown block in the FINAL visible
  answer, never inside thinking.
- Apply the ban to every model with tools, not only reasoner-named
  ones (qwen3-max does not match /reason|r1|qwq|expert/).
- Keep the plain 'no tools -> no tool instructions' behavior intact.
- Tests: thinking-tool ban present for standard and expert mappings,
  absent when client sends no tools.
… Yes

After two successful tool calls the model degraded to replying with a
bare 'Yes' to the user's 'retry and continue' message several times in
a row (seen in web UI, 2026-08-21). Root cause: when the transcript
ends with a [TOOL RESULT], the prompt carried only the generic SYSTEM
REMINDER about the tool_calls format — no directive to continue the
task. The model literally answered the last user message as a yes/no
question instead of resuming work.

Append a [TASK IN PROGRESS - CONTINUE NOW] directive when the last
message is a tool result: the user's instruction still stands, the
work is unfinished, continue now with the next tool_calls block or
(only if truly done) the final answer. Bare acknowledgements (Yes/OK/
Done) are explicitly forbidden as answers.
@firegoaway
firegoaway force-pushed the fix/qwen-reliability-and-baxia branch from 556e783 to 1f044b1 Compare August 21, 2026 07:09
User field report (2026-08-21): with a 328K-char Hermes conversation
compacted into context.txt, the tool block (full JSON schemas with
multi-KB descriptions) still duplicated large parts of the Hermes
system prompt already present in the attachment, inflating every
request and contributing to model confusion (bare 'Yes' replies).

- formatCompactTools: cap tool descriptions at 200 chars and property
  descriptions at 100 chars (word-boundary ellipsis). Names, types,
  required arrays, enums and defaults are preserved verbatim. Measured
  on real Hermes tool schemas: 75.8% size reduction (7746 -> 1876
  bytes on a 4-tool sample; ~35 tools in practice).
- Context-file note now instructs the model: read the attachment,
  internalize task and context, briefly restate intent (1-2
  sentences), continue the work strictly following the attachment's
  instructions, and never answer only the last message. Also mentions
  that Qwen may rename context.txt (e.g. hash_Pasted_Text_....txt)
  and to rely on the attachment, not the filename.
@firegoaway
firegoaway force-pushed the fix/qwen-reliability-and-baxia branch from e13dba9 to e9ec9a8 Compare August 21, 2026 09:00
…tput

Field logs (2026-08-21): the degraded Qwen backend emits literal
'Tool <name> does not exists.' (and 'does not exist') error chips
directly into the content channel when the model attempts internal
tool calls mid-thinking. These leaked into the visible Hermes
response as concatenated chip runs before any real text.

- stripToolErrorChips / createToolErrorChipFilter in think-filter.mjs:
  regex scrubber for non-stream and stream paths, with chunk-boundary
  hold buffer so chips split across SSE chunks are still caught.
  Normal prose about tools is preserved (matches only the exact chip
  pattern: optional leading space, 'Tool', tool name, 'does not
  exist(s)', optional trailing dot).
- Wired into the streaming pipeline after the think-tag filter and
  into parseModelToolCallsSafe for non-stream responses.

Note: real fix remains the prompt-level ban (3812542); this scrubber
is the safety net for the residual chip leakage observed in field
logs.
…id-round

Field incident (2026-08-21, chat 'Qwen Account Pool Implementation',
export 1787311091624): AbortError BodyStreamBuffer / net::ERR_ABORTED
struck BEFORE the first SSE chunk arrived in Node. The POST was
delivered — the server generated a full answer (16 content chunks,
done). But the #completionRound catch block blindly re-POSTed the same
body (same timestamp_ms) into the same chat: a second user message
appeared, a sibling branch '1/2' hung under the agent's request, and
only answer Staks-sor#2 reached the user while answer Staks-sor#1 was lost.

Same disease we cured in browser-proxy's runProxyFetchStream, one
level higher — the client retry loop.

- harvestLatestAssistantMessage / harvestTransportFailedCompletion in
  harvest.mjs: poll chat history (GET /api/v2/chats/{id}) for a NEW
  finished assistant message; wait for in-progress generation to
  complete; emit only the un-streamed tail via onText.
- #completionRound catch: on transient transport error AFTER the POST
  went out, harvest first; re-POST only when history proves the POST
  was never delivered (no new messages after two polls) or the proxy
  is dead.
- createQwenIncrementalParser.snapshot(): current visible text, used
  to compute the missing tail.

Full suite: 591 tests, 589 pass, 0 fail.
… key, idle backoff

Field reports (2026-08-23/24, kilocode over 'npm run api', port 4318):

1. EMPTY_UPSTREAM_STREAM after a series of successful tool calls.
   The server accepted the completion POST but emitted no SSE chunk
   within firstContentMs (240s on degraded days) - while generation
   often KEEPS RUNNING server-side (the 2026-08-21 incident proved
   answers get saved to chat history). New: before throwing
   EMPTY_UPSTREAM_STREAM, #completionRound polls chat history via
   harvestAfterFirstContentTimeout -> harvestTransportFailedCompletion
   and returns the saved answer; the error only surfaces when the
   server truly produced nothing.

2. '[Error parsing tool call JSON from model]': degraded Qwen drops
   the "arguments" key and inlines the argument object right after
   the name: {"name": "read_file", {"path": ...}} - invalid JSON,
   the whole tool_calls block fell through as prose and the turn
   was lost. New repairMissingArgumentsKey inserts the missing key;
   wired into parseCallsJson, extractBareToolCallsArray and
   extractBareToolCalls repair chains.

3. Retries hammered a degraded upstream instantly. runWithEmptyStreamRetry
   now waits exponential backoff (1s, 2s... capped at 30s; injectable
   backoffBaseMs for tests) between empty-stream attempts.

4. idleMs default 90s -> 360s: healthy Qwen reasoning phases between
   visible deltas can exceed 90s on degraded days.

Full suite: 607 tests, 605 pass, 0 fail.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant