Repository navigation
A waiting request says why it is not placed, on a worker #636
Description
Part of #623.
Scope
On a single host (a worker), a request waiting in the queue says why it is not placed yet: in simlock list --requests, GET /v1/lease-requests, status.get waiting, and a "Why" row in the console's Waiting inspector. A worker's NO_CAPACITY answer names the capacity check that refused it, so #637 can show it on a gateway.
A request whose new device fails to provision twice no longer waits: it is rejected with a new error code PROVISION_FAILED carrying the driver's error (decided by the maintainer, 2026-10-18).
In short
Each time the daemon tries to place a waiting request, it keeps the reason the attempt stopped, in memory. A queued request shows the reason from its latest attempt. A starting request shows none: its stage already says the daemon is working on it. The reasons and their texts are feature #623 Shared values, rows "Waiting reason (worker)", "Reason text", "NO_CAPACITY detail" and "PROVISION_FAILED" (decided by the maintainer, 2026-10-18).
flowchart LR
A[Request arrives or queue wakes] --> P{Plan}
P -- grant / boot / create / evict --> S[starting]
P -- blocked --> R["Keep the reason on the waiter (new)"]
R --> Q[queued]
Q --> L["list.get requests: reason on the entry (new)"]
P -- blocked, noWait --> N["NO_CAPACITY with details.capacityReason (new)"]
Words used:
- Reason: the first check that stopped the request at its latest placement attempt, for example
platform-running-limit, printed "Full: running limit for iOS". - Capacity reason: one of the four capacity refusals (
device-limit,ram-budget,global-running-limit,platform-running-limit).
Before and after:
$ simlock list --requests
Request req_7: agent-b, ios iPhone 16, queued at 1, waiting 12s
Request req_9: agent-c, ios iPhone 16, queued at 2, waiting 3s
Request req_11: agent-d, android Pixel 8, starting, waiting 1s
$ simlock list --requests # after
Request req_7: agent-b, ios iPhone 16, queued at 1, waiting 12s — full: running limit for iOS
Request req_9: agent-c, ios iPhone 16, queued at 2, waiting 3s — behind an earlier request
Request req_11: agent-d, android Pixel 8, starting, waiting 1s
| Situation | Today | After |
|---|---|---|
| iOS running limit reached, nothing idle to shut down | queued at 1, waiting 12s |
... — full: running limit for iOS |
| Another request is ahead in the simulator/emulator line | queued at 2, ... |
... — behind an earlier request |
| Creating a device failed twice | queued at 1, ... |
rejected: PROVISION_FAILED with the driver's error (decided by the maintainer, 2026-10-18) |
| The warm pool is booting a device that fits | queued at 1, ... |
... — a matching device is starting |
| Physical: every match leased or absent | physical ..., queued at 1, ... |
... — every matching device is leased or absent |
| Being placed | starting, waiting 1s |
as today |
--no-wait at the RAM budget |
NO_CAPACITY, no details |
NO_CAPACITY, details.capacityReason: "ram-budget" |
If this goes wrong, a user would see a reason that is stale or wrong, or a reason on a request that is starting.
Technical spec
Facts
| # | Fact | Source |
|---|---|---|
| F1 | Only the head of the virtual line plans. Any other virtual waiter gets wait, or no-capacity under noWait, without asking the planner. |
src/leasing/lease-acquisition-coordinator.ts:891-907 |
| F2 | A queue wake drives the head of the virtual line and every pending physical waiter, nobody else. | src/leasing/lease-acquisition-coordinator.ts:1171-1181 |
| F3 | The planner waits, outside noWait, when the warm pool is booting or creating a fitting device for nobody. |
src/leasing/acquisition-planner.ts:107-116, :139-143 |
| F4 | A refused boot of a shut-down device with no idle device to evict is blocked; so is a boot whose claim is taken. |
src/leasing/acquisition-planner.ts:145-164 |
| F5 | A refused new device is blocked when there is no victim; at device-limit it first tries a managed victim; two failures block a provision that capacity allows. |
src/leasing/acquisition-planner.ts:166-187 |
| F6 | A taken eviction claim is blocked. The two eviction plans carry only a claim and a device, no reason. |
src/leasing/acquisition-planner.ts:67-76, :189-197 |
| F7 | blocked returns wait or no-capacity and drops the capacity reason it was given. |
src/leasing/acquisition-planner.ts:222-224 |
| F8 | A physical plan with no ready, present, free fit is blocked; a fit held for an older queued physical request is skipped. |
src/leasing/acquisition-planner.ts:128-137, :132; src/leasing/lease-acquisition-coordinator.ts:729-738 |
| F9 | The capacity refusal reasons are device-limit, ram-budget, global-running-limit, platform-running-limit. |
src/core/capacity/strategy.ts:30-34 |
| F10 | A blocked plan reaches #defer, which rejects a noWait waiter with NoCapacityError and enqueues any other. |
src/leasing/lease-acquisition-coordinator.ts:882-886, :1071-1078, :1239-1246 |
| F11 | After a first provisioning failure the waiter plans again at once; after a second it is enqueued without a plan, or a probe is declined with NoCapacityError. |
src/leasing/lease-acquisition-coordinator.ts:842-857 |
| F12 | A failed eviction (shutdown or delete threw) defers the waiter with no plan. | src/leasing/lease-acquisition-coordinator.ts:948-960, :978-984 |
| F13 | A physical waiter with no plan defers; a failed physical grant defers too. | src/leasing/lease-acquisition-coordinator.ts:718-721, :774-776 |
| F14 | A queued waiter whose spec matches a reclaiming device gets a reclaiming progress push; its plan is untouched. |
src/leasing/lease-acquisition-coordinator.ts:1080-1101 |
| F15 | NoCapacityError carries no fields. |
src/leasing/lease-acquisition-coordinator.ts:82-87 |
| F16 | Waiter state lives in memory only; a daemon start fails every request the previous process left open. | src/leasing/AGENTS.md "State and timers", "Rules" |
| F17 | waitingRequests() is the request book's waiting(queue.places()); places() gives a queuePosition only to a queued waiter, and the book maps that to stage queued, else starting. |
src/leasing/create-leasing.ts:280, src/leasing/wait-queue.ts:169-175, src/leasing/lease-request-book.ts:67-90, :308-313 |
| F18 | The gateway builds its own entries from the same book and its own WaitQueue. |
src/gateway/fleet-coordinator.ts:208-210 |
| F19 | waitingRequestSchema has no reason. It types list.get requests, status.get waiting, and a worker view's waiting. |
src/contract/schemas.ts:311-319, :1158; src/contract/operations.ts:123, :584 |
| F20 | The daemon parses each handler result through the output schema; an undeclared field is stripped. | src/contract/AGENTS.md "Rules" |
| F21 | The worker answers list.get requests and status.get waiting from queue.waitingRequests(). |
src/daemon/dispatcher.ts:380, :754 |
| F22 | GET /v1/lease-requests returns list.get requests as is; the console reads that route. |
src/http/app.ts:308-315, ui/src/state/waiting.ts:29 |
| F23 | NO_CAPACITY declares no details, and is not in CODES_WITH_DECLARED_DETAILS. |
src/contract/errors.ts:37, :148-175 |
| F24 | The socket sends details only from a DispatchError. HTTP puts them in the body only for a DispatchError whose code is in CODES_WITH_DECLARED_DETAILS. |
src/daemon/server.ts:843-853, src/http/errors.ts:154-170 |
| F25 | The worker dispatcher already turns a leasing error into a DispatchError with details: LEASE_ID_TAKEN with leaseId. |
src/daemon/dispatcher.ts:493-495 |
| F26 | POST /v1/lease-requests with noWait answers a refusal through errorResponse. |
src/http/app.ts:294-300 |
| F27 | fromWireError passes details through unchecked. |
src/contract/errors.ts:453-466 |
| F28 | The CLI parses the list with waitingRequestSchema and prints one line per request ending waiting <n>s. |
src/cli/index.ts:975-976, :987-1010 |
| F29 | The console's Waiting inspector shows cards for Waiting, Place, Stage, then facts Request, Created, Queue. A stage it does not know shows as its raw word. | ui/src/screens/waiting/index.tsx:108-142, ui/src/model/waiting.ts:104-124, ui/src/model/tones.ts:44, ui/src/model/waiting.test.ts:92 |
| F30 | The console's model/ imports contract types only. |
ui/AGENTS.md:5, :20 |
| F31 | Every contract shape change bumps the protocol by one in the same PR; the range is { min: 39, max: 39 }. |
docs/internal/agent-rules/toolchain.md:153-156, src/contract/AGENTS.md:58-60, src/contract/protocol.ts:13 |
| F32 | A replayed failed request answers its stored code only. | src/daemon/error-code.ts:90-94 |
| F34 | Cancelling a queued waiter rejects it and wakes nobody; so does enqueuing after a second provisioning failure. A queue timeout kicks the queue. | src/leasing/lease-acquisition-coordinator.ts:609-619, :842-857; src/leasing/create-leasing.ts:122-135 |
| F35 | The gateway lists a worker's own entries spread from its view, so every field the schema declares is kept. | src/gateway/dispatcher.ts:387-397 |
| F36 | The e2e test anchors each line at waiting \d+s$: single host at :46, :53; gateway at :91, :96, :98. Its limits are one iOS device, one running. |
e2e/waiting-requests.test.ts:13, :37-101 |
| F37 | Core's index re-exports capacity helpers but not CapacityRefusalReason, which only src/core/capacity/index.ts exports. |
src/core/index.ts:34-38, src/core/capacity/index.ts:21 |
| F33 | Docs that describe the line and the route today. | docs/CLI.md:1562-1580, docs/HTTP-API.md:1529-1561, docs/CONSOLE.md:289-313 |
| F38 | Exit codes group errors by class; 1 is internal or a failure the caller cannot fix by asking differently (INTERNAL, DAEMON_STARTUP_FAILED, WORKER_UNREACHABLE); 14 and 15 are taken by lease and component install outcomes. |
docs/CLI.md:84-118, src/contract/errors.ts:226-386 (decided by the maintainer, 2026-10-18) |
| F39 | The error tables users read: the CLI's exit codes and HTTP's status table. | docs/CLI.md:75-96, docs/HTTP-API.md:1758 (decided by the maintainer, 2026-10-18) |
What changes
-
The plan says why it is blocked.
blocked(F7) takes a reason and returns it onwaitandno-capacity. Each path gives:Path Reason Not the head of the virtual line (F1) behind-earlier-requestWarm pool booting a fit (F3) waiting-for-deviceBoot refused, no victim (F4) the refusal's capacity reason New device refused, no victim (F5) the refusal's capacity reason Victim found, its claim taken (F5, F6) the capacity reason that asked for the eviction Physical, only fits held for an older request (F8) behind-earlier-requestBoot claim already taken (F4) waiting-for-devicePhysical, no fit free (F8) no-free-physicalThe
evict-runningandevict-managedplans also carry the capacity reason that asked for them (F6). The capacity reasons map one to one (F9). The mapping is aRecordkeyed by core'sCapacityRefusalReason, so the compiler fails when the two lists differ. -
The waiter keeps it. Every enqueue sets the waiter's reason from the plan that led to it. Each new plan replaces it. It is never written to
state.json(F16).- Failed eviction (F12): the capacity reason that asked for the eviction.
- Failed physical grant (F13):
no-free-physical. - A request whose match is being cleaned (F14) keeps the capacity reason its plan gave; the
reclaimingpush stays as today. - The head leaving without a wake (F34): cancelling a queued waiter now wakes the queue, so the new head plans again and replaces its
behind-earlier-request(decided by the maintainer, 2026-10-18).
-
The listing shows it.
waitingRequests()addsreasonto eachqueuedentry that has one, never to astartingone (F17). The gateway's own entries get none (F18; A waiting request says why each worker cannot take it, on a gateway #637 addsworkers). A gateway's list keeps thereasonon a worker's own entries (F35); the CLI prints them with the same suffix. -
NO_CAPACITYnames the capacity reason.NoCapacityErrorcarries the plan's reason. When it is a capacity reason, the worker dispatcher throwsDispatchError("NO_CAPACITY", message, { capacityReason }), as it does forLEASE_ID_TAKEN(F25). Otherwise no details, as today. So the socket sends it (F24), and HTTP's 503 body carriescapacityReason(F24, F26). A replay answers no details, as today (F32). -
CLI. A
queuedline with a reason ends—plus the reason text with only its first letter lower-cased. The text comes from feature Operational metrics kept over hours and shown in the console #623 Shared values "Reason text"; the platform word is the request's (iOS,Android). -
Console. The inspector gains a "Why" fact after Queue, with the reason text, for an entry with a reason. No row otherwise. A reason the console does not know shows as its raw word, as a stage does (F29).
-
Two text tables, each checked. The CLI keeps its table in
src/cli, the console inui/src/model(F30). Each side has one test that its table covers every reason the contract's enum lists. -
A second provisioning failure rejects. A first failure still plans again at once, as today (F11). A second no longer enqueues: the waiter is rejected with
PROVISION_FAILED, kind domain, HTTP 500, exit code 1 (F38), whose message is the driver's error. A probe (fleetRequestIdset) that fails twice is declined withPROVISION_FAILEDtoo, notNO_CAPACITY. The rejection wakes the queue, as the boot-timeout and component-being-removed rejections do (src/leasing/lease-acquisition-coordinator.ts:828-845), so the next waiter plans. Those two stay as they are (decided by the maintainer, 2026-10-18).
Modules touched
Searches: waitingRequestSchema, WaitingRequest, places(, blocked(, NoCapacityError, "NO_CAPACITY", CapacityRefusalReason, formatWaitingRequests, WaitingRow.
src/contract/schemas.ts,src/contract/errors.ts,src/contract/protocol.tssrc/core/index.ts(exportsCapacityRefusalReason)src/leasing/acquisition-planner.ts,src/leasing/lease-acquisition-coordinator.ts,src/leasing/lease-request-book.ts,src/leasing/wait-queue.ts,src/leasing/create-leasing.tssrc/daemon/dispatcher.tssrc/cli/index.tsui/src/model/waiting.ts,ui/src/screens/waiting/index.tsxe2e/waiting-requests.test.ts: its five line patterns (F36) gain the reason suffix, and the single-host test pins the limits below- Docs:
docs/CLI.md,docs/HTTP-API.md,docs/CONSOLE.md,docs/CLIENT.md; manualssrc/leasing/AGENTS.md,src/contract/AGENTS.md
flowchart LR
cli["CLI (changed)"] --> d["worker dispatcher (changed)"]
http[HTTP] --> d
ui["console Waiting (changed)"] --> http
d --> lease["leasing: coordinator, book (changed)"]
lease --> plan["planner (changed)"]
plan --> cap[capacity]
d --> contract["contract (changed)"]
sequenceDiagram
participant G as Gateway or client
participant D as Worker dispatcher
participant C as Coordinator
participant P as Planner
G->>D: lease.request noWait
D->>C: request
C->>P: plan
P-->>C: no-capacity, ram-budget
C-->>D: NoCapacityError(ram-budget)
D-->>G: NO_CAPACITY, details.capacityReason ram-budget
G->>D: list.get requests
D-->>G: [{ stage: queued, reason: platform-running-limit, ... }]
Contract and event changes
waitingRequestSchemagainsreason?: enumof the seven values in feature Operational metrics kept over hours and shown in the console #623 Shared values "Waiting reason (worker)" (decided by the maintainer, 2026-10-18). A value outside the enum fails the parse, and so doesreasonon astartingentry.ErrorDetailsMap.NO_CAPACITYbecomes{ capacityReason?: enum of the four };NO_CAPACITYjoinsCODES_WITH_DECLARED_DETAILS.- New error code
PROVISION_FAILED: kinddomain,httpStatus500,cliExitCode1 (F38); no details; feature Operational metrics kept over hours and shown in the console #623 Shared values "PROVISION_FAILED" (decided by the maintainer, 2026-10-18). - This PR bumps the protocol by one,
minwith it (F31; feature Operational metrics kept over hours and shown in the console #623 Shared values "NO_CAPACITY detail"). - Events: none.
Other code on the same state
Searches: readers of queue.pending(), queue.list(), .places() outside tests (4 hits in src/leasing, 5 in src/gateway); writers of waiter fields in lease-acquisition-coordinator.ts.
- Warm pool
waitingDemand(lease-acquisition-coordinator.ts:621-635) and#heldByOlder(:729-738): read spec and state, not the reason. No conflict. cancelPending(:609-619): rejects a queued waiter; it leaves the list (F17), and now wakes the queue (F34). A wake only plans the head and physical waiters under the lock, as every wake does.- The gateway (F18): shares the book and queue code. Its own entries carry no
reason; a worker's entries keep theirs (F35). - A waiting request says why each worker cannot take it, on a gateway #637: adds
workersto the same schema and readsdetails.capacityReason, and tells a worker'sPROVISION_FAILEDfromNO_CAPACITY(decided by the maintainer, 2026-10-18). A waiting request says why each worker cannot take it, on a gateway #637 depends on this task. - The event history is a SQLite file, history.db #629,
usage.getandsimlock statsfrom the projections #631, A quarantined device says what failed #635 each bump the protocol. The PR that merges second bumps again. - Overview and Waiting: a range picker, wait p95, leases granted and three charts #633: adds the wait p95 to the Waiting page. Other lines of the same screen; whichever merges second takes the base branch.
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
list.get requests |
ok | Each queued entry with its latest reason |
as today |
list.get requests |
client on another protocol | PROTOCOL_VERSION_UNSUPPORTED at hello, as today; existing hello tests cover it |
as today |
lease.request noWait |
capacity refusal | NO_CAPACITY with details.capacityReason |
none |
lease.request noWait |
other refusal (F1, F8) | NO_CAPACITY, no details |
none |
lease.request, noWait or not, probe included |
second provisioning failure | PROVISION_FAILED with the driver's message; nothing listed; the queue wakes (decided by the maintainer, 2026-10-18) |
none |
lease.request noWait |
replayed by idempotency key | stored code, no details (F32) | none |
Console GET /v1/lease-requests |
unknown reason word | raw word in the Why row | none |
- Restart mid-step: the reason is in memory; the restart fails the request (F16). Nothing left behind; no test needed.
- Two callers at once: a listing reads the waiter's reason as the last plan left it. One thread; no torn read; no test needed.
Rules in play
architecture.md10: "A rule is enforced in exactly one place." The reason comes from the plan; each text table is checked against the contract's enum by a test; the capacity list is checked by the compiler.architecture.md16: "Policy is a pure function". The planner returns the reason; it stores nothing.architecture.md8: "The daemon owns all state; the CLI is a thin client." The CLI only prints the field.safety.md10: "Anything that arrives over the wire is a claim".reasonandcapacityReasonare closed enums in the contract.src/contract/AGENTS.md: "Every change to a contract shape bumps it by one in the same PR."testing.md6, 7: throughcreateLeasing, and the planner's own test for its many cases.documentation.md3: no repo path in what the CLI prints.
Tests
Seam: createLeasing through createTestEngine (src/leasing/testing.ts) and its waitingRequests(), which list.get requests returns (F21); model on src/leasing/create-leasing.test.ts:3757. The planner's cases in src/leasing/acquisition-planner.test.ts.
- Planner: each blocked path returns its reason: not tried by the planner; warm-pool boot
waiting-for-device; boot refusedram-budget,global-running-limit,platform-running-limit; provision refuseddevice-limit,ram-budget,global-running-limit,platform-running-limit; victim claim taken atdevice-limitand atplatform-running-limit, with the eviction plans carrying it; physical fits held only for an older requestbehind-earlier-request; boot claim takenwaiting-for-device; physicalno-free-physical. Extend "waits or rejects when policy cannot reserve capacity" (acquisition-planner.test.ts:211). - The same reasons come back on
no-capacityundernoWait. - A queued request at the iOS running limit lists
reason: "platform-running-limit"; a second one behind it listsbehind-earlier-request; the stored request record has no reason. - The reason follows the latest attempt: when the head is granted, the request behind it becomes head and lists its own new reason, not
behind-earlier-request. - A first provisioning failure still plans again at once, and a success then grants (decided by the maintainer, 2026-10-18).
- A second provisioning failure rejects the request with
PROVISION_FAILEDand the driver's message; nothing stays listed for it; the request behind it plans again and lists its own reason (decided by the maintainer, 2026-10-18). - A probe (
fleetRequestIdset) whose provisioning fails twice answersPROVISION_FAILED, notNO_CAPACITY(decided by the maintainer, 2026-10-18). - Cancelling the queued head makes the request behind it plan again and list its own reason, not
behind-earlier-request. - A request while the warm pool boots a fitting device lists
waiting-for-device. - A failed eviction lists the capacity reason that asked for it; a failed physical grant lists
no-free-physical; a request whose only match is reclaiming lists the planner's capacity reason. - A waiting physical request with every fit leased lists
no-free-physical(physical-leasing.test.ts:582). - A
startingrequest lists no reason: one being provisioned, and one held at its first plan. - A gateway's own queued entry lists no
reason: extendsrc/gateway/fleet-coordinator.test.ts:4309. A gateway'slist.get requestskeeps a worker's own entry'sreason: extendsrc/gateway/dispatcher.test.ts:1436. - Dispatcher: a
noWaitlease.requestrefused at the RAM budget, at the device limit and at the platform running limit rejectsNO_CAPACITYwithdetails.capacityReasonof each; one behind an earlier request rejectsNO_CAPACITYwith no details; a replay of the RAM-budget request by its idempotency key rejectsNO_CAPACITYwith no details. Model onsrc/daemon/dispatcher.test.ts:473. - HTTP:
POST /v1/lease-requestswithnoWaitat the running limit answers 503 withcapacityReasonin the error body (src/http/app.test.ts). - Contract:
waitingRequestSchemaaccepts each of the seven reasons on aqueuedentry, and refuses"bogus"and any reason on astartingentry;NO_CAPACITYis inCODES_WITH_DECLARED_DETAILS(src/contract/schemas.test.ts,errors.test.ts). - Contract:
PROVISION_FAILEDis kinddomain, HTTP 500, exit code 1 (errors.test.ts) (decided by the maintainer, 2026-10-18). - CLI: a queued request prints
— <text>for every reason,platform-running-limitonce for iOS and once for Android, only the first letter lower-cased; a queued request with no reason and a starting one print as today. Extendsrc/cli/index.test.ts:6284. - CLI and console tables: each side's table has a text for every reason the contract's enum lists (one test in
src/cli, one inui/src/model). - Console model: the chosen row's Why is the text for every reason, the raw word for an unknown one, and absent with no reason (
ui/src/model/waiting.test.ts). - Console screen: the inspector shows a Why fact after Queue with the text, and no Why fact for a starting request (
ui/src/screens/waiting/waiting.test.tsx:76). - E2E, single host (
e2e/waiting-requests.test.ts:37-55), limits{ ios: { maxDevices: 2, maxRunning: 1 }, maxRunning: 2 }, one lease held: agent-b's line ends— full: running limit for iOS, agent-c's— behind an earlier request. - E2E, gateway (
:57-101): the worker's local-b line ends— full: device limit, on the worker and on the gateway; the fleet line is unchanged.
Done when
- The tests above pass.
- On the fake driver, with
ios.maxDevices2,ios.maxRunning1, globalmaxRunning2 and one iOS lease held, a secondsimlock leasewaits;simlock list --requestsprints its line ending— full: running limit for iOS(e2e/waiting-requests.test.ts:37-55), and the console's Waiting inspector shows the same text under Why. - With the same limits, one iOS lease held and no request waiting,
lease.requestwithnoWait: trueover the socket is the head, and answersNO_CAPACITYwithdetails.capacityReason: "platform-running-limit". docs/CLI.md,docs/HTTP-API.md,docs/CONSOLE.mdanddocs/CLIENT.mddescribe the reason and the detail.- The error tables in
docs/CLI.md,docs/HTTP-API.mdanddocs/CLIENT.mdlistPROVISION_FAILED(F39) (decided by the maintainer, 2026-10-18).
Out of scope
- A gateway's own entries and per-worker reasons (A waiting request says why each worker cannot take it, on a gateway #637).
- A reason on a
startingrequest. - Keeping reasons across a restart, or in events.
- Waiting only behind same-platform requests (A request waits only behind requests for the same platform #387).
- The gateway's own
NO_CAPACITYcarrying a capacity reason.
Depends on
Written by an agent.
Activity
feat: a waiting request says why it is not placed, on a worker (#636)
refactor: read the capacity reason through a function the dispatcher …
fix(leasing): a cancel wakes the queue and a twice-failed head holds …
test: the load-free list answer lets a read in flight at the test's e…
fix(leasing): a wake drives the first waiter that has not failed twic…
fix(leasing): a later wake can place a lone twice-failed waiter (#636)
V3RON commentedon Oct 18, 2026
Handoff
Done: PR #692 is green locally at 25dd426 (616/616 leasing tests). It has two review rounds and three fix-checks. R1–R5, X1, X2, X4 and X5 are fixed and verified.
Not done: F1. The PR stays a draft and is not in the stack yet.
Findings: F1 (blocking).
src/leasing/lease-acquisition-coordinator.ts:1192-1199: a wake that does not follow a failure drives onlyqueue.head. When two or more waiters have each failed two provisions, a release that fits the second one never places it. There is no test with more than one twice-failed waiter. Fix it the same way as the earlier X1/X3 fixes: drive the virtual waiters in line order and skip only the request that just failed.Blocked on: the maintainer. The decision taken in delivery (PR comment "Decision taken": a twice-failed request is not replanned by the wake that follows its second failure) keeps producing edge cases. Either confirm it, so F1 can be fixed as above, or choose the base behaviour, where every wake replans the head.
Written by an agent.
fix(leasing): a second provisioning failure rejects with PROVISION_FA…
fix(contract): PROVISION_FAILED exits 1, not the lease-ended 14 (#636)
docs(events): a probe's second failed provision answers PROVISION_FAI…
feat(history): history.db, usage projections, simlock stats, GET /v1/…