Qwen reliability: Baxia anti-bot solver + tool-call JSON repair + context-file pipeline - #22
Open
firegoaway wants to merge 13 commits into
Open
Qwen reliability: Baxia anti-bot solver + tool-call JSON repair + context-file pipeline#22firegoaway wants to merge 13 commits into
firegoaway wants to merge 13 commits into
Conversation
…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
Author
Было:Стало: |
Author
…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
force-pushed
the
fix/qwen-reliability-and-baxia
branch
from
August 21, 2026 02:56
117a55f to
47422b5
Compare
…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
force-pushed
the
fix/qwen-reliability-and-baxia
branch
from
August 21, 2026 07:09
556e783 to
1f044b1
Compare
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
force-pushed
the
fix/qwen-reliability-and-baxia
branch
from
August 21, 2026 09:00
e13dba9 to
e9ec9a8
Compare
…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.
6 tasks
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Сводка
Дополнение к #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, JSONRGV587+data.url), охлаждение на основе серий, минимальный интервал между POST-запросами/completions, экспоненциальная задержка при пустых потокахbaxia-solver.mjs: локальный решатель для слайдера перетаскивания AWSC nc — не требует внешнего сервиса капчи. Сканирует кадры на наличиеspan.nc_1_n1z, имитирует человеческое перетаскивание (easeOut + дрожание + перелёт), определяет успех по cookiex5secи автоматически сбрасывает охлаждение; при неудаче переходит к охлаждениюbrowser-proxy.mjsзапускает решатель в обоих ветвях наказания (текстовой и потоковой);client.mjsдобавляет слот регулировки перед отправкой запроса и преобразует ошибки наказания/охлажденияQWEN_BAXIA_AUTO_SOLVE=0(отключение решателя),QWEN_PUNISH_COOLDOWN_MS2. Восстановление 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— все инварианты пройденыДополняет #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/completionsstarts returning a Baxia punish page instead of SSE — the source of the "empty stream" cascade.request-pacing.mjs: punish detection (HTML_____tmd_____/punish, JSONRGV587+data.url), streak-based cooldown, min-interval pacing between POST /completions, empty-stream backoffbaxia-solver.mjs: local solver for the AWSC nc drag slider — no external captcha service. Scans frames forspan.nc_1_n1z, drives a human-like drag (easeOut + jitter + overshoot), detects success via thex5seccookie and auto-clears the cooldown; falls back to the cooldown when unsolvedbrowser-proxy.mjsruns the solver on both punish branches (text + stream);client.mjsadds a pacing slot before completion and maps punish/cooldown errorsQWEN_BAXIA_AUTO_SOLVE=0(solver off),QWEN_PUNISH_COOLDOWN_MS2. 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.parsefails 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)parseCallsJson(fenced path) and the streaming salvage chain: strict → brace fix → quote-escape fix → truncation repair3. 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 passImproves #21