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 module: one budget, a keep rule that fits, and an off switch #368
After this PR warmth has one owner: the warm pool module. A released device that fits a waiting request stays warm and is granted to it (closes #350). A released device stays warm when there is room, and is shut down when there is not. warmPool.enabled: false keeps nothing warm. At startup the pool shuts down whatever is over the running limit. Targets and the reserve come in later tasks.
In short
A release ends as today for the caller. In the background the purge commits the driver's result, and the warm pool then runs one pass: it shuts down idle devices over the budget, oldest first, and boots back a shut-down device that serves a waiting request or was released a moment ago and has room. The budget is the running limit minus every slot a leased, reclaiming, quarantined or reserved device holds.
flowchart LR
A[device.reclaimed on the bus] --> B[warm pool pass]
B --> C{over budget?}
C -->|yes| D[shut down idle devices, LRU,<br/>never one that serves a waiting request]
C -->|no| E{shut-down device serves<br/>a waiting request, or room?}
E -->|yes| F[boot it back, kick the queue]
E -->|no| G[leave it shut down]
Loading
waiting request: a lease request still queued for a device (state new or queued), not one already booting or creating its own. serves: a device serves a waiting request when it fits the request's model or class, OS and image tag, and its pool mode is the mode the request resolved to. Example: a released iPhone 15 on 26.0 serves a waiting --class phone request; a slim one does not serve --class phone --mode full.
Before, at the running cap with --class phone waiting:
$ simlock release lse_15 # iPhone 15
# iPhone 15 shut down, iPhone 17 booted for the waiter (30 s)
After:
$ simlock release lse_15
# iPhone 15 purged and granted to the waiter
Released device
Waiting request
Today
After
iPhone 15 / 26.0
--class phone
shut down, iPhone 17 booted
kept, granted
iPhone 15 / 26.0, slim
--class phone --mode full
shut down
shut down
Pixel 8
--model "iPhone 17"
shut down
shut down; the iPhone boots into the slot
iPhone 17
none, room under the limit
kept
kept
iPhone 17
none, enabled: false
kept
shut down
Pixel 8
none, enabled: false
kept running
restored, then shut down
If this goes wrong: a released device is shut down while a request it fits waits, or a device is kept and the machine runs over its limit.
Technical spec
File and line references are to main at 4a52f6e, after #373.
Modules touched
flowchart LR
classDef changed fill:#f3d9a4,stroke:#8a6a2a
classDef new fill:#cfe3f7,stroke:#2f5f8f
BUS((event bus)) -.-> CONV
subgraph WP[src/core/warm-pool/]
IDX[index.ts]:::new
POL[policy.ts]:::new
CONV[converger.ts]:::new
CFG[config.ts]:::new
end
CONV --> POL
CONV --> LIFE[managed-device-lifecycle.ts]
CONV --> CAP[capacity/index.ts]
CONV --> CLAIMS[device-operation-claims.ts]
CONV --> REG[registry snapshot]
CONV --> ACQ[lease-acquisition-coordinator.ts<br/>waitingRequests, kick]:::changed
POL --> DOM[domain.ts fits, specMode]
POL --> IDLE[idle-order.ts]
ENG[lease-engine.ts]:::changed --> IDX
CONFIG[config.ts]:::changed --> CFG
Loading
New src/core/warm-pool/index.ts exporting WarmPool (construct, start, dispose, pass for tests), warmPoolConfigValidator, defaultWarmPoolConfig, and the WarmPoolConfig type. Nothing outside the directory imports another file in it.
src/core/warm-pool/policy.ts: evaluate(view): readonly WarmProposal[], pure. view is { devices, leases, waiting, capacity: RunningCapacity, claims: isClaimed, config, now }. A proposal is { action: "shutdown" | "boot", deviceId, reason }. Order: shutdowns over budget by compareLeastRecentlyUsed, skipping any device that serves a waiting request; then boots for a shutdown device that serves the first request in the line (the queue head, whether or not it is in flight; a request behind the head gets no boot, because strict FIFO cannot grant it and idle shutdown would undo the boot; A request waits only behind requests for the same platform #387 later splits the line per platform), then, most recently released first, for shutdown devices released less than idle.shutdownAfterMs ago while the budget has room. With enabled: false every unleased ready device is a shutdown proposal and nothing is a boot.
Budget per platform: room = maxRunning − reserveSetting − running − reserved from RunningCapacity (running counts ready, leased, reclaiming and quarantined devices, as src/core/capacity/limits.ts does; reserved is its count of in-flight provisioning and running reservations, which is how a device being created is counted, once). room < 0 means over budget by that many devices. A boot proposal fits when room ≥ 1 for the platform and globally. reserveSetting is 0 here and arrives in A reserve of running slots, and a request waits for a device on its way #369.
serves(device, waiting): fits(waiting.requirement, device.spec, classOf) and specMode(device.spec) === waiting.mode. The pool reads waiters through a new core-side read port on LeaseAcquisitionCoordinator, waitingDemand(): readonly WaitingDemand[], one entry per waiter in state new or queued (a processing waiter has device work in flight, a provision, boot or eviction, and is being served, not waiting), with { platform, requirement, mode, classOf } from the waiter's resolved spec and requirement (AcquisitionWaiter). It is separate from the wire-shaped waitingRequests() on LeaseEngine, which does not change.
src/core/warm-pool/converger.ts: subscribes to daemon.started, device.reclaimed, device.quarantine-recovered, lease.granted, lease.released, device.shutdown, device.deleted, cleanup.executed, capacity.changed, plus a tick every 30 s (a module constant, not a config key). One pass at a time; a trigger during a pass requests one more. For each proposal, inside decisions.run: revalidate state, lease and claim, then lifecycle.shutdown(device, "warm-pool", "cleanup") or, for a boot, capacity.tryReserveRewarm then lifecycle.boot under a boot claim (a new lifecycle method beside bootForLease that releases its claim on success and commits ready with readyTransitionUpdate), releasing the reservation after the commit. A failed step logs one warn line with deviceId, step, error and leaves the device in the state the lifecycle left it. After every action, succeeded or failed, kick(), so a request waiting on a pool boot re-plans either way.
Docs: docs/CONFIGURATION.md gains warmPool.enabled; docs/internal/ARCHITECTURE.md "Device state machine" and "Running capacity" paragraphs describe the pass; docs/internal/COMPONENTS.md gains the warm pool row; docs/internal/IDEAS.md unchanged (already points at the ADR).
Contract and event changes
device.shutdown gains the initiator value warm-pool; both EVENTS.md rows list it.
No new event. No protocol change.
Rules in play
architecture.md rules 5 (the pool observes the bus and acts by direct calls), 10 (the budget is enforced here and nowhere else), 14, 15, 16.
safety.md rule 2: a pass never touches a leased or claimed device, revalidated inside the decision section.
events.md rule 6: warm-pool is an additive initiator value.
Tests
policy.ts: a released iPhone 15 that serves a waiting class request is proposed for boot, not shutdown, at the running cap.
policy.ts: a slim device is not proposed for boot for a waiting full request of its class, and a full one not for a slim request.
policy.ts: over budget, the least recently used idle device is proposed for shutdown and one that serves a waiting request never is.
policy.ts: with enabled: false, every unleased ready device is proposed for shutdown and no boot is proposed.
policy.ts: a shut-down device released longer ago than idle.shutdownAfterMs is not booted back without a waiting request.
policy.ts: a shut-down device that serves only a request behind the queue head is not booted, including while the head is in flight.
policy.ts: with room for one boot and two recently released shut-down devices, the more recently released one is proposed.
converger.ts: a boot that fails still kicks acquisition.
converger.ts: a device leased between the proposal and the action is left alone.
converger.ts: a boot holds a rewarm reservation until its commit, and releases it when the boot fails.
converger.ts: two triggers during one pass run exactly one more pass.
converger.ts: a failed shutdown logs one line and the device stays ready.
lease engine: after daemon.started with three ready devices and maxRunning: 2, one is shut down with initiator warm-pool.
boundary: no file outside src/core/warm-pool/ imports a file inside it other than index.ts.
Done when
At the running cap with simlock lease --class phone waiting, simlock release of a fitting iPhone grants that device to the waiter and simlock events shows no device.provisioned for it.
With warmPool.enabled: false, a released fake iOS device is shutdown and a released fake Android device is shutdown after its device.reclaimed with strategy snapshot.
With maxRunning lowered below the number of ready devices and the daemon restarted, the excess devices are shutdown with initiator warm-pool after daemon.started.
simlock config shows warmPool.enabled: true by default.
changed the title [-]Reclaim is its own coordinator and commits what the driver returns[/-][+]The warm pool module: one budget, a keep rule that fits, and an off switch[/+]on Oct 5, 2026
Done: PR #386 (draft) adds the src/core/warm-pool/ module: one budget, a keep rule that fits a waiting request, and warmPool.enabled: false. Two review rounds; 5 round-1 findings fixed. Tests green on test:changed, 3 alive mutants with no observable effect.
Not done: round 2 left 3 blocking findings, and one is a behaviour defect, so rule 14 stops here.
Findings:
policy.ts:154: when the queue head is in flight, the pool boots for the request behind it and takes the slot the head needs. A unit test reproduces it.
Stale docs: CONFIGURATION.md:26 (says no simulator stays warm), lease-engine.ts:310 and ARCHITECTURE.md:1162 (say the first pass follows startup convergence).
Blocked on: the maintainer. The spec's policy line says "boots for shutdown devices that serve a waiting request". Booting for a request behind the queue head starts a loop with idle-shutdown, so the round-1 fix boots only for the head. Decide: (a) only the head, in flight or not, or (b) every waiter, with idle-shutdown skipping a device booted for one.
Done: PR #386 (draft) at e7677c6. The pool now boots only for the queue head, starts after startup convergence, and CONFIGURATION.md is corrected. Three review rounds; 8 blocking findings fixed. test:changed green.
Not done: round 3 left 2 blocking findings, so rule 14 stops here.
Findings:
policy.ts:175: simlock nuke without --delete-devices gets undone. Nuke marks every leased device as just released and shuts it down. After maintenance ends, the next pass boots those devices back as recently released, against CLI.md:1798. Likely fix: nuke's forced release should not count as recently released for the pool.
policy.test.ts:316: "boots nothing for a request behind a head whose device work is in flight" cannot fail. Its view has no free room, so nothing is booted whatever the head rule does.
Blocked on: the maintainer. Fix both on the branch (or say "fix and re-review") and re-run deliver.
Done: PR #386 (draft) passed review round 5 with 0 blocking findings open. All round-3 and round-4 findings are fixed: nuke stays nuked, the in-flight head test can fail, and the inFlight guard has a test. Rounds 4 and 5 ran with the maintainer's approval. Notes from round 4 were filed as #388 and #389.
Not done: the PR body still lacks its ## Review section, the review-notes comment is not posted, the PR is still a draft, and the gate has not run. Totals to write into the body: spec review 10 blocking, 4 fixed, 11 notes; code review 8 blocking, 5 fixed, 13 notes; 5 rejected. Mutate: 3 alive, all explained as equivalent in the PR body.
Findings: none open.
Blocked on: nothing. The session hit its 10 h budget. Re-run /deliver 359: it finishes the #386 body, marks it ready, runs the gate, then moves on to #369.
Part of #359.
Scope
After this PR warmth has one owner: the warm pool module. A released device that fits a waiting request stays warm and is granted to it (closes #350). A released device stays warm when there is room, and is shut down when there is not.
warmPool.enabled: falsekeeps nothing warm. At startup the pool shuts down whatever is over the running limit. Targets and the reserve come in later tasks.In short
A release ends as today for the caller. In the background the purge commits the driver's result, and the warm pool then runs one pass: it shuts down idle devices over the budget, oldest first, and boots back a shut-down device that serves a waiting request or was released a moment ago and has room. The budget is the running limit minus every slot a leased, reclaiming, quarantined or reserved device holds.
flowchart LR A[device.reclaimed on the bus] --> B[warm pool pass] B --> C{over budget?} C -->|yes| D[shut down idle devices, LRU,<br/>never one that serves a waiting request] C -->|no| E{shut-down device serves<br/>a waiting request, or room?} E -->|yes| F[boot it back, kick the queue] E -->|no| G[leave it shut down]waiting request: a lease request still queued for a device (state
neworqueued), not one already booting or creating its own. serves: a device serves a waiting request when it fits the request's model or class, OS and image tag, and its pool mode is the mode the request resolved to. Example: a released iPhone 15 on 26.0 serves a waiting--class phonerequest; a slim one does not serve--class phone --mode full.Before, at the running cap with
--class phonewaiting:After:
--class phone--class phone --mode full--model "iPhone 17"enabled: falseenabled: falseIf this goes wrong: a released device is shut down while a request it fits waits, or a device is kept and the machine runs over its limit.
Technical spec
File and line references are to
mainat 4a52f6e, after #373.Modules touched
flowchart LR classDef changed fill:#f3d9a4,stroke:#8a6a2a classDef new fill:#cfe3f7,stroke:#2f5f8f BUS((event bus)) -.-> CONV subgraph WP[src/core/warm-pool/] IDX[index.ts]:::new POL[policy.ts]:::new CONV[converger.ts]:::new CFG[config.ts]:::new end CONV --> POL CONV --> LIFE[managed-device-lifecycle.ts] CONV --> CAP[capacity/index.ts] CONV --> CLAIMS[device-operation-claims.ts] CONV --> REG[registry snapshot] CONV --> ACQ[lease-acquisition-coordinator.ts<br/>waitingRequests, kick]:::changed POL --> DOM[domain.ts fits, specMode] POL --> IDLE[idle-order.ts] ENG[lease-engine.ts]:::changed --> IDX CONFIG[config.ts]:::changed --> CFGsrc/core/warm-pool/index.tsexportingWarmPool(construct,start,dispose,passfor tests),warmPoolConfigValidator,defaultWarmPoolConfig, and theWarmPoolConfigtype. Nothing outside the directory imports another file in it.src/core/warm-pool/policy.ts:evaluate(view): readonly WarmProposal[], pure.viewis{ devices, leases, waiting, capacity: RunningCapacity, claims: isClaimed, config, now }. A proposal is{ action: "shutdown" | "boot", deviceId, reason }. Order: shutdowns over budget bycompareLeastRecentlyUsed, skipping any device that serves a waiting request; then boots for ashutdowndevice that serves the first request in the line (the queue head, whether or not it is in flight; a request behind the head gets no boot, because strict FIFO cannot grant it and idle shutdown would undo the boot; A request waits only behind requests for the same platform #387 later splits the line per platform), then, most recently released first, forshutdowndevices released less thanidle.shutdownAfterMsago while the budget has room. Withenabled: falseevery unleasedreadydevice is a shutdown proposal and nothing is a boot.room = maxRunning − reserveSetting − running − reservedfromRunningCapacity(runningcountsready,leased,reclaimingandquarantineddevices, assrc/core/capacity/limits.tsdoes;reservedis its count of in-flight provisioning and running reservations, which is how a device being created is counted, once).room < 0means over budget by that many devices. A boot proposal fits whenroom ≥ 1for the platform and globally.reserveSettingis 0 here and arrives in A reserve of running slots, and a request waits for a device on its way #369.serves(device, waiting):fits(waiting.requirement, device.spec, classOf)andspecMode(device.spec) === waiting.mode. The pool reads waiters through a new core-side read port onLeaseAcquisitionCoordinator,waitingDemand(): readonly WaitingDemand[], one entry per waiter in stateneworqueued(aprocessingwaiter has device work in flight, a provision, boot or eviction, and is being served, not waiting), with{ platform, requirement, mode, classOf }from the waiter's resolved spec and requirement (AcquisitionWaiter). It is separate from the wire-shapedwaitingRequests()onLeaseEngine, which does not change.src/core/warm-pool/converger.ts: subscribes todaemon.started,device.reclaimed,device.quarantine-recovered,lease.granted,lease.released,device.shutdown,device.deleted,cleanup.executed,capacity.changed, plus a tick every 30 s (a module constant, not a config key). One pass at a time; a trigger during a pass requests one more. For each proposal, insidedecisions.run: revalidate state, lease and claim, thenlifecycle.shutdown(device, "warm-pool", "cleanup")or, for a boot,capacity.tryReserveRewarmthenlifecycle.bootunder abootclaim (a new lifecycle method besidebootForLeasethat releases its claim on success and commitsreadywithreadyTransitionUpdate), releasing the reservation after the commit. A failed step logs onewarnline withdeviceId,step,errorand leaves the device in the state the lifecycle left it. After every action, succeeded or failed,kick(), so a request waiting on a pool boot re-plans either way.src/core/warm-pool/config.ts:enabled: boolean(defaulttrue). Validator composed intosrc/core/config.ts:135,657besidequarantine.src/core/lease-engine.ts: constructs and starts the pool after the startup converger, so its first pass followsdaemon.started; disposes it on stop.src/core/startup-converger.ts: no change beyond Reclaim is its own coordinator and commits what the driver returns #373; the first pass is what converges a loweredmaxRunning.docs/CONFIGURATION.mdgainswarmPool.enabled;docs/internal/ARCHITECTURE.md"Device state machine" and "Running capacity" paragraphs describe the pass;docs/internal/COMPONENTS.mdgains the warm pool row;docs/internal/IDEAS.mdunchanged (already points at the ADR).Contract and event changes
device.shutdowngains the initiator valuewarm-pool; bothEVENTS.mdrows list it.Rules in play
architecture.mdrules 5 (the pool observes the bus and acts by direct calls), 10 (the budget is enforced here and nowhere else), 14, 15, 16.safety.mdrule 2: a pass never touches a leased or claimed device, revalidated inside the decision section.events.mdrule 6:warm-poolis an additive initiator value.Tests
policy.ts: a released iPhone 15 that serves a waiting class request is proposed for boot, not shutdown, at the running cap.policy.ts: a slim device is not proposed for boot for a waitingfullrequest of its class, and a full one not for aslimrequest.policy.ts: over budget, the least recently used idle device is proposed for shutdown and one that serves a waiting request never is.policy.ts: withenabled: false, every unleased ready device is proposed for shutdown and no boot is proposed.policy.ts: a shut-down device released longer ago thanidle.shutdownAfterMsis not booted back without a waiting request.policy.ts: a shut-down device that serves only a request behind the queue head is not booted, including while the head is in flight.policy.ts: with room for one boot and two recently released shut-down devices, the more recently released one is proposed.converger.ts: a boot that fails still kicks acquisition.converger.ts: a device leased between the proposal and the action is left alone.converger.ts: a boot holds a rewarm reservation until its commit, and releases it when the boot fails.converger.ts: two triggers during one pass run exactly one more pass.converger.ts: a failed shutdown logs one line and the device staysready.daemon.startedwith three ready devices andmaxRunning: 2, one is shut down with initiatorwarm-pool.src/core/warm-pool/imports a file inside it other thanindex.ts.Done when
simlock lease --class phonewaiting,simlock releaseof a fitting iPhone grants that device to the waiter andsimlock eventsshows nodevice.provisionedfor it.warmPool.enabled: false, a released fake iOS device isshutdownand a released fake Android device isshutdownafter itsdevice.reclaimedwith strategysnapshot.maxRunninglowered below the number of ready devices and the daemon restarted, the excess devices areshutdownwith initiatorwarm-poolafterdaemon.started.simlock configshowswarmPool.enabled: trueby default.Out of scope
reserveRunningand waiting for a device on its way (A reserve of running slots, and a request waits for a device on its way #369).Depends on
Approval
Written by an agent.