Skip to content

feat(qwen): a headless Qwen Code worker backend on Alibaba's Token Plan - #8

Draft
yury-procoders wants to merge 105 commits into
procoders:mainfrom
yury-procoders:feat/qwen-backend
Draft

feat(qwen): a headless Qwen Code worker backend on Alibaba's Token Plan#8
yury-procoders wants to merge 105 commits into
procoders:mainfrom
yury-procoders:feat/qwen-backend

Conversation

@yury-procoders

Copy link
Copy Markdown
Contributor

qwen — a headless Qwen Code worker on Alibaba's Token Plan

Adds qwen as an eighth dispatch backend: a Bash-spawned Qwen Code CLI process in its own git
worktree, under the process-group timeout supervisor, with a mandatory kernel sandbox. It is the
only backend besides codex that requires OS-level confinement. Enforcement stays where it already
lives — the caller's git-derived scope gate.

It also earns a second role: qwen becomes a cross-brand advisor, second in priority after
codex (codex > qwen > opus), giving a third independent model family for error decorrelation
alongside Claude and GPT.

Stacked PR. This branches from a local merge of #5 (zai), #6 (tier-model-pool) and #7
(rate-limit-rerouting), all still open. Against main the diff shows all of them; the qwen work
itself is 38 files, +7,734/−143. Review after those three land.

What it adds

  • Worker. scripts/compound-v-run-qwen-worker.sh — worktree isolation, env -i credential
    scrub, sandbox wiring, an ancestor-.env preflight, the git-derived scope gate, and a canonical
    job_result. Worker-only: a qwen reviewer job is rejected by the manifest validator by name.
  • Opt-in gate. qwen is off by default and enforced at validation, not in prose: a manifest
    naming qwen is rejected unless an operator-local acknowledgment record carries a current
    terms-version marker. The record holds an acknowledgment only — never the API key.
  • Advisor arm. Read-only through four independent mechanisms: --approval-mode=plan,
    --safe-mode, an --exclude-tools denylist, and an empty scratch cwd plus a scratch QWEN_HOME,
    so no settings file exists anywhere in the discovery chain for an injected mcpServers to live in.
  • Models. qwen3.8-max (deep, standard) and qwen3.6-flash (light), from a live catalog read.
    glm-5.2 is 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 response
envelope, 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-tools bypasses confirmation rather than restricting tools. --exclude-tools /
    --core-tools are the real levers. The same inversion shipped once before in zai's first draft.
  • QWEN_SANDBOX overrides the --sandbox flag — the published docs claim the reverse — and
    --sandbox is a boolean, not a profile selector. An ambient SANDBOX silently disables sandboxing
    altogether, and cannot be defended by pre-setting it, because setting it is the disable.
  • The response's first element is system/init (which carries model), not session_start.
    There is no sandbox field anywhere, so containment cannot be proven from the payload; the
    guarantee is qwen's own FatalSandboxError instead.
  • A run in which every API call failed still reports subtype:"success", is_error:false,
    exit 0. The worker now detects this and emits a classifiable status: error rather than a silent
    no-op that would have looked like a completed job which built nothing.
  • The plan itself: an earlier draft targeted the Coding Plan throughout. The operator's
    subscription is the Token Plan, and the two share no entitlement.

Known limits

  • Alibaba's Token Plan terms say the plan is "for interactive use with compatible AI programming and
    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 risk
    knowingly, which is why the backend is opt-in and off by default.
  • The credits-per-token ratio is unpublished and is deliberately not estimated anywhere.
  • light → qwen3.6-flash is a naming inference, not a measurement.
  • kimi is not on this plan (kimi-k2.7-code returns HTTP 403); it belongs to the Coding Plan.
  • A one-word prompt costs ~17,000 input tokens — the system preamble plus 64 tool definitions. On a
    credit-metered plan that is real money, and --core-tools is the lever. Measured, not tuned.

Gates

All green on the branch: every scripts/*.py --selftest, shellcheck scripts/*.sh hooks/*.sh, all
six bash suites (test-qwen-worker-stub.sh 96 assertions; the zai suites 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.

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)
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
yury-procoders marked this pull request as draft August 4, 2026 16:00
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