Repository navigation
Operational metrics kept over hours and shown in the console #623
Description
Request: #612
Problem
The console can only show what happened in the last hour, and only what it saw itself. The daemon keeps no figures: the console builds its charts by counting the events it holds, so a reload starts them from zero. Several figures an operator needs are not sent at all: how long requests wait, why a waiting request is not placed, and why a device was quarantined.
Working the figures out from the event history on every read does not scale. The history can hold about a million events, and one full read takes more than a second. It also keeps only seven days.
Who it is for
Operators of a device cloud, one host or a gateway with workers. They want to know whether they have enough capacity, why agents wait, and why a device was taken out of use. A team lead deciding whether to buy another Mac. Someone tuning capacity, the warm pool or lease times who wants to see the effect.
Outcome
- The Overview shows how long requests waited (95th percentile) and how many leases were granted, for a chosen range: the last hour, 6 hours, 24 hours or 7 days. The same range drives three charts: leases held, requests in the queue, and device slots in use. A reload or a daemon restart shows the same figures.
- The Waiting page shows wait p95 for the last hour. Its inspector says, for a waiting request, why it is not placed yet. On a gateway there is one line per worker ("Full: running limit", "Drained", "OS not installed"); on a single host there is one line, the host.
simlock list --requestsprints the same reason. - The Attention inspector says why a device was quarantined: what failed ("Purge failed", "Shutdown failed", "Delete failed", "Stalled while provisioning") and the tool's short error message. It still says so after a restart.
simlock stats,GET /v1/statsand a new Usage page in the console show the full figures for a window. These are requests, grants and how each was served, wait, held time and turnaround, provisioning and boot time, rejections by reason, device incidents, and use per platform, per worker and per requester. A CSV export gives one row per request.- Figures reach back 90 days by default. The event history itself is kept 7 days, as today.
- A 7-day figure comes back within half a second, even with 90 days of figures kept.
- Nothing slows a lease: recording the figures adds about 30 µs per event, written where the event is written today.
How it works for the user
flowchart LR
O[Operator opens Overview] --> P{Range: 1h / 6h / 24h / 7d}
P --> C[Tiles and 3 charts for that range]
C -->|reload or daemon restart| C
O --> W[Waiting inspector]
W --> R["One reason per worker:<br/>Full: running limit / Drained / ..."]
O --> A[Attention inspector]
A --> Q["Purge failed:<br/>DriverError: simctl erase exited 164"]
P -->|range older than the kept figures| N["Note: figures cover from <time>"]
Before:
$ simlock list --requests
Request req_8f2: tok_ci, iPhone 16, queued, waiting 42s
$ simlock stats
error: unknown command 'stats'
After:
$ simlock list --requests
Request req_8f2: tok_ci, iPhone 16, queued, waiting 42s — full: running limit for iOS
$ simlock stats --since 24h
Usage from 2026-10-15T13:00:00.000Z to 2026-10-16T13:00:00.000Z
Requests: 214 (209 granted, 5 rejected)
Sources: warm 160, booted 41, provisioned 8
Wait: p50 400ms, p95 38s, max 2m10s (214 samples)
Slots: peak 8 of 8, mean 5.2
...
A history that was damaged at start (once):
$ simlock status
...
History: the event history was damaged and set aside at 2026-10-16 09:12; a fresh one was started.
Examples
The reason a waiting request gives, by what stops it:
| Situation | Today | After |
|---|---|---|
| Single host, iOS running limit reached | nothing | Full: running limit for iOS |
| Single host, request behind another | nothing | Behind an earlier request |
| Gateway, worker drained | nothing | that worker's line: Drained |
| Gateway, worker without the OS | nothing | that worker's line: OS not installed |
| Gateway, worker answered full without a capacity reason | nothing | Full |
| Gateway, worker on an older Simlock | nothing | Protocol mismatch |
Request already being placed (starting) |
nothing | no reason; the stage says it |
Ranges asked for:
| Ask | Today | After |
|---|---|---|
| Console range 1h | counted from events the page holds | from the daemon |
| Console range 7d | not offered | from the daemon |
simlock stats --since 30d on a host upgraded yesterday |
no command | figures, with "Figures cover from " |
simlock stats --from <100 days ago> --to <95 days ago> |
no command | error: the history does not reach back that far |
What could go wrong
- The figures could undercount after a crash or a full disk. Likely only on a full disk; the status says the history is not being written.
- A gateway's charts have gaps for the time a worker was disconnected. Expected; a gap shows as no data, not zero.
- A damaged history file is set aside and figures restart empty. Rare; the status says so once, and the old file is kept.
- After upgrading, figures start empty: the old event file is not imported.
Words used
- Event history: the record of every fact Simlock emitted, e.g.
lease.granted. Kept 7 days. - Figures: numbers worked out from the history, e.g. wait p95 for the last 24 hours. Kept 90 days.
- p95 (95th percentile): the wait that 95 of 100 requests stayed under, e.g. "p95 38s".
- Range / window: the span the figures cover, counted back from now, e.g. "last 24 hours".
- Reason (waiting): the first check that stopped a waiting request at its last placement attempt, e.g. "Full: running limit for iOS".
- Quarantine: a device taken out of use after cleaning it failed; Simlock retries and gives up after three tries.
- Set aside: a damaged history file renamed to
history.db.damaged-<time>and kept, never deleted.
Non-goals
- Moving the registry, tokens or worker list into SQLite (Daemon state stores in SQLite (state.db) #624).
- Importing the old event file on upgrade.
- Alerting, thresholds, notifications, billing.
- A Prometheus or OpenMetrics endpoint.
- Explicit start and end dates in the console; the CLI and the route take them.
- Measuring what happens inside a device.
- Recording anything about a person beyond the requester id and token label events already carry.
Completion conditions
- On the fake driver, after a scripted run, the Overview shows wait p95 and leases granted, and three charts for the picked range (1h, 6h, 24h, 7d). After a reload and after
simlock daemon stopandstart, the same range shows the same numbers. - The Waiting page shows wait p95 for the last hour. For a request waiting on a full single host, its inspector shows one line with the capacity reason. On a gateway with two workers, one drained, it shows two lines, one saying
Drained.simlock list --requestsprints the same reason. - A device quarantined after a failed purge shows
Purge failedand the error text, cut to 300 characters, in the Attention inspector, insimlock status --json, and after a daemon restart. simlock stats --since 24h --json,GET /v1/stats?since=24hand the Usage page for 24 hours show the same figures, and those match a count by hand fromsimlock events --since 24h.simlock stats --csvwrites one row per lease request in the window.- A fresh install writes
history.dband noevents.jsonl.simlock events --since 1hworks with and without a running daemon. - Events older than
eventLog.retentionare gone after the next prune; figures older than that are still answered untilmetrics.retention. - A
history.dbcorrupted while the daemon is stopped is renamed aside at the next start. Leasing works, andsimlock statussays the history was set aside. docs/CLI.md,docs/HTTP-API.md,docs/CONSOLE.md,docs/CONFIGURATION.mdanddocs/EVENTS.mddescribe the commands, the route, the pages, the keys and the history file.
Open questions
Decisions
- ADR 0032 — The event history is a SQLite database, with projections beside it (feat(history): history.db, usage projections, simlock stats, GET /v1/stats and console figures (#623) #626):
history.dbreplacesevents.jsonl; each event and its projection rows commit in one transaction; events 7 days, figures 90 days; a damaged file is set aside; a broken projection is rebuilt at start; a gateway keeps its own. Supersedes ADR 0016 §1 and §4, and narrows ADR 0006 §2. - ADR 0016 §2, §3, §5, §7, §8 and ADR 0021 §5 still hold: the event facts the figures read,
coversFromandHISTORY_NOT_KEPT, labels, bucket widths, and how a gateway counts fleet requests. - Not ADRs, decided in this session: the reason list below, the four quarantine kinds, and a worker's
NO_CAPACITYanswer carrying its capacity reason. - Supersedes feature Usage metrics: utilisation, wait times and provisioning durations from the event history #329 and its open tasks
usage.getandsimlock stats: the figures from the event history, on a worker and a gateway #347,GET /v1/statsand thesimlock stats --csvledger #348 and Console Usage view with utilisation and waiting charts #349 (draft PR feat(usage): usage.get and simlock stats, the figures from the event history #356). Their figures are carried into this feature.
Shared values
| Value or state | Rule | Written by | Read by | Source |
|---|---|---|---|---|
history.db |
<data dir>/history.db, the only event history. events.jsonl is never written or read again; an existing one is left untouched. |
#629 | #630, #631, CLI events |
ADR 0032 §1 |
events table |
One row per envelope, unique by id; a second insert of the same id is ignored. Indexed by timestamp. |
#629 | #630 (rebuild), events.replay |
ADR 0032 §1, §5 |
| One transaction per event | The event row and every projection change for it commit together, synchronously, in the bus subscriber. WAL, synchronous=NORMAL. |
#629 (writer), #630 (projection step inside it) | — | ADR 0032 §2 |
| Write failure | First failing write logs once (error) and stops all history writes until restart; the daemon keeps running. |
#629 | #629 (status.get) |
ADR 0032 §2; today src/bus/event-file.ts:134 |
status.get history |
{ opened: boolean, writing: boolean, setAsideAt?: number, rebuiltAt?: number, rebuildFailed?: true } (epoch ms). Sent whenever known, while starting too. opened false until the file is open, and when it could not be opened. writing false after a write failure or when not opened. setAsideAt set when this start renamed a damaged file. rebuiltAt set when this start rebuilt projections; rebuildFailed when that rebuild failed (the projections are then empty for the window the events cover). |
#629 (opened, writing, setAsideAt), #630 (rebuiltAt, rebuildFailed) |
#629 CLI simlock status, #631 (opened false → HISTORY_UNAVAILABLE) |
this session |
| Status lines | History: the event history is not being written; see daemon.log. / History: the event history was damaged and set aside at <local time>; a fresh one was started. / History: figures were rebuilt from the event history at <local time>. / History: figures could not be rebuilt; see daemon.log. Printed by simlock status only when the field is set. |
#629 (first two), #630 (last two) | — | this session |
| Damaged file | PRAGMA quick_check not ok, or open fails → rename to history.db.damaged-<epoch ms> (with its -wal and -shm), never delete, open a fresh file. |
#629 | — | ADR 0032 §4 |
| Config keys | eventLog.retention (ms, default 7 days) on event rows. eventLog.maxBytes (default 256 MiB, at least 16 MiB) on the stored size of the event rows, oldest first; projection rows do not count. metrics.retention (ms, default the larger of 90 days and eventLog.retention) on projection rows; a value shorter than eventLog.retention fails the load naming the key. A value out of bounds fails the load naming the key. eventLog.rotateBytes is removed: the daemon warns about it as an unknown key, and simlock config set drops it from the file when it writes, saying so once. |
#629 (eventLog, rotateBytes), #630 (metrics) | #629, #630 | ADR 0032 §3; src/core/config.ts:756-760, :832; src/cli/index.ts:339-351 |
| Prune | At daemon start, after the socket claim, and every hour: event rows by eventLog.retention, then the oldest event rows until their stored size fits maxBytes; projection rows by metrics.retention; then an incremental vacuum and wal_checkpoint(TRUNCATE) give the space back. A lock held by another process waits up to 5 s and is never damage. |
#629 (events, vacuum), #630 (rows) | — | ADR 0032 §3, §4 |
| Request row | One per lease.requested, and one per lease.rejected with no lease.requested (flag refusedAtAdmission, requestedAt = the rejection's time). Fields: requestId, requester, platform, model?, workerId?, requestedAt, grantedAt?, rejectedAt?, reason?, source?, leaseId?, workerLeaseId?, heldMs?, endedAt?, endReason?, probe, declined? (a worker's row for a gateway dispatch, fleetRequestId set). A probe's lease.declined sets rejectedAt, reason and declined: true. On a gateway, heldMs is measured on the worker's clock (ADR 0021 §5); on a worker it is endedAt - grantedAt. |
#630 | #631, #632 (ledger) | ADR 0032 §2; ADR 0016 §2; ADR 0021 §5 |
| Minute row | Per minute (start epoch ms, floor) and workerId ("" on a worker for itself, "*" for a gateway's fleet total) and platform (ios, android, or all for the worker's or fleet's total). Values leasesHeld, slotsUsed, slotsMax, ramUsedBytes?, ramLimitBytes?, queueDepth, waiting, each as peak, last and integral (value × ms in the minute), plus knownMs, the ms of the minute with a known value. Queue depth and RAM exist only on all rows. all and * rows are updated on every step, so their peak is the true total peak. A minute row is written only for a minute in which a value changed. A reader carries the last value forward through minutes with no row, up to to. A daemon stop, a daemon start and, on a gateway, worker.disconnected write a gap marker; nothing is carried across one, so the gap is no data. |
#630 | #631 | ADR 0032 §2, §6 |
| Device fact row | One per device.provisioned (duration), device.ready with bootDuration, device.quarantined, device.recovered, device.quarantine-recovered, device.recovery-failed, device.quarantine-abandoned, device.quarantine-stranded, and lease.declined: at, kind, durationMs?, platform?, workerId?, deviceId?. Kept for metrics.retention. |
#630 | #631 | ADR 0032 §2 |
Device event platform |
platform: "ios" | "android" added to the payload of lease.granted, device.ready, device.quarantined, device.recovered, device.quarantine-recovered, device.recovery-failed, device.quarantine-abandoned and device.quarantine-stranded (additive). A device fact row takes its platform from its event; an event from before this field has none. |
#635 (device.quarantined), #630 (the others) |
#630 | this session; src/bus/index.ts:115-176 |
| Projection version | An integer per projection in the database; an upgrade that changes the rows ships a SQL migration. A failed migration, or a stored version newer than the code's, stops the daemon from starting with an error naming the projection and the version. A failed projection check at the right version rebuilds at start from events, in batches that let other work run; rows older than the oldest event are kept. | #630 | #630 | ADR 0032 §4 |
usage.get |
Role admin, input { from, to, includeLedger? } epoch ms, from < to, to - from ≤ 90 days; from is floored to the bucket width, to is not rounded. Output window, coversFrom, partial, bucketMs, totals, platforms, workers, requesters, series[], and requests[] only when includeLedger. A series point is { at (bucket end), leasesHeld, slotsUsed, slotsMax, ramUsedBytes?, queueDepth, waiting } from the all rows (* on a gateway), each the peak in its bucket, null where there is no data. Means are time-weighted from the integrals. Percentiles by nearest rank; null over an empty set. Reads run on a worker thread with a read-only connection; one read per window in flight, a second asker for that window waits for it. A 7-day answer over 90 days of rows returns within 500 ms and never holds up a lease. |
#631 (#632 adds includeLedger, requests[]) |
#632, #633, #634 | ADR 0016 §5, §8; ADR 0032 §5 |
HISTORY_NOT_KEPT |
domain error, CLI exit 12, HTTP 422, details { oldestTs }; when the window ends before the oldest projection row. |
#631 | #632 | ADR 0016 §5 |
history.usage |
The handle #630 hands out and #631 reads, on the read thread: oldestTs(), requestRows({from,to}), minuteBuckets({from,to,bucketMs,workerId,platform}) (peak = max of minute peaks, integral = sum, knownMs = sum, carrying across empty minutes), deviceFacts({from,to}). |
#630 | #631, #632 | ADR 0032 §5 |
HISTORY_UNAVAILABLE |
domain error, CLI exit 1, HTTP 503, as DOCTOR_UNAVAILABLE; usage.get answers it when history.db could not be opened. |
#631 | #632 | src/contract/errors.ts:338-344 |
| Bucket width | Smallest of 1 min, 5 min, 15 min, 1 h, 6 h, 1 day that keeps ≤ 200 points. | #631 | #633 (axis labels) | ADR 0016 §8 |
GET /v1/stats |
?since=<duration> or ?from=<ISO>&to=<ISO> (to defaults to now); neither = last 24 h; &ledger=1 adds requests[], any other ledger value is 400. Operator token; agent token 403. The ledger holds at most 100,000 rows, the newest kept, with ledgerTruncated: true when cut; simlock stats --csv says so on stderr. CSV cells starting with =, +, - or @ get a leading '. |
#632 | #633, #634 | this session |
| Console ranges | 1h, 6h, 24h, 7d, in the URL as ?range=; default 1h. Fetched as the console fetches every route (ADR 0013 §3: on events and every second, coalesced). The Waiting page's wait p95 is always the last hour. |
#633 | #634 | ADR 0013 §3; this session |
| Waiting reason (worker) | waiting[].reason on a queued entry, never on starting: behind-earlier-request, device-limit, ram-budget, global-running-limit, platform-running-limit, waiting-for-device, no-free-physical. On a gateway, a worker's own local waiting entries keep their reason in the gateway's list. From the request's latest plan; a failed eviction keeps the capacity reason that asked for it; a match being cleaned gives the capacity reason; a boot claim already taken gives waiting-for-device. |
#636 | #636 (CLI, console), #637 | this session |
| Waiting reason (gateway) | waiting[].workers[]: { workerId, reason, capacityReason? }, one per worker the gateway holds a view of (disconnected and incompatible included). reason: disconnected, incompatible, not-ready, unhealthy, drained, busy, no-driver, unknown-model, runtime-missing, capability-missing, full, no-free-physical, not-chosen. The first failing check in stage order wins; drained comes before a remembered refusal. From the request's latest walk. capacityReason only with full; a value outside the four reads as plain full. |
#637 | #637 (CLI, console) | this session |
| Reason text | behind-earlier-request "Behind an earlier request"; device-limit "Full: device limit"; ram-budget "Full: RAM budget"; global-running-limit "Full: running limit"; platform-running-limit "Full: running limit for <iOS|Android>"; waiting-for-device "A matching device is starting"; no-free-physical "Every matching device is leased or absent"; disconnected "Not connected"; incompatible "Protocol mismatch"; not-ready "Not ready yet"; unhealthy "Not running"; drained "Drained"; busy "Busy with its own queue"; no-driver "No <iOS|Android> support"; unknown-model "Model not offered"; runtime-missing "OS not installed"; capability-missing "Missing capability"; full "Full", or "Full: " + the capacity reason's text after "Full: "; not-chosen "Not chosen this time". The CLI prints a worker reason after —, and each gateway line indented as <worker label or id>: <text>, with only the text's first letter lower-cased. |
#636 (worker), #637 (gateway) | console, CLI | this session |
NO_CAPACITY detail |
A worker's NO_CAPACITY error carries details.capacityReason, one of the four capacity reasons, when it has one; HTTP's 503 body carries it too. A contract shape change: the PR bumps the protocol by one. |
#636 | #637 | this session; src/contract/AGENTS.md:59 |
PROVISION_FAILED |
Error code, kind domain, HTTP 500, CLI exit code 1. A worker rejects a request whose new device fails to provision twice with it, the driver's error as its message; a probe that fails twice answers it too. A gateway walks on to the next worker, and rejects with it and the last worker's message when every worker that could take the request answered it. |
#636 | #637, CLI, HTTP | maintainer, 2026-10-18; src/contract/errors.ts:226-386 (decided by the maintainer, 2026-10-18) |
| Quarantine cause | Device record and status.get device: quarantineCause { kind: "purge-failed" | "shutdown-failed" | "delete-failed" | "stalled", stalledState?: "provisioning" | "reclaiming", message? }. Written once, on entry; retries leave it. message is the driver's error summary, cleaned like capabilityReason, cut to 300 characters with a final … inside them when cut, left out when empty; stalled has none. status.get and list.get leave message out for an agent-role caller; operators and the host CLI see it. Dropped when the device leaves quarantine. device.quarantined gains the same three fields. |
#635 | #635 (console, CLI) | this session; src/core/stable-error.ts:10, src/core/doctor.ts:78 |
Tasks
flowchart LR
T7["#635 quarantine cause"] --> T8["#636 why waiting: worker"] --> T9["#637 why waiting: gateway"] --> T1["#629 history.db"]
T1 --> T2["#630 projections"] --> T3["#631 usage.get + stats"] --> T4["#632 /v1/stats + CSV"]
T4 --> T5["#633 Overview range + charts"] --> T6["#634 Usage page"]
Seven tasks change contract shapes and each bumps the protocol, so they run one after another (#635 → #636 → #637 → #629 → #630 → #631 → #632). The small ones go first, by the maintainer's choice. #633 and #634 change only the console.
- The event history is a SQLite file, history.db #629 The event history is a SQLite file, history.db — after it, the event history lives in
history.db, read by index, set aside when damaged.
Risk: leasing keeps working but events stop being recorded, andsimlock eventsshows nothing new. - Projections: request, minute and device fact rows #630 Projections: request, minute and device fact rows — after it, figures are kept 90 days in rows written with each event.
Risk: figures are wrong or missing while leases work. usage.getandsimlock statsfrom the projections #631usage.getandsimlock statsfrom the projections — after it,simlock statsprints the figures for any window.
Risk:simlock statsprints wrong numbers or fails.GET /v1/statsand thesimlock stats --csvledger #632GET /v1/statsand thesimlock stats --csvledger — after it,GET /v1/statsand a CSV export give the same figures.
Risk: the route or the CSV disagrees with the CLI.- Overview and Waiting: a range picker, wait p95, leases granted and three charts #633 Overview and Waiting: a range picker, wait p95, leases granted and three charts — after it, the Overview shows wait p95, leases granted and three charts for 1h to 7d.
Risk: the Overview shows empty or stale charts. - Console Usage page #634 Console Usage page — after it, the console's Usage page shows every figure for a range.
Risk: the Usage page shows numbers that differ fromsimlock stats. - A quarantined device says what failed #635 A quarantined device says what failed — after it, a quarantined device says what failed.
Risk: a quarantined device shows no or the wrong cause. - A waiting request says why it is not placed, on a worker #636 A waiting request says why it is not placed, on a worker — after it, a waiting request on a host says why it waits.
Risk: a waiting request on a single host shows no or the wrong reason. - A waiting request says why each worker cannot take it, on a gateway #637 A waiting request says why each worker cannot take it, on a gateway — after it, a waiting request on a gateway says why each worker cannot take it.
Risk: a gateway's per-worker lines are missing or wrong.
Completion conditions and the task that delivers each:
- Overview tiles, three charts, range picker, same after reload and restart → Overview and Waiting: a range picker, wait p95, leases granted and three charts #633.
- Waiting p95 → Overview and Waiting: a range picker, wait p95, leases granted and three charts #633. Single-host reason → A waiting request says why it is not placed, on a worker #636. Gateway per-worker reasons → A waiting request says why each worker cannot take it, on a gateway #637. CLI reason → A waiting request says why it is not placed, on a worker #636, A waiting request says why each worker cannot take it, on a gateway #637.
- Quarantine cause in the Attention inspector,
status --jsonand after restart → A quarantined device says what failed #635. simlock stats,GET /v1/statsand the Usage page agree →usage.getandsimlock statsfrom the projections #631,GET /v1/statsand thesimlock stats --csvledger #632, Console Usage page #634.- CSV →
GET /v1/statsand thesimlock stats --csvledger #632. history.dbreplacesevents.jsonl;simlock eventswith and without a daemon → The event history is a SQLite file, history.db #629.- Event retention and figure retention → The event history is a SQLite file, history.db #629, Projections: request, minute and device fact rows #630.
- Damaged file set aside → The event history is a SQLite file, history.db #629.
- Docs → each task for the part it adds.
Open work in the same area:
- Usage metrics: utilisation, wait times and provisioning durations from the event history #329,
usage.getandsimlock stats: the figures from the event history, on a worker and a gateway #347,GET /v1/statsand thesimlock stats --csvledger #348, Console Usage view with utilisation and waiting charts #349, PR feat(usage): usage.get and simlock stats, the figures from the event history #356 — the same figures from the event file. Superseded by this feature and closed. PR feat(usage): usage.get and simlock stats, the figures from the event history #356's figure code is reused where it fits (usage.getandsimlock statsfrom the projections #631). - Daemon state stores in SQLite (state.db) #624 — state stores in SQLite. Will use the SQLite port The event history is a SQLite file, history.db #629 adds. No Depends on: it is a request, not yet specified.
- Gateway republishes a worker-pushed request.dispatched into its history #434 — a gateway republishes a worker-pushed
request.dispatchedinto its history. Projections: request, minute and device fact rows #630 never readsrequest.dispatchedas a request fact; fleet outcomes come from the gateway's ownrequest.grantedandlease.rejectedonly (ADR 0021 §5). No Depends on. - Console fleet totals count a starting worker's leases and devices as zero #406, A gateway hides an owner's leases on a worker that is starting #405 — gateway fleet totals during a worker start. They touch the Overview's current-state tiles, not the range figures Overview and Waiting: a range picker, wait p95, leases granted and three charts #633 adds. No Depends on.
- Every device shape says when the device entered its state #318 / PR feat(core): every device shape says when the device entered its state #324 — device shape gains when it entered its state. Same schema as A quarantined device says what failed #635, different fields. They run at once; whichever merges second takes the base branch.
- Console: time in state for every device #319 — console time in state. Changes the Attention inspector's device facts, as A quarantined device says what failed #635 does. It is a draft; no Depends on.
- A request waits only behind requests for the same platform #387 — a request waits only behind same-platform requests. It would change when A waiting request says why it is not placed, on a worker #636 gives
behind-earlier-request. A request, not specified; no Depends on. - A waiter that times out while its lease is being written gets a timeout, and the lease is kept #402 — a waiter timing out mid-grant. Gives one wrong wait sample at that edge; no Depends on.
- Android: stream a leased physical phone's screen #618, iOS: stream a leased physical iPhone's screen #619 — physical screen streams. Touch the Leases inspector and stream module, not these screens. No Depends on.
Written by an agent.
Activity
usage.getandsimlock statsfrom the projections #631GET /v1/statsand thesimlock stats --csvledger #632V3RON commentedon Oct 16, 2026
Spec updated: all business sections written; Decisions (ADR 0032, PR #626), Shared values and Tasks (#629–#637) added. Supersedes #329.
Written by an agent.
V3RON commentedon Oct 16, 2026
Spec updated after the first spec check: Shared values (history status, size cap, minute rows, projection version, reads, quarantine message, wait reasons, platform on events, history.usage) and the task order; every task body fixed for its blocking findings.
Written by an agent.
V3RON commentedon Oct 18, 2026
Delivery run
Merged: none. The feature's stack merges together at the end: #626 (ADR) and #690 (#635) are in it and ready.
Parked: #636 — a blocking finding (F1, the queue wake with two twice-failed waiters) is still open after the review cap; it needs a maintainer decision.
Waiting: #637 — on #636; #629 — on #637; #630 — on #629; #631–#634 — each on the task before it.
Decided: #636 — does the wake after a second provisioning failure replan that request? → no (the PR comment on #692 says so; still pending the maintainer).
Asleep: 2 min
Written by an agent.
V3RON commentedon Oct 18, 2026
Delivery run (2)
Merged: none. The stack merges at the end: #626, #690 (#635) and #692 (#636) are in it and ready.
Parked: #637 — a src finding confirmed by the fix-check (a disconnected worker counts as able to take the request); PR #693 is still a draft.
Waiting: #629 — on #637; #630–#634 — each on the task before it.
Decided: #636 — second provisioning failure →
PROVISION_FAILED, exit 1 (by the maintainer); #637 — the gateway walks on afterPROVISION_FAILEDonly before progress (in delivery, pending the maintainer).Written by an agent.
V3RON commentedon Oct 19, 2026
Delivery run
Merged: none. The stack merges at the end: #626, #690 (#635), #692 (#636), #693 (#637) and #694 (#629) are ready.
Parked: #630 — the session budget ran out mid-green; the handoff on #630 lists the five test fixes.
Waiting: #631–#634 — each on the task before it.
Decided: #636 — second provisioning failure →
PROVISION_FAILED, exit 1 (by the maintainer); #637 — the gateway walks on afterPROVISION_FAILEDonly before progress (pending the maintainer); #630 — where a red test disagrees with the spec, the spec wins.Asleep: 269 min
Written by an agent.
V3RON commentedon Oct 19, 2026
Delivery run
Merged: none
Parked: #630 — the replay treats a falling seq as a new run, which wipes state when worker clocks are skewed; how to find a run boundary needs a decision (PR #695)
Waiting: #631 — on #630, #632 — on #631, #633 — on #632, #634 — on #633
Written by an agent.
docs(daemon): manual back to 150 lines (#623)
feat(history): history.db, usage projections, simlock stats, GET /v1/…
github-actions commentedon Oct 19, 2026
All 9 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.github-actions commentedon Oct 19, 2026
All 9 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.V3RON commentedon Oct 19, 2026
Delivery run
Merged: #626 (ADR 0032 and #629–#637, as one PR after a stack sync made the stack's PRs merge into each other)
Parked: none
Waiting: none
Decided:
seqin the catch-up,rowidin the rebuild), decided by the maintainerusage.getandsimlock statsfrom the projections #631: running minute → left out of the figuresusage.getandsimlock statsfrom the projections #631: what counts as a boot →bootDurationabove 0usage.getandsimlock statsfrom the projections #631: timing → bound on a running read threadGET /v1/statsand thesimlock stats --csvledger #632: ledger timing bound → 2.5 sWritten by an agent.