feat(qwen): a headless Qwen Code worker backend on Alibaba's Token Plan - #8
Draft
yury-procoders wants to merge 105 commits into
Draft
feat(qwen): a headless Qwen Code worker backend on Alibaba's Token Plan#8yury-procoders wants to merge 105 commits into
yury-procoders wants to merge 105 commits into
Conversation
Adds `zai` as a sixth dispatch backend so codex, claude and z.ai can run concurrently on separate quotas. v1 role is worker + rate-limit fallback; never a reviewer, never an arbiter seat. Mechanism: Bash-spawned `claude -p --bare` in a git worktree, under the process-group supervisor, with the dispatcher's provider credentials scrubbed via `env -i` and z.ai's Anthropic-compatible endpoint injected. Enforcement stays the caller's git-derived scope gate. Grounded by live probes (claude 2.1.207, codex-cli 0.144.4, real Coding Plan key): the redirect works, z.ai validates model names, the agentic tool loop completes, `session_id` is a real UUID, and codex cannot reach z.ai at all (it requires the Responses API, which z.ai does not expose).
The first draft's `--allowedTools` list did not do what it claimed. Measured against the real binary: `--tools` decides which tools exist, `--allowedTools` decides which run unprompted. With only the latter the worker had `Bash` (which the draft said was withheld) and no `Write` at all — and under `--bare` `Write` cannot be restored, so a bare worker cannot create a file. Replaces `--bare` with HOME/CLAUDE_CONFIG_DIR redirection to a scratch dir plus `--tools` + `--allowedTools` + `--exclude-dynamic-system-prompt-sections`. That is the only measured configuration that both gives the worker `Write` and keeps the cacheable prefix byte-identical across worktrees: 4 tools, ~3.3k tokens per request, 2.30 credits per job. Also corrected from the audits: real z.ai 429 codes replace "shapes unobserved"; an explicit classifier branch is mandatory or GLM errors silently get OpenAI's needle set; `failure-policy.py` needs a FALLBACK entry or a credit wall halts the run; `parallel-dispatcher.md`'s adapter table was missing from the file list; two cited CI gates do not exist and must be added; `light` stays glm-5-turbo on a head-to-head measurement (16% faster, and glm-4.7 only 7% cheaper, not 32%). Adds the three pre-flight audits and their knowledge-base files.
light is glm-5-turbo on a head-to-head measurement, not on the multiplier table: turbo 8.5s/2.56 credits vs glm-4.7 10.1s/2.38 — 16% faster for 7% more, because glm-4.7 emits ~60% more output and eats its own lower multiplier. Also locks the property that registering a backend does not widen advisor eligibility: zai has no consult adapter and must never be selected.
…o codex rules classify() ends with `else: rules = _CODEX_RULES`, so registering zai without its own branch would classify an unrecognised GLM error with OpenAI's needles — verified: 'please run `codex login`' came back as auth. It is now 'other'. The CLI's --backend choices still omit devin and opencode, which cannot call this script at all. Pre-existing; deliberately untouched here.
…of halting A missing FALLBACK key reads as None, which decide() turns into halt — the first credit wall would have stopped the whole run. Verified before the fix: action=halt. devin and opencode are still missing from this table and still halt. Pre-existing; left untouched here.
… cost The counts come from z.ai's own response, so zai stays OUT of UNMEASURED_BACKENDS. total_cost_usd and modelUsage[*].costUSD are computed from Anthropic's price table for a model that never ran and are never carried — job_result.usage has no cost field to hold one.
…e assertion
The stub test caught two real defects before either could ship:
* the worker never entered the worktree. claude has no --cd equivalent
(contrast codex exec --cd), so it would have edited files in the launcher's
cwd while the gate diffed an untouched worktree — a job that changed nothing
where it was supposed to, waved through as clean.
* a BLOCKED job crashed the worker. The scope gate signals its verdict through
the exit code (0 clean, 1 blocked, 2 gate fault) and the worker treated any
non-zero as fatal, so every out-of-scope write died instead of returning a
blocked job_result.
It also forced a fix in the usage extractor: --output-format json emits one JSON
DOCUMENT, and line-wise parsing silently yields nothing the moment that document
is pretty-printed. It now parses the document first and falls back to JSONL.
The stub test validates argv; the defect that broke the first draft passed every argv assertion because the argv was exactly as intended. This runs the REAL claude binary against a local stub HTTP server — no network, no key, no quota — and asserts the tool array actually sent, the Bearer header, the absence of x-api-key, that git status is out of the cached system block, and that the tools and system blocks are byte-identical across two different worktrees (the property that makes the prefix cacheable across parallel workers). SKIPs cleanly when claude is not on PATH.
The dispatcher's adapter table is load-bearing — without a row nothing maps backend: zai to its worker script. devin and opencode are still absent from that table; pre-existing, not fixed here.
Neither gate existed. shellcheck covered only hooks/*.sh, leaving the worker scripts — the largest shell surface in the repo — with no static analysis at all; and no step ran any scripts/test-*.sh, so every bash-level proof was manual-only. Turning both on found four issues, all in the new zai files, and none in the pre-existing scripts. --read-only now does something real (it empties the allow-file, so a read-only job that writes anything is BLOCKED) instead of being accepted and forgotten.
The new shellcheck gate over scripts/*.sh failed on the runner: it has 0.9.0, which reports SC2015 where 0.11.0 (used locally) stays quiet. Two of the three sites are pre-existing files the gate had never looked at before — which is the point of adding it. Semantics are unchanged in all three: each meant 'if NOT (A and B) then C', which is exactly what SC2015 warns may be misread.
…(PR 2 of 3) Lets one tier name several (backend, model) pairs and hands successive jobs to them in turn, so a run burns three providers' quotas evenly instead of draining one. Purely additive: a config with no pools key behaves exactly as today. Round-robin decides at FIRST dispatch and the choice is frozen into state.json, so /v:resume re-dispatches to the recorded backend rather than re-deriving it — otherwise an interrupted job could come back on a different backend with different isolation and a different worktree. The one exception is a quota failure, where moving the job is the point. Independent of the zai backend; zai just becomes an eligible pool member.
The pool spec is cut from main and linked the zai design doc, which lives only on feat/zai-backend. The gate is line-based and does not respect inline code spans, so even quoting the path in backticks trips it — the branch dependency is now stated in prose instead. Adds the archaeology and library audits.
Four blocking defects in the first draft, two of them mine and inverted:
* the justification for skipping quota-aware balancing named the wrong
backend. z.ai is the MOST measurable of the three; the genuinely blind one
is claude, which this repo already lists in UNMEASURED_BACKENDS. The real
reason is architectural — Compound V never speaks HTTP to a provider, it
reads a CLI's stdout, so no provider header ever reaches the dispatcher.
* the draft would have introduced the policy regression its own Non-goals
promised to avoid: the never-Haiku execution-layer gate fires on a job's
explicit model, and a pool job carries none, so the gate became unreachable.
It now runs on the RESOLVED model.
claude is excluded from pools by default: Anthropic shares usage limits between
Claude and Claude Code, so a claude pool member competes with the operator's own
session. Integer weights (default 1) ship now — they are the only way to express
a reduced share, and weighted rotation is what comparable routers actually do.
Assignment is computed from MANIFEST order, not dispatch order, and the member
list is frozen at run start; an unavailable member is skipped with the counter
still advancing, so a shrinking pool cannot shift later assignments.
Corrected: rate_limited retries the same backend and never reroutes; only
out_of_credits reroutes, and today always to claude — which under a pool would
dump an exhausted run onto the operator's subscription. Availability has no
mechanism in this repo at all (removed in v2.6.2), so the spec now defines it
narrowly instead of assuming it. PR 1 is a declared merge prerequisite.
Adds the domain audit and its knowledge-base file.
Six rounds between the archaeologist and the doc-validator; real errors found in both directions. Two practices came out of it and are now in the knowledge base: report a check's denominator and a negative control that can fail (a broken harness and a clean repo both print zero), and re-run a gate after editing any doc that describes it — explaining a construct requires reproducing it, which is how a dead link landed inside the paragraph warning against dead links.
The allow-list used to be written to a predictable path next to the worktree ($WT.art/write_allowed.globs) before the child started and read back after it exited. The child has Bash + Write and no kernel confinement, so it could overwrite that file before the gate read it — reproduced end to end: an out-of-scope write plus an appended `**` to the allow-file came back as a clean success. The list is now built as repeated --allow arguments in the parent shell and passed directly to compound-v-scope-check.py, which already supported --allow alongside --allow-file. No file on disk sits between the worker and the gate for zai to tamper with. Every other worker still uses the file-based allow- list; the pattern is inherited across all six, and fixing it repo-wide is a separate, larger change.
.claude/compound-v.json pools.balanced: - light: claude (default resolves to sonnet at light) + zai (default glm-5-turbo at light). - standard: claude with an explicit "sonnet" override (balanced's default at standard is opus) + zai (default glm-5.2 at standard). Haiku was considered and explicitly rejected: it benchmarks below every model already in play here (SWE-bench Verified: Sonnet 5 85.2%, Qwen3.7-Max 80.4%, Kimi K2.5 76.8%, Haiku 4.5 73.3% -- weakest of the four), so it would add zero capability while requiring the never-Haiku gate to be relaxed (compound-v-validate-manifest.py + compound-v-pool-state.py, enforced in three places) for no benefit. The actual goal -- keep light/standard work off the Max quota -- is already met by zai's subscription-plan billing. Verified live: freeze_pool_members() with a real ZAI_API_KEY resolves light -> [sonnet, glm-5-turbo] and standard -> [sonnet, glm-5.2], both members available:true, and a two-job (light+standard) pool manifest validates with zero problems end to end.
(cherry picked from commit 5fb4d9a800ae110e2c07068f607648240da8fc5d)
(cherry picked from commit 2337b35d015b6a8336a8f0fac282ec44f6f58d68)
(cherry picked from commit c3b1bc8fb59e1295588d9c43f32044ded06b0174)
…rified base behavior
…lan, keep zai for glm-5.2
Adds qwen as a headless worker backend and cross-brand advisor on Alibaba's Token Plan. Lockstep-bumps plugin.json, marketplace.json and CHANGELOG.md together per the CI cross-check.
yury-procoders
marked this pull request as draft
August 4, 2026 16:00
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.
qwen— a headless Qwen Code worker on Alibaba's Token PlanAdds
qwenas an eighth dispatch backend: a Bash-spawned Qwen Code CLI process in its own gitworktree, under the process-group timeout supervisor, with a mandatory kernel sandbox. It is the
only backend besides
codexthat requires OS-level confinement. Enforcement stays where it alreadylives — the caller's git-derived scope gate.
It also earns a second role:
qwenbecomes a cross-brand advisor, second in priority aftercodex (
codex > qwen > opus), giving a third independent model family for error decorrelationalongside Claude and GPT.
What it adds
scripts/compound-v-run-qwen-worker.sh— worktree isolation,env -icredentialscrub, sandbox wiring, an ancestor-
.envpreflight, the git-derived scope gate, and a canonicaljob_result. Worker-only: aqwenreviewer job is rejected by the manifest validator by name.qwenis off by default and enforced at validation, not in prose: a manifestnaming
qwenis rejected unless an operator-local acknowledgment record carries a currentterms-version marker. The record holds an acknowledgment only — never the API key.
--approval-mode=plan,--safe-mode, an--exclude-toolsdenylist, and an empty scratch cwd plus a scratchQWEN_HOME,so no settings file exists anywhere in the discovery chain for an injected
mcpServersto live in.qwen3.8-max(deep, standard) andqwen3.6-flash(light), from a live catalog read.glm-5.2is on the same plan and reachable by explicit override.Verification
Verified live on 2026-08-04 against qwen 0.21.5 with a real Token Plan key, end to end (exit 0, a
real generation from
qwen3.8-max). Measured: the invocation shape, the auth mechanism, the responseenvelope, and the usage extraction.
Not covered, and stated rather than rounded away: the scope gate, the merge-back and the BLOCKED
path have only ever run against a stub. The failure-classifier needles were derived from a different
endpoint's error bodies and remain unverified here — they fail closed to
other.The read-only guarantee was probed twice, with the target directory empty after each attempt: asked
to create a file → refused; asked to write via shell → refused, and the session reported no shell
tool loaded. Two probes are evidence, not proof — which is why codex still ranks first.
Corrections the live pass forced
Each of these was a documentation claim that measurement overturned. They are the reason the probe
was worth its cost:
--allowed-toolsbypasses confirmation rather than restricting tools.--exclude-tools/--core-toolsare the real levers. The same inversion shipped once before inzai's first draft.QWEN_SANDBOXoverrides the--sandboxflag — the published docs claim the reverse — and--sandboxis a boolean, not a profile selector. An ambientSANDBOXsilently disables sandboxingaltogether, and cannot be defended by pre-setting it, because setting it is the disable.
system/init(which carriesmodel), notsession_start.There is no
sandboxfield anywhere, so containment cannot be proven from the payload; theguarantee is qwen's own
FatalSandboxErrorinstead.subtype:"success",is_error:false,exit 0. The worker now detects this and emits a classifiable
status: errorrather than a silentno-op that would have looked like a completed job which built nothing.
subscription is the Token Plan, and the two share no entitlement.
Known limits
agent tools only. Do not use it for automated scripts or application backends." The ambiguity is
unresolved and documented in full in
adapter-qwen.md; the operator accepts the account riskknowingly, which is why the backend is opt-in and off by default.
light → qwen3.6-flashis a naming inference, not a measurement.kimiis not on this plan (kimi-k2.7-codereturns HTTP 403); it belongs to the Coding Plan.credit-metered plan that is real money, and
--core-toolsis the lever. Measured, not tuned.Gates
All green on the branch: every
scripts/*.py --selftest,shellcheck scripts/*.sh hooks/*.sh, allsix bash suites (
test-qwen-worker-stub.sh96 assertions; thezaisuites unchanged at 44 and 8),version lockstep at 2.19.0, frontmatter lint, 649 intra-repo links with none dead, and all 12
historical manifests still validate.