qwen-code/docs/developers
jinye 2eb5cd6df5
feat(serve): observe daemon and child memory against real denominators (#8423)
* feat(serve): observe daemon memory pressure against a real denominator

The daemon samples its own RSS and heap but has nothing to divide them
by, so nothing in `/daemon/status` says whether a figure is fine or
nearly fatal. #8245 landed the denominator (`limits.memory`); this turns
it into a reading.

`runtime.memory.pressure` reports `level`, `ratio`, `source`, and the six
raw figures behind them. The level is the worse of two independent
ratios, because the two failure modes are independent: a container dies
by RSS against its cgroup limit, while a process on a large host can
exhaust V8's heap long before RSS is a meaningful fraction of the
machine. Reporting only one hides whichever failure the deployment is
actually heading for. `source` names which ratio produced the level, and
`unknown` says the daemon could not measure itself — which a consumer
must not read as healthy.

The denominator is `availableMemoryMb`, not `effectiveBudgetMb`: pressure
asks how close this process is to being killed, and what kills it is the
cgroup limit or host memory. An operator's budget is a policy number, so
classifying against it would report `critical` for a daemon in no danger.

`--memory-pressure-mode` is `off | observe`, default `observe`. Both
modes report every figure; only `observe` also raises the
`daemon_memory_pressure` warning, so `off` leaves the top-level `status`
rollup untouched — the thresholds are inherited from an interactive-CLI
monitor and are not yet calibrated for a long-running daemon, and a
deployment that alerts on `status` needs the reading without the verdict.
There is deliberately no `enforce`: nothing here remediates, and a value
a caller can pass but never use is a dead switch. It arrives with the
enforcement.

Scope is the daemon root process only. `childRssCoverage` still reads
`primary_only` and says so on the wire; aggregate child RSS and channel
workers are separate measurements and land separately.

Severity is `warning` at every level including `critical`, because
`error` would make `rollupStatus` return `error` for the whole daemon —
too strong a claim to stake on uncalibrated thresholds.

Refs #8051.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(serve): report aggregate ACP child RSS, not just the primary's (#8462)

* test(serve): close the under-determined assertions review probed

The automated review mutation-probed this diff and found several
assertions that were live but under-determined — each mutant it names
kept the whole suite green. All confirmed locally, and all now fail:

- Deleting `level !== 'normal'` from the issue gate raised
  daemon_memory_pressure on a healthy daemon and flipped top-level
  status to warning on every response — the exact false positive
  `--memory-pressure-mode off` exists to opt out of. Now covered on both
  sides: nothing raised at a realistic denominator, exactly one warning
  at a denominator sized to land this process in `soft`.
- Summing children over `list()` instead of `listManaged()` dropped a
  draining-but-process-holding workspace while `activeAcpChildren` still
  counted it. The draining bridge now reports RSS, so the byte count can
  only come from that child.
- The message's denominator ternary had no coverage; inverting it sent
  an operator hunting RSS growth during a heap-driven incident.
- A truthiness guard on `ageMs` turned a measured-fresh reading (age
  exactly 0, when a status read lands in the sampler's millisecond) into
  `null`, which the field's own docs say never means fresh.
- The multi-contributor age test listed ages ascending, so a
  plain-overwrite accumulator produced the same answer as Math.max.
  Reordered descending, which kills last-wins and first-wins both.

Two declaration-only hunks — the issue-code union member and the
`pressure` field — were guarded by tsc alone, which vitest does not run.
Both are now pinned at runtime by asserting the code string and the full
key set.

Also fixes a real display defect: `toFixed(0)` renders a ratio of 0.795
as "hard at 80%", and 80% is critical's documented threshold. One
decimal, so the number and the level cannot contradict each other.

And corrects a JSDoc claim of mine that was simply wrong: `pressure` is
absent not only for direct-embed but on the bootstrap /daemon/status
route, which omits runtime.memory wholesale even though the budget is
resolved — and that window is not just startup, since a daemon whose
runtime fails to start serves the bootstrap app for its lifetime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(serve): model a per-child heap partition of the daemon budget (#8508)

* feat(serve): add the child-heap admission primitives, unwired

Groundwork for #8182 step 2. Nothing calls any of this yet, so no child
is sized differently and no spawn is refused.

`ProcessRegistry.committedProcessCount` counts attached children plus
reservations that have not attached. That is the figure admission has to
key on: `reserve()` inserts its token synchronously before `spawn()`, so
two racing spawns each see the other, while neither appears in
`activeProcessCount` until its child attaches. A child leaves the count
on exit rather than when `terminate()` starts, so a channel swap counts
twice while the old process winds down — deliberate, since its memory is
still resident.

`getAcpMemoryArgs(explicitMb?)` takes an optional share that bypasses
both the module cache and the raise-only guard. Both bypasses are
load-bearing. The cache, because the share depends on how many children
are live now rather than on the host. The guard, because a
budget-derived share is normally *below* the daemon's own heap limit, so
routing it through `targetMB > currentLimitMB` would drop the flag,
silently restore the overcommit, and leave every test green — the trap
against a multi-GB runner, and mutation-checking it by reinstating the
guard fails two tests.

`createChildHeapPolicy` holds the mode, the budget, and the would-be
refusal counter, and answers `decide(concurrentChildren)`. The refusal
is derived from the unclamped quotient, not from
`recommendedChildShareMb`, because that function clamps *up* to the
512 MB floor: past the point where the pool stops covering the count its
answer saturates and can no longer distinguish "barely does not fit"
from "wildly does not fit".

`ChildHeapPoolExhaustedError` with both transport mappings — REST 503
with Retry-After, ACP `child_heap_pool_exhausted` — added together,
since the two mappings are hand-written and drift silently otherwise.
Refusing at spawn rather than at registration is the correction #8182
demands: registration allocates nothing, so this surfaces as "no new
session in this workspace right now", which is true and retryable.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(serve): size each ACP child by concurrently live children

Wires the primitives from the previous commit into the spawn path, behind
`--child-heap-mode off | observe | enforce`, default `observe`.

Under `enforce` a child's `--max-old-space-size` is a share of the child
pool divided by the children concurrently committed at the moment it
spawns — read from the shared ProcessRegistry after `reserve()`, so two
racing spawns each see the other. When the pool cannot cover another
child at the 512 MB floor the spawn is refused with
ChildHeapPoolExhaustedError, which is what turns a per-child ceiling into
an aggregate bound: concurrent children can never exceed pool/512.

Keyed on concurrency, never on registrations. A dormant workspace has no
child, so it costs nothing — the specific correction #8182 records
against the withdrawn proposal, which would have shrunk a lone live child
to 614 MB because of 24 idle registrations.

Default `observe` computes the share and the admission decision and
applies neither, counting the refusals that would have happened. The
divisor has never been checked against a real multi-workspace deployment,
and a non-zero count is how an operator learns enforcement would have
broken them without being broken. It also catches the case worth
worrying about: a channel swap counts the dying child alongside its
replacement, so on a saturated pool enforcement could refuse a restart
and leave that workspace with no child at all. Excluding terminating
children would authorise real overcommit to dodge a hypothetical
refusal, so the count reports it instead.

Ceilings already granted are not revisited — V8 cannot lower them — so
granted ceilings transiently exceed the pool. Acceptable: the flag is a
ceiling, not a reservation, and a workspace with no live sessions has no
child and picks up the current share on its next spawn.

`limits.memory.enforced` stops being a required literal `false`. #8245
made it one so a client could never mistake that namespace for
enforcement that had not shipped; it has now, so the field is a boolean
derived from the mode — and stays `false` under `observe`, which applies
nothing.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(serve): correct the claims child-heap enforcement makes false

Two sentences in the protocol doc described the memory section as
unconditionally observational: "a required `enforced: false`", and "no
child spawn argument derives from these values, and no request is
refused on their basis". Both are false under `--child-heap-mode
enforce`, so both are rewritten rather than left to rot — `enforced` is
now documented as the boolean that answers exactly this, and the refusal
is documented with its wire shape on both transports.

Also documents `childHeap.refusals` as the calibration signal, since a
would-be-refusal count is useless if operators do not know to read it
before switching to `enforce`; the flag row in the three operator docs;
and the design doc's Part 1, which listed applying a share as a
compatibility risk without recording how that was resolved.

The end-to-end test asserts the policy reaches a real booted daemon's
status with `enforced: false` under the default mode — the wire type in
that test is a hand-written mirror, so its `enforced: false` literal had
to widen too, which is the check that caught the type not being widened
everywhere.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(serve): cover both branches of the enforced tripwire

`enforced` was only ever asserted false — the unit tests build no policy
and the end-to-end daemon runs the default `observe` mode, so the branch
that makes the field worth having was untested. Hardcoding it back to
`false` passed everything.

Also pins `childHeap: null` as distinct from a policy in `off` mode: the
first says no policy exists (direct-embed, or the bootstrap window before
the runtime is built), the second says one exists and computes nothing.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): partition the child pool so granted ceilings stay inside it

Review was right that the previous design did not deliver the aggregate
bound it claimed. Sizing each child by the count live at *its* spawn
bounds the child count but not the memory: V8 cannot lower a running
child's ceiling, so grants accumulate as P + P/2 + P/3 + ... = P x H(n).
Reproduced exactly — 9557 MB authorised against a 3687 MB pool at seven
children on an 8 GB host, and 61355 MB against 15360 MB at the limit on
32 GB. That is 2.6x and 4x the pool, which is what the policy exists to
prevent.

Grant accounting alone does not fix it: the first child would take the
whole pool and the second would be refused immediately. Keeping the
invariant requires early children not to receive the whole pool, so the
ceiling is now a fixed partition — childPoolMb / maxConcurrentChildren,
constant for every child, with maxConcurrentChildren itself derived from
the pool and capped at MAX_DAEMON_WORKSPACES. The sum is then
n x ceiling <= pool by construction, with no ledger of outstanding
grants and no dependence on arrival order. Tested as an invariant across
four host sizes: fill the daemon to its admission limit and the
authorised total still fits.

The cost is deliberate and now documented rather than hidden: a lone
workspace on a 32 GB host gets 614 MB rather than the pool, because any
child may still be running when the house fills. An 8 GB host admits
seven concurrent children at 526 MB each.

Also from review:

- The policy is no longer built for an injected `deps.bridge`. That
  bridge carries its own channel and never reaches the factory the
  policy rides on, so status could report `enforced: true` while nothing
  was being sized.
- Both transport mappings now have direct tests. They are hand-written
  beside each other and drift silently; the spawn-policy tests cannot
  catch a wire regression.
- Swept the "does not size any child" claim, which enforce makes false,
  out of the CLI help text, ServeOptions docs, the two operator tables,
  and the e2e header comment. The 17-configuration table realigns
  wholesale because that cell was its widest — whitespace only.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(serve): model the child heap partition, defer applying it

Review established that the refusal counter cannot tell an operator
whether enforcement is safe, and that is the ground the enforcing mode
stood on. While observing, children run on the host-derived ceiling
(16384 MB on a 32 GB host), so a workload needing 2 GB of old space is
healthy with zero refusals and OOMs the moment a 614 MB partition is
applied. The counter measures admission pressure, not ceiling adequacy.

Rather than ship a switch with no safe way to decide when to turn it on,
`enforce` is removed. `--child-heap-mode` is `off | observe`, and the
mode that would apply the partition arrives with the measurement that
justifies it: peak old-space per child, compared against the modeled
ceiling. That is a real measurement chain — the child reports rss and
cpu today, and `--max-old-space-size` bounds old space specifically, so
neither rss nor heapUsed answers the question.

With nothing applying the partition, the machinery that existed only to
apply it goes too rather than shipping unreachable:
`getAcpMemoryArgs(explicitMb?)`, `ChildHeapPoolExhaustedError` and both
transport mappings, and `limits.memory.enforced` reverts to the required
literal `false` it was before. The spawn path is untouched again; the
factory asks the policy what it would decide purely so the count is
real.

Also fixes the zero-pool defect review found, which the removed clamp
caused: forcing at least one admissible child on a 512 MB host — where
the root reserve consumes the whole 256 MB budget — produced a ceiling
of 0, and `--max-old-space-size=0` is V8's *default* heap, not a zero
ceiling. A pool that cannot cover one child at the floor now reports
`maxConcurrentChildren: 0` and `perChildCeilingMb: null`, and the test
that enshrined the old behaviour is inverted.

Status now publishes `maxConcurrentChildren` and `perChildCeilingMb`, so
an operator can judge the partition against their own workload — the
substitute for a counter that cannot judge it for them. Every claim that
a zero refusal count means the partition is safe to apply is removed
from the flag help, the operator docs, the protocol doc, and the design
doc.

Refs #8182.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): repair the child-heap assertion and the reservation leak

Three findings review raised against #8508 after the partition became
observation-only, all still live on this branch now that it has merged.

The status assertion in `run-qwen-serve.test.ts` failed on head: it used
`toEqual` against `{ mode, refusals }` while the wire also carries
`maxConcurrentChildren` and `perChildCeilingMb`, so the suite was red at
217 passed / 1 failed. The local type restating the wire shape was short
the same two fields. Both are filled in, and the assertion stays `toEqual`
so an unannounced field still fails it — the two derived figures get
matchers because this suite boots a real daemon and the pool follows the
machine. What they have to satisfy is now pinned separately: a fixed
ceiling times the number admitted must fit inside the pool it partitions,
which is the whole reason the partition bounds anything.

`decide()` and `getAcpMemoryArgs()` ran between `reserve()` and the `try`
that cancels the reservation. `childHeapPolicy` is a public
`createSpawnChannelFactory` option, so `decide()` is caller code and may
throw; the spawn then rejected with the token held for the process
lifetime, inflating `committedProcessCount` for every later spawn. Both
calls move inside the `try`. The regression test is mutation-verified —
reverting the move gives `expected 1 to be +0`.

`ServeOptions.memoryBudgetMb` still promised a `childHeapMode: 'enforce'`
that sizes children and refuses spawns. No such mode exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): report no child-heap partition under `off`

`snapshot()` returned `maxConcurrentChildren` and `perChildCeilingMb`
unconditionally, so a daemon run with `--child-heap-mode off` still
published a partition — 7 children at 526 MB on an 8 GB host — under a
mode whose documentation says "do not model it". Review raised it, and it
mattered more than it looked: with `enforce` gone, `off` and `observe`
differed only in whether `refusals` incremented, so nothing on the wire
distinguished a model that was switched off from one in force.

Both figures are now `null` under `off`, which required widening
`maxConcurrentChildren` to `number | null` in the daemon type and the SDK
mirror. `null` rather than `0`: zero is already the computed answer for a
pool too small to host one child at the 512 MB floor, and collapsing the
two would tell an operator who disabled the model that their host cannot
run anything. That leaves three distinguishable states — no policy at all
(`childHeap: null`), a policy modeling nothing (`mode: 'off'` with null
figures), and a live model — and each now has a test.

The `off` unit test previously asserted only `refusals`, so its name
("models nothing at all when off") promised more than it checked. It now
covers the figures, with a sibling test pinning 7 / 526 under `observe` on
the same budget so nulling them unconditionally cannot satisfy both.
Mutation-verified in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): never model a child heap ceiling below the documented minimum

`perChildCeilingMb` is `min(floor(pool / maxConcurrentChildren),
legacyChildCeilingMb)`. The first term is at least `MIN_CHILD_HEAP_MB` by
construction; the second is `floor(available / 2)` and is not, so the
`Math.min` could publish a ceiling *below* the `minChildHeapMb` sitting beside
it in the same snapshot:

    avail=768  --memory-budget-mb 1024  pool=512  legacyCeil=384  perChild=384
    avail=1023 --memory-budget-mb 1024  pool=767  legacyCeil=511  perChild=511

Unreachable from a derived budget — the pool reaches 0 first — but an explicit
budget has a floor of 1024 while available memory does not, and
`docs/users/qwen-serve.md` tells operators on exactly these hosts to pass that
flag. The documented remedy is what reaches the band.

Refuse the model rather than shrink under the floor, with
`maxConcurrentChildren` zeroed in lockstep: a ceiling no child may run at is
not a partition, and "one child fits" beside a null ceiling is the same
contradiction from the other side. Nothing is applied today so the impact was a
wrong published figure, but this is the number the partition asks to be judged
by and the one an `enforce` mode would hand to `--max-old-space-size`.

The existing matrix resolves derived budgets only, which is why the mutation
sweep came back clean; add the `budgetMb` axis, asserting in each case the
shape that makes it reachable, and pin the inclusive boundary (1024/1024 ->
one child at 512) so nulling unconditionally cannot pass instead.

Also, in the same review pass:

- Split usable-gauge handling into numerator and denominator. Coercing an
  unusable numerator to 0 published `rssBytes: 0, rssRatio: 0, level: 'normal',
  source: 'rss'` — a daemon that measured nothing, indistinguishable from an
  idle one, which is the confusion `source: 'unknown'` and `sampled: 0` exist
  to prevent everywhere else here. An unusable numerator now retires its own
  side. Zero stays a reading for a numerator and not for a denominator.
- Document that `rssRatio` divides by host total under
  `availableMemorySource: 'host'`, so it is a lower bound on real pressure
  there — a denominator problem no threshold calibration addresses.
- Document that `refusals` counts channel swaps at full occupancy (the
  terminating child is counted until it exits) and equals the total spawn count
  on a host too small to model a partition. Deliberately not fixed by giving
  the comparison swap headroom, which would admit a 26th ceiling against a
  25-child pool.
- Keep the sampler's rejection handler as a documented backstop — the shipped
  `refreshChildResource` never rejects, but it is an optional `async` interface
  member, so a foreign implementation throwing early would otherwise surface as
  an unhandled rejection — and give it the workspace so it is attributable
  across the fan-out.
- Test hygiene: drop a duplicated `enforced` assertion; replace a host-
  dependent `expect.any(Number)` with a key-set pin plus a branch, since a
  small host now legitimately reports no partition; use `vi.spyOn(Date, 'now')`
  over direct assignment; reuse the exported `ChildHeapMode` on the child-heap
  side, leaving the independent `memoryPressureMode` switch alone.

Reported by @wenshao.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-07 06:10:37 +00:00
..
daemon feat(serve): observe daemon and child memory against real denominators (#8423) 2026-08-07 06:10:37 +00:00
daemon-client-adapters docs(serve): Close multi-workspace hardening gaps (#7019) 2026-07-16 17:33:53 +00:00
daemon-ui feat(web-shell): Add sidebar customization API for branding, navigation, session actions, and footer (#7379) 2026-07-21 11:20:51 +00:00
development feat(telemetry): Track tool execution outcomes (#8180) 2026-08-03 10:21:44 +00:00
examples docs(serve): Close multi-workspace hardening gaps (#7019) 2026-07-16 17:33:53 +00:00
tools fix(core): Avoid replaying unsafe MCP tool calls (#8387) 2026-08-03 11:04:38 +00:00
_meta.ts docs(daemon): Refresh daemon docs in English (#5144) 2026-06-15 22:38:01 +08:00
architecture.md docs: refresh architecture overview (#8325) 2026-08-02 02:45:00 +00:00
channel-plugins.md docs(channels): Document loops and proactive delivery (#7628) 2026-07-26 07:32:18 +00:00
contributing.md revert: remove local PR verification gate (#7031) 2026-07-16 11:24:38 +00:00
qwen-serve-protocol.md feat(serve): observe daemon and child memory against real denominators (#8423) 2026-08-07 06:10:37 +00:00
roadmap.md docs: standardize GitHub Actions capitalization (#6367) 2026-07-06 06:55:07 +00:00
sdk-java.md feat(sdk-java): Add daemon transport (#7463) 2026-07-23 12:38:34 +00:00
sdk-python.md doc[sdk-python] Expand Python SDK usage documentation (#3995) 2026-05-12 15:27:00 +08:00
sdk-typescript.md feat(web-shell): show subagent sessions in detail panel (#7380) 2026-07-22 02:31:08 +00:00