Repository navigation
A reserve of running slots, and a request waits for a device on its way #369
Description
Part of #359.
Scope
After this PR an operator can keep running slots free of warm devices with warmPool.reserveRunning, so a request for a model nobody predicted boots at once instead of shutting a warm device down first. And a request for a device that is already on its way, booting or being created, waits for it instead of starting a second one.
In short
The reserve is a number of running slots per platform that idle warm devices may not fill. Default none, which is today's behaviour. The global budget subtracts the sum of the platform reserves.
$ simlock config set warmPool.reserveRunning.ios 1
$ simlock daemon restart
# iOS running limit 3: at most 2 idle devices stay warm
$ simlock lease --model "iPhone 15" # with two warm iPhone 17
{"state":"booting"} # no warm device shut down first
| Config | Today | After |
|---|---|---|
reserveRunning absent |
n/a | reserve 0 on both platforms |
reserveRunning.ios: 1, limit 3 |
unknown key, ignored | at most 2 warm iOS devices |
reserveRunning.ios: 3, limit 3 |
ignored | accepted; no iOS device ever warm |
reserveRunning.ios: 4, limit 3 |
ignored | accepted; same as 3 |
reserveRunning.ios: -1 |
ignored | config error |
on its way: a device that serves the request, is either under a boot claim or in provisioning, and that no other request already owns: the pool's own boots and creations. A device another request is booting or creating is ignored, as today. A request that finds one waits and is granted when it is ready. Under --no-wait the case is ignored and the request plans as today.
flowchart LR
R[request arrives] --> P{ready device serves it?}
P -->|yes| G[grant]
P -->|no| W{device on its way serves it?}
W -->|yes, and nobody owns it| H[wait, granted when ready]
W -->|no| S[shut-down device? boot it]
S -->|none| C[provision, as today]
If this goes wrong: a request waits on a boot that fails, until its own deadline, instead of booting its own; or the reserve is counted twice and the pool is smaller than configured.
Technical spec
File and line references are to main at 4a52f6e, after #368.
Modules touched
flowchart LR
classDef changed fill:#f3d9a4,stroke:#8a6a2a
CFG[warm-pool/config.ts]:::changed --> POL[warm-pool/policy.ts]:::changed
PLAN[acquisition-planner.ts]:::changed --> CLAIMS[device-operation-claims.ts]:::changed
PLAN --> DOM[domain.ts]
ACQ[lease-acquisition-coordinator.ts] --> PLAN
src/core/warm-pool/config.ts:reserveRunning: { ios: number; android: number }, non-negative integers, default 0 each.src/core/warm-pool/policy.ts: the per-platform budget subtracts the platform's reserve; the global budget subtracts their sum. A reserve above the platform'smaxRunningmeans no warm device for that platform.src/core/device-operation-claims.ts: a claim gains an optionalowner(a waiter id). The planner'sboot-shutdownclaim and the request path's provision carry the waiter's id; the pool's boots and creations carry none.DeviceOperationClaimsgainsclaim(deviceId): { kind, owner? } | undefined;isClaimedis unchanged. A device with no claim at all is never on its way: a request's device between its registration and its boot claim, and aprovisioningrecord a crash left behind, both look like that and are ignored.src/core/device-provisioner.ts:82-96:provisiontakes an optionalclaim: { kind: "boot", owner?: string }and, when given, takes that claim on the new device in the same decision section that registers it, holding it until the handoff. The request path passes its waiter id; the pool (in Warm targets: devices booted ahead of demand, exempt from idle shutdown, pre-created under fresh identity #370) passes a claim with no owner.src/core/acquisition-planner.ts:86-98: after thereadysearch and before theshutdownsearch, and only whennoWaitis false, find a device for whichservesRequestholds (with the!isClaimedterm moved out of it), that isprovisioningorshutdown, and whose claim is of kindbootwith no owner. If found, returnwait. UndernoWaitthe search is skipped and the plan is as today.- A second request for the same model while the first is booting or creating its own device is unchanged: that device has an owner, so the second request plans as today.
src/core/lease-acquisition-coordinator.ts: no change. A waiter woken bydevice.ready's kick re-plans and is granted the ready device, as today.- Docs:
docs/CONFIGURATION.mdgainswarmPool.reserveRunning.<platform>and a sentence on waiting for a device on its way under the lease section;docs/internal/ARCHITECTURE.md"Device requests" lists the new planner case;docs/internal/adr/0015-...mdis not edited (ADR 0017 declares the narrowing).
Contract and event changes
config.get returns the new required warmPool.reserveRunning, so the protocol version moves to 17. device.shutdown with initiator warm-pool gains a cause: an idle device in a slot the reserve holds. No new events.
Rules in play
architecture.mdrules 10 (the budget stays inpolicy.ts; the planner reads claims, not the pool) and 15 (the planner learns a claim kind, never the pool).safety.mdrule 10 for the new config values.
Tests
policy.ts: withreserveRunning.ios: 1andmaxRunning3, a third idle iOS device is proposed for shutdown and a second is not.policy.ts: with reserves ios 1 and android 1, the global budget is the global limit minus 2.policy.ts: a reserve above the platform limit proposes every idle device of that platform for shutdown.- config: a negative or non-integer reserve fails the load naming
warmPool.reserveRunning.ios. - planner: a request whose only serving device is under an unowned
bootclaim planswait; undernoWaitit plans as if the device were absent. - planner: a request whose only serving device is
provisioningunder an unownedbootclaim planswait; one under a claim owned by another waiter, or with no claim at all, is ignored and the request provisions, as today. - lease engine: a request waiting on a pool boot that fails re-plans on the kick and provisions its own device.
- planner: a serving device under an
evictionorcleanupclaim is ignored and the request provisions, as before. - planner: a device on its way of another mode does not make the request wait.
- lease engine: a request arriving during a warm-pool boot that serves it is granted that device when the boot settles, and
device.provisioneddoes not fire.
Done when
- With
warmPool.reserveRunning.ios: 1and an iOS running limit of 3, hold three fake iOS leases and release them one by one: the first released device staysshutdownwith nodevice.readyafter itsdevice.reclaimed(two still leased leave no room under the reserve); once all three are released, two of the three arereadyand one isshutdown(which one is not specified), andsimlock statusshows two warm iOS devices. - With a warm-pool boot in flight for a fake iPhone 17,
simlock lease --model "iPhone 17"is granted that device andsimlock eventsshows onedevice.readyand nodevice.provisionedfor it; the same lease with--no-waitprovisions its own device, as today. simlock configprintswarmPool.reserveRunningwith both platforms at 0 by default.
Out of scope
- Targets (Warm targets: devices booted ahead of demand, exempt from idle shutdown, pre-created under fresh identity #370). Status reporting of the reserve (The warm pool in status, doctor, one event, and the console #371).
Depends on
Approval
- Approved for delivery
Written by an agent.
Activity
[-]The warm pool module: one budget, a keep rule that fits, and an off switch[/-][+]A reserve of running slots, and a request waits for a device on its way[/+]feat(core): the warm pool module: one budget, a keep rule that fits, …
github-actions commentedon Oct 6, 2026
Now
task:ready: approved, technical spec present, #368 closed.test(e2e): warmPool.reserveRunning and a lease that waits for a booti…
V3RON commentedon Oct 6, 2026
Handoff
Done: PR #394 (draft) implements the reserve and makes a request wait for a device already on its way. 10/10 spec tests green, 3 e2e flows, docs updated. Mutate: 1 alive, equivalent. Not reviewed yet.
Not done: review, gate.
Findings: Done when line 1 says the first released iOS device stays down and the second and third are booted back. In practice the count holds (two warm, one down), but which device stays down depends on timing. The pool pass from
device.reclaimedruns while the released device's reclaim claim is still held, so it boots an older shut-down device instead. This timing comes from #368, not the reserve. The test asserts the count only.Blocked on: the maintainer. Either (a) line 1 checks only the count, or (b) the pool must favour the most recently released device, which changes when it runs a pass.
Written by an agent.
14 remaining items
V3RON commentedon Oct 6, 2026
Handoff
Done: PR #394 implements the reserve and the wait for a device on its way; 10/10 spec tests green, Done when 3/3 with e2e evidence, mutate 1 alive (equivalent fixture). Review rounds 1–3 fixed 11 blocking findings.
Not done: Review section, ready, gate.
Findings: two blocking test findings open after the last review round (e34c9e0):
capacityReservation.release()at lease-acquisition-coordinator.ts:859 keeps the suite green).Out of scope, filed: A request queued behind a failed boot whose destroy also fails is never woken #400, A running-slot reservation can leak when the warm pool claims a device between a failed boot and its destroy #401, flaky Flaky e2e: gateway-fast-fail times out on a lease of the busy-worker tests #395.
Blocked on: the maintainer's call — approve one more fix round for the two test findings, or fix them by hand.
Written by an agent.
V3RON commentedon Oct 6, 2026
Handoff
Done: an extra fix round (62e3cc2) fixed both round 3 findings. Round 4 found 3 new blocking findings, all in e2e/warm-pool-reserve.test.ts. The code under review is unchanged and passed.
Not done: Review section, ready, gate.
Findings (round 4, commit 62e3cc2):
booting. There is no such state; only the makeReady count catches it.Blocked on: the maintainer's call. Approve another fix round or fix this test by hand.
Written by an agent.