You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The warm pool in status, doctor, one event, and the console #371
After this PR an operator can see the warm pool: simlock status shows whether it is on, the reserve, and each target with how many are ready, booting, and why it is short. simlock doctor reports a target that can never be met as configured. One event marks a target going short. The console shows the block on a worker's page.
In short
$ simlock status
warm pool: enabled, reserve ios 1 android 0
iPhone 17 / 26.0 / full wanted 2 ready 1 booting 1
iPhone 17 / 27.0 / full wanted 1 ready 0 booting 0 short: runtime-missing
$ simlock doctor
warm-pool-target-unreachable iPhone 17 / 27.0 / full: iOS 27.0 is not installed; run simlock component install ios 27.0
short: a target for which the pass could do nothing, with one reason. A target that is filling (a boot or creation in flight), held by the boot cap, or waiting behind a queued request is not short. When several reasons apply the first of this list is reported: disabled, no-driver, runtime-missing, unknown-model, unresolvable, boot-failed, device-limit, running-limit, reserve, ram-budget. With the pool off every configured target is short: disabled.
flowchart LR
P[pass ends] --> Q{target below count and the pass<br/>recorded a refusal for it?}
Q -->|no| N[not short]
Q -->|yes| S[short with reason]
S --> E{was it short<br/>after the last pass?}
E -->|no| EV[emit warm-pool.target-missed]
E -->|yes| Z[no event]
Loading
If this goes wrong: status says a target is short when it is filling, or the event fires every pass on a machine that is always full.
Technical spec
File and line references are to main at 4a52f6e, after #369 and #370.
src/core/warm-pool/converger.ts: figures(): WarmPoolFigures built after each pass: enabled, reserveRunning, and per resolved target the kind as resolved (platform, model, osVersion, mode), count, ready, booting (boots and provisions in flight for it), short?. policy.ts records, per target it could not fill, the first refusal reason in the order above, and records nothing for a target held by the boot cap or by a queued request. A target is short exactly when the policy recorded a reason for it. The one definition lives in policy.ts; status, doctor and the event read it. The converger keeps the last pass's short set and emits warm-pool.target-missed for each target that is short now and was not after the previous pass.
src/contract/schemas.ts (statusGetOutput, beside capacity at :707): required warmPool block as in the JSON above; short is an enum of the ten reasons. src/contract/protocol.ts:47: range to {min: 17, max: 17}.
src/daemon/dispatcher.ts:333 (#statusGet): reads the figures through a WarmPoolReader port on LeaseEngine (figures()), beside the capacity reader in src/core/lease-ports.ts.
src/core/doctor.ts: finding kind warm-pool-target-unreachable with the target's kind and the reason, for a target whose last figures are short with runtime-missing or unknown-model, and one finding per platform whose targets' counts add up to more than that platform's maxRunning minus its reserve, plus one when all targets' counts add up to more than the global maxRunning minus the sum of the reserves. Remediation names the install command or the limit and the sum. src/contract/schemas.ts:595 (doctorFindingSchema) gains the kind.
src/cli/index.ts:2307 (formatStatus): the block as printed above; simlock doctor prints the new finding like the others.
src/contract/schemas.ts (workerViewSchema, :946-966): the view gains optional warmPool, the same block. src/gateway/worker-link.ts: the status.get refresh fills it. src/gateway/aggregate.ts: status.get on a gateway carries each worker's block under its worker entry as the view holds it: absent for a worker whose status was never read or that is incompatible, the last read block for a disconnected one. The gateway's own top-level warmPool is { enabled: false, reserveRunning: { ios: 0, android: 0 }, targets: [] }.
src/http needs no route change: status is served as it is. ui/src/views/worker-detail.tsx: a "Warm pool" section listing the targets with the four numbers and the short reason, reading the existing status route.
Docs: docs/CLI.md (status, doctor), docs/HTTP-API.md and docs/CLIENT.md (the status block), docs/CONSOLE.md (the section), docs/CONFIGURATION.md (how short is reported), both EVENTS.md files, docs/internal/ARCHITECTURE.md protocol paragraph.
doctor findings gain kind warm-pool-target-unreachable.
New event warm-pool.target-missed: payload { platform, model, osVersion, mode, count, ready, reason }, emitted by warm-pool after a pass commits, on the edge only. Row in both EVENTS.md files.
architecture.md rule 10: short is computed once, in the converger; status, doctor and the event read it.
documentation.md rule 2: no ADR citation in the end-user docs.
safety.md rule 10 is untouched: nothing new arrives over the wire.
Tests
policy: a target with ready 1 booting 1 count 2 records no reason; one held by the boot cap or a queued request records none; one refused for the reserve and the running limit records running-limit.
converger: warm-pool.target-missed fires once when a target first ends short, not on the next short pass, and again after it was met and missed again.
dispatcher: status.get carries warmPool with the converger's figures; on a daemon whose pool is off it carries enabled: false, reserveRunning, and every configured target with ready 0, booting 0 and short: disabled.
doctor: a target with runtime-missing yields warm-pool-target-unreachable naming the install command; two iOS targets of 2 on a limit of 3 yield one finding naming the sum and the limit; a target short for ram-budget does not.
contract: a status.get output without warmPool fails to parse; protocol range is 17.
CLI: simlock status prints the block and --json carries it; simlock doctor prints the finding.
gateway: status.get on a gateway carries a connected worker's block from its last status refresh, none for an incompatible worker, and an empty one of its own.
console: the worker page renders the targets table from the status route.
Done when
simlock status --json shows warmPool with enabled, reserveRunning and every configured target with count, ready, booting, and short when short; with warmPool.enabled: false every target shows short: disabled.
With a target naming a runtime that is not installed, simlock doctor prints warm-pool-target-unreachable with the install command.
simlock events --follow shows one warm-pool.target-missed when a target first goes short and none on the following passes.
Through a gateway, simlock status --json shows the warmPool block of each connected worker.
The console's worker page shows the warm pool section.
Out of scope
Fleet-level targets or a gateway pool.
Changing targets through the console or an admin command.
changed the title [-]Warm targets: devices booted ahead of demand, exempt from idle shutdown, pre-created under fresh identity[/-][+]The warm pool in status, doctor, one event, and the console[/+]on Oct 5, 2026
Done: PR #411 implements the task. All 13 round 1 findings are fixed and verified at 7550639. The warmPool field in status.get is optional, and review accepted that.
Not done: round 2 confirmed 9 new blocking findings, listed in the round 2 review on PR #411. Rule 14 allows no more rounds:
a test does not prove the target-missed edge key (iPhone 17 and iPhone 16 are different kinds);
the default-mode relay can be deleted with a green suite;
the status bounds are declared in both config.ts and schemas.ts;
the reserve cap is copied into doctor.ts;
the machine-wide over-limit check can print a negative room;
the console row id is not unique;
three docs miscount the event and the status and console rows.
Findings: none outside the PR.
Blocked on: the maintainer approving fix round 3 and review round 3, or taking the PR over.
Done: the extra round fixed 5 of the 6 code findings from round 2, verified at 7b2fd31.
Not done: round 3 confirmed these open findings, listed in full in its report:
code: putting the kind-only rowId back in ui/src/views/worker-detail.tsx:272 leaves the suite green;
claims: 8 lines still say "one per kind" or "one per target", in docs/EVENTS.md, docs/CONSOLE.md, docs/internal/EVENTS.md, src/bus/index.ts, src/contract/operations.ts, src/cli/index.ts:2413, worker-detail.tsx:237 and converger.ts:121,392. The code gives one row per refused or configured entry, and a target's miss ends on the merged count.
Findings: none outside the PR.
Blocked on: the maintainer approving one more fix round and review round, or taking the PR over.
Part of #359.
Scope
After this PR an operator can see the warm pool:
simlock statusshows whether it is on, the reserve, and each target with how many are ready, booting, and why it is short.simlock doctorreports a target that can never be met as configured. One event marks a target going short. The console shows the block on a worker's page.In short
short: a target for which the pass could do nothing, with one reason. A target that is filling (a boot or creation in flight), held by the boot cap, or waiting behind a queued request is not short. When several reasons apply the first of this list is reported:
disabled,no-driver,runtime-missing,unknown-model,unresolvable,boot-failed,device-limit,running-limit,reserve,ram-budget. With the pool off every configured target isshort: disabled.flowchart LR P[pass ends] --> Q{target below count and the pass<br/>recorded a refusal for it?} Q -->|no| N[not short] Q -->|yes| S[short with reason] S --> E{was it short<br/>after the last pass?} E -->|no| EV[emit warm-pool.target-missed] E -->|yes| Z[no event]If this goes wrong: status says a target is short when it is filling, or the event fires every pass on a machine that is always full.
Technical spec
File and line references are to
mainat 4a52f6e, after #369 and #370.Modules touched
src/core/warm-pool/converger.ts:figures(): WarmPoolFiguresbuilt after each pass:enabled,reserveRunning, and per resolved target the kind as resolved (platform, model, osVersion, mode),count,ready,booting(boots and provisions in flight for it),short?.policy.tsrecords, per target it could not fill, the first refusal reason in the order above, and records nothing for a target held by the boot cap or by a queued request. A target is short exactly when the policy recorded a reason for it. The one definition lives inpolicy.ts; status, doctor and the event read it. The converger keeps the last pass's short set and emitswarm-pool.target-missedfor each target that is short now and was not after the previous pass.src/contract/schemas.ts(statusGetOutput, besidecapacityat :707): requiredwarmPoolblock as in the JSON above;shortis an enum of the ten reasons.src/contract/protocol.ts:47: range to{min: 17, max: 17}.src/daemon/dispatcher.ts:333(#statusGet): reads the figures through aWarmPoolReaderport onLeaseEngine(figures()), beside the capacity reader insrc/core/lease-ports.ts.src/core/doctor.ts: finding kindwarm-pool-target-unreachablewith the target's kind and the reason, for a target whose last figures are short withruntime-missingorunknown-model, and one finding per platform whose targets' counts add up to more than that platform'smaxRunningminus its reserve, plus one when all targets' counts add up to more than the globalmaxRunningminus the sum of the reserves. Remediation names the install command or the limit and the sum.src/contract/schemas.ts:595(doctorFindingSchema) gains the kind.src/cli/index.ts:2307(formatStatus): the block as printed above;simlock doctorprints the new finding like the others.src/contract/schemas.ts(workerViewSchema, :946-966): the view gains optionalwarmPool, the same block.src/gateway/worker-link.ts: thestatus.getrefresh fills it.src/gateway/aggregate.ts:status.geton a gateway carries each worker's block under its worker entry as the view holds it: absent for a worker whose status was never read or that isincompatible, the last read block for a disconnected one. The gateway's own top-levelwarmPoolis{ enabled: false, reserveRunning: { ios: 0, android: 0 }, targets: [] }.src/httpneeds no route change: status is served as it is.ui/src/views/worker-detail.tsx: a "Warm pool" section listing the targets with the four numbers and the short reason, reading the existing status route.docs/CLI.md(status, doctor),docs/HTTP-API.mdanddocs/CLIENT.md(the status block),docs/CONSOLE.md(the section),docs/CONFIGURATION.md(how short is reported), bothEVENTS.mdfiles,docs/internal/ARCHITECTURE.mdprotocol paragraph.Contract and event changes
status.getoutput gains requiredwarmPool; protocol 17.doctorfindings gain kindwarm-pool-target-unreachable.warm-pool.target-missed: payload{ platform, model, osVersion, mode, count, ready, reason }, emitted bywarm-poolafter a pass commits, on the edge only. Row in bothEVENTS.mdfiles.Rules in play
events.mdrules 1 (name: subjectwarm-pool, past-tense fact), 3 (post-commit), 8 (both catalogs).architecture.mdrule 10:shortis computed once, in the converger; status, doctor and the event read it.documentation.mdrule 2: no ADR citation in the end-user docs.safety.mdrule 10 is untouched: nothing new arrives over the wire.Tests
ready 1 booting 1 count 2records no reason; one held by the boot cap or a queued request records none; one refused for the reserve and the running limit recordsrunning-limit.warm-pool.target-missedfires once when a target first ends short, not on the next short pass, and again after it was met and missed again.status.getcarrieswarmPoolwith the converger's figures; on a daemon whose pool is off it carriesenabled: false,reserveRunning, and every configured target withready 0,booting 0andshort: disabled.runtime-missingyieldswarm-pool-target-unreachablenaming the install command; two iOS targets of 2 on a limit of 3 yield one finding naming the sum and the limit; a target short forram-budgetdoes not.status.getoutput withoutwarmPoolfails to parse; protocol range is 17.simlock statusprints the block and--jsoncarries it;simlock doctorprints the finding.status.geton a gateway carries a connected worker's block from its last status refresh, none for an incompatible worker, and an empty one of its own.Done when
simlock status --jsonshowswarmPoolwithenabled,reserveRunningand every configured target withcount,ready,booting, andshortwhen short; withwarmPool.enabled: falseevery target showsshort: disabled.simlock doctorprintswarm-pool-target-unreachablewith the install command.simlock events --followshows onewarm-pool.target-missedwhen a target first goes short and none on the following passes.simlock status --jsonshows thewarmPoolblock of each connected worker.Out of scope
Depends on
Approval
Written by an agent.