Skip to content

state.db opens and checks at start, and the worker's registry lives in it #761

Closed

Description

@V3RON

Part of #754.

Scope

A worker keeps its devices, leases, lease requests and components in state.db, one row per record in real columns, and writes only the rows a change touches. Each daemon opens and checks its own state.db at start. It refuses to start with state.json in its folder, with a damaged file, or with a schema version newer than its own. This task also builds the state module the later tasks add their owners to, the DeviceDataTable seam, and the fake driver's table. The iOS and Android tables are task 2. The stack merges together (delivery rule 15), so a real driver's devices are not storable until task 2 lands on top.

Kept whole, at about 40 test files: the registry cannot store a device without the state module, the seam and a device-data table, and every test that builds a registry switches to the new helper in the same change. Those tests also change their driverData to the fake's one shape, { fakeDeviceId }. While doing so, drop a test that only repeats what another proves, or that guards a race the synchronous commit removes. The maintainer asked for that pruning (spec session, 2026-10-20).

In short

The worker's registry moves from state.json to state.db. A renew, a grant or any other change commits as one SQLite transaction that writes only the rows it changed, so a renew writes one lease row however many lease requests are stored. Every refusal is the daemon's start error, logged as Daemon failed to start, exit 1. The exact messages are in the feature's Shared values.

flowchart LR
  A[startDaemon] --> B["openStateDatabase (new)"]
  B -->|old file, damage, lock, newer version| X[throw: Daemon failed to start]
  B --> C["Registry.load from rows (changed)"]
  C --> D[createCore, leasing, server as today]
  D --> E["each change: one transaction, changed rows only (changed)"]
Loading
  • device-data table: a driver's own table, one row per device, e.g. fake_device_data(device_id, fake_device_id) for driverData: { fakeDeviceId: "d1" }.
  • replay: a repeat of a lease.request with the same Idempotency-Key. After this task, its answer's device is the device as it is at replay time. Its lease, environment and timing are as first answered.

If this goes wrong, a user would notice a worker that forgets or changes a device, lease or request after a restart, or one that will not start.

Technical spec

Facts

Commit 8db8112b.

# Fact Source
F1 state.json is written whole on every commit through writeFileAtomic, mode 0600, holding devices, physicalDevices, leases, leaseRequests, components and unknown keys src/core/registry.ts:958-988
F2 Memory is assigned after the awaited write, then commit listeners run src/core/registry.ts:989-993
F3 Registry.load reads state.json if present; bad JSON throws Invalid JSON in registry state, a bad shape throws Invalid registry state src/core/registry.ts:213-215, :1002-1016
F4 Load-time fixes for old records: ownerId ?? requesterId, lastRenewedAt, ttlMs from defaultTtlMs, idChosenByRequester === true, warm → reclaiming, missing leaseIdentity → reusable, missing mode → full, retired keys featureProfile and lease mode, unknown fields kept per record src/core/registry.ts:1557-1571, :1379, :1471-1493, :1104-1163, :1017-1028
F5 defaultTtlMs is read on load only, by ADR 0004's record fix src/daemon/main.ts:258-261
F6 RegistryOptions: filesystem, clock, idGenerator, eventBus, statePath?, defaultTtlMs?, leaseIdentity?, leaseRequestLimits? src/core/registry.ts:51-75
F7 Every #commit caller: recordComponent, deleteComponent, registerDevice, transitionDevice, completeReclaimWithoutPurge, enterQuarantine, recordQuarantineRetryFailure, recoverFromQuarantine, strandQuarantine, deleteQuarantined, markForeignStateDetected, markForeignProvenanceDetected, clearForeignStateDetected, markRecoveryAttempt, clearRecovery, markDeviceMissing, endLeaseAndMarkDeviceMissing, endLeaseAndMarkDeviceAbsent, rewriteAbsentReason, enrollPhysical, reenrollPhysical, unenrollPhysical, createLease, beginRelease, renewLease, createLeaseRequest, settleLeaseRequest, failOpenLeaseRequests src/core/registry.ts:245-937 (grep #commit)
F8 Record fields: DeviceRecordFields (src/core/domain.ts:216-245), VirtualDevice (:247-255), PhysicalDevice (:257-266), VirtualSpec (:29-48), PhysicalSpec (:59-67), QuarantineCause (:208-212), LeaseRecord (:296-330), LeaseTiming, LeaseGrant (:333-345), LeaseRequestRecord (:377-403), DeviceRequest (src/core/driver.ts:19-31), ComponentRecord (src/core/registry.ts:91-96), ComponentReceipt = Readonly<Record<string, string>> (src/core/driver.ts:513) as cited
F9 stored() strips physical from a virtual record and its spec; withoutCauseUnlessQuarantined drops quarantineCause outside quarantined src/core/registry.ts:1181-1191
F10 enrollPhysical writes driverData: {}; PhysicalInspection has no driver data src/core/registry.ts:655; src/core/driver.ts:225-240
F11 Device records are never removed: unenrol and delete move them to deleted src/core/registry.ts:725-738; src/core/AGENTS.md "deleted records stay in the file"
F12 Lease-request retention: open kept, settled kept 10 min after settling, at most 10,000; pruned inside createLeaseRequest's commit src/core/lease-request-store.ts:48-71; src/core/config.ts:59, :64; src/core/registry.ts:890-901
F13 A replay answers the stored grant src/leasing/lease-request-book.ts:402-405; a request is found by grant.lease.id at :335
F14 lease.request's answer carries no driverData (grantedDeviceSchema); list.get devices (admin) and doctor orphans do src/contract/schemas.ts:260-263, :339-340; :194-200, :816-838
F15 Worker start order: Registry.load :261, loadInstanceId :276, discoverDrivers :283, createCore :340; createAuth before the mode branch :222; gateway branch :233; leftover sweep over dataDirectory and dirname(statePath) :473-478 src/daemon/main.ts
F16 A thrown start error is logged Daemon failed to start with message to the role's daemon.log, exit code 1 src/daemon/main.ts:1486-1493, :1503-1506
F17 SIMLOCK_DRIVERS_MODULE replaces discovery; the module must export createDrivers or start fails src/daemon/main.ts:1085-1092, :1295-1312; src/daemon/main.test.ts:2564
F18 Both fakes write driverData: { fakeDeviceId } src/core/fake-driver.ts:454, :544; e2e/fake-driver/fake-driver.ts:452, :566, :751, :769
F19 NodeSqliteOpener.open pre-creates the file 0600 unless read-only, then new DatabaseSync(path, { readOnly }); a missing module throws SqliteUnavailableError src/ports/sqlite.ts:28-39
F20 History's configureDatabase sets busy_timeout 5000, auto_vacuum=INCREMENTAL, WAL, synchronous=NORMAL, and also creates history's own tables; inTransaction runs a plain BEGIN; passesQuickCheck and isLockError are pure. All are imported only inside src/history src/history/database.ts:5-69
F27 Test driverData values today: {} 177×, { opaque: true } 4×, { driverOnly: "value" } 3×, null 2×, undefined 2× rg -o "driverData: [^,}]*" src --glob '*.test.ts' | sort | uniq -c
F28 Three test groups hold a registry write in flight with HeldWritesFilesystem to prove a transition committed meanwhile keeps its own src/core/testing.ts:62-96; src/core/doctor.test.ts:562-591, :2561-2591; src/leasing/lease-health-monitor.test.ts:694-750
F29 e2e may import only dist/<module>/index.js (past-index, past-index-unbuilt), and lint covers e2e .dependency-cruiser.mjs:21-35, :49-58; package.json:60
F30 More e2e reads of state.json: e2e/physical-enrollment.test.ts:669 reads enrolled apps; e2e/daemon-folders.test.ts:111 asserts the gateway folder has none as cited
F21 History's version messages src/history/versions.ts:27, :38
F22 Tests that build a registry: 28 files call Registry.load, 36 pass statePath; JSON reads of state.json in src/leasing/lease-request-book.test.ts:48, :245 and src/leasing/lease-reconciler.test.ts:375; restarts at src/core/registry.test.ts:195, :354, :535, src/leasing/create-leasing.test.ts:1724, :1767, src/daemon/main.test.ts:897, :971 rg -l "Registry.load" src, rg -l statePath src
F23 e2e helpers write and read state.json directly: seedPhysicalState (e2e/helpers/physical-state.ts:67-119), patchPhysicalRecords, physicalRecords (e2e/helpers/physical-presence.ts:166-185), used by 6 e2e files rg -l "physical-state|physical-presence" e2e
F24 src/daemon/test-relay-support.ts:181, :354 pass statePath as cited
F25 Components are any src/ directory with an index.ts, guarded by past-index .dependency-cruiser.mjs:1-36
F26 docs/CLI.md documents breaking changes under the command they touch docs/CLI.md:1449 "Breaking in 0.3.0"

Modules touched

Searches: Registry.load, statePath, state.json, RegistryOptions, defaultTtlMs, driverData, #commit, createDrivers over src/, e2e/ and docs/.

  • New src/state/ (index.ts, testing.ts, AGENTS.md): openStateDatabase, the old-file check, the checks and migrations.
  • src/ports/sqlite.ts: SqliteSchemaOwner type; :memory: is not pre-created; passesQuickCheck and isLockError move here from src/history/database.ts, which then imports them. configureDatabase stays history's: the state module sets its own pragmas (busy_timeout 5000, WAL, synchronous=NORMAL) and creates only its own tables.
  • src/core/registry.ts, src/core/driver.ts (DeviceDataTable), src/core/fake-driver.ts (fakeDeviceDataTable), src/core/testing.ts (the registry test helper, describeDeviceDataTable), src/core/index.ts.
  • src/daemon/main.ts, src/daemon/test-relay-support.ts; e2e/fake-driver/index.ts (exports deviceDataTables, with its own table defined in e2e/fake-driver/, F29); e2e/helpers/physical-state.ts, e2e/helpers/physical-presence.ts, e2e/physical-enrollment.test.ts:669, e2e/daemon-folders.test.ts:111 (asserts the gateway's state.db has no devices table).
  • src/core/testing.ts: HeldWritesFilesystem and the three test groups of F28 go. A synchronous commit has no write to hold, and the commit-interleaving test below replaces them.
  • Comments naming state.json that the Done when rg finds: src/core/lease-request-store.ts:33, src/core/domain.ts:26, :238, src/core/instance-identity.ts:6, :34, src/gateway/drain-store.ts:17, src/ports/paths.ts:7, src/ports/logger.ts:121, src/contract/schemas.ts:268 (a comment only; the console lane then runs), src/drivers/ios/index.ts:2137, src/daemon/error-code.ts:92, docs/internal/flows/startup.md:127, src/leasing/AGENTS.md:106.
  • The 28 to 36 test files of F22.
  • Docs: docs/CLI.md (new section), docs/internal/ARCHITECTURE.md:29 and the module table, docs/internal/flows/startup.md:17, :46, src/core/AGENTS.md, src/daemon/AGENTS.md, src/leasing/AGENTS.md:92, src/ports/AGENTS.md, src/history/AGENTS.md if a helper moves.
flowchart LR
  daemon["daemon/main.ts *"] --> state["state (new) *"]
  daemon --> core["core Registry *"]
  daemon --> e2efake["e2e fake drivers module *"]
  state --> ports["ports/sqlite.ts *"]
  core --> ports
  core --> dd["DeviceDataTable in core/driver.ts *"]
  history[history] --> ports
Loading

Surface and extension points

  • src/state/index.ts exports openStateDatabase(options): Promise<StateDatabase> and its types. Options: dataDirectory, sqlite: SqliteOpener, filesystem (old-file check only), owners: readonly SqliteSchemaOwner[], oldFiles: readonly string[], upgradeStep: string (the role's <step> of the old-file message in the feature's Shared values), logger. StateDatabase: database: SqliteDatabase, transaction<T>(work: () => T): T, close(). testing.ts exports openMemoryStateDatabase(owners) (:memory: through the real adapter).
  • Extension point (architecture rules 19, 20): DeviceDataTable has four implementations named (fake now, iOS and Android in task 2, Vega later). It differs from rule 20's shape in two ways, both because a table belongs to a driver (rule 2): the registry is keyed by platform, not by name, and each driver's own test runs the shared suite with its own examples. Definition: DeviceDataTable extends SqliteSchemaOwner with write(database, deviceId, driverData): void (insert or replace) and read(database, deviceId): unknown (throws when the row is missing). Its registry is the ReadonlyMap<Platform, DeviceDataTable> the composition root passes to Registry.load. The conformance suite is describeDeviceDataTable(name, table, examples) in src/core/testing.ts: each implementation's own test file runs it with that driver's example values, since only the driver knows its shapes. This task runs it for fakeDeviceDataTable. The registry never names a platform (architecture rule 21).
  • Registry.load(options) takes state: StateDatabase, deviceData: ReadonlyMap<Platform, DeviceDataTable>, clock, idGenerator, eventBus, leaseIdentity?, leaseRequestLimits?. filesystem, statePath and defaultTtlMs go away. registryOwner (the registry SqliteSchemaOwner) is exported from core for the composition root.

The tables

Owner registry, version 1. Every table is STRICT, and a required field is NOT NULL. Every value is a column. Optional fields are NULL when absent. Booleans are 0/1.

Table Key Columns
devices id platform, physical, driver_device_id, state, model, os_version, spec_mode (VirtualSpec.mode), image_tag, class, mode (DeviceRecord.mode), lease_identity, created_at, last_lease_ended_at, ready_at, shutdown_at, foreign_state_detected_at, foreign_provenance_detected_at, recovering_since, recovery_attempts, deferred_reclaim_lease_id, quarantined_at, quarantine_attempts, quarantine_next_retry_at, quarantine_cause_kind, quarantine_cause_stalled_state, quarantine_cause_message, address, reclaiming_since, absent_reason
device_enrolled_apps device_id, position app
leases id device_id, requester_id, owner_id, granted_at, ttl_ms, ttl_deadline, last_renewed_at, id_chosen_by_requester
lease_requests id requester_id, owner_id, idempotency_key, lease_id, fleet_request_id, platform, model, os_version, mode, image_tag, class, physical, created_at, state, settled_at, failure_code, failure_message
lease_grants request_id (references lease_requests) device_id (references devices), lease_id, requester_id, owner_id, granted_at, ttl_ms, ttl_deadline, last_renewed_at, id_chosen_by_requester, estimated_provision_ms, estimated_boot_ms, estimated_reclaim_ms, estimated_ready_ms
lease_grant_environment request_id, name value
components platform, version installed_at
component_receipt platform, version, key value

Owner fake-device-data, version 1: fake_device_data(device_id key, fake_device_id TEXT NOT NULL), STRICT, in src/core/fake-driver.ts; write refuses anything but { fakeDeviceId: string } with Invalid fake driver data for device <id>. Every unit test's driverData becomes { fakeDeviceId: <its device's id> } (F27). The e2e fake module defines its own e2e-fake-device-data table with the same columns and rule, since e2e cannot import src/core/fake-driver.ts (F29). One fake table serves both platforms in a map.

Rules:

  • A virtual device has exactly one row in its platform's device-data table. A physical device has none and loads with driverData: {} (F10).
  • A commit writes the rows whose record object differs from the committed one (records are immutable), deletes the rows of records no longer present (pruned requests), and writes the device-data row of every device row it writes, all in one StateDatabase.transaction. Memory and listeners update after it returns (F2). A throw rolls back and leaves memory as it was.
  • Load reads every table. A row with a value outside its type, a virtual device with no device-data row, a device whose platform has no table in the map, or a grant naming a missing device throws RegistryLoadError naming the table and key.
  • In memory a granted request keeps its lease, environment and timing. Its grant.device is the current device record, read when the grant is read (F13).
  • The load-time fixes of F4 and the unknown-field passthrough are removed.

Start (src/daemon/main.ts)

Before createAuth (F15), both roles: openStateDatabase on the role's data directory. Worker owners: registryOwner plus each table of the device-data map. Gateway owners: none in this task. Worker old files: state.json. Gateway: none in this task. Its steps, in this order, each refusing with the feature's Shared values message:

  1. Any old file present → refuse.
  2. node:sqlite unavailable → refuse.
  3. Open, then set the state module's own pragmas: busy_timeout=5000, journal_mode=WAL, synchronous=NORMAL. A lock error past 5 s → lock refusal. Any other open error → damage refusal (open: <message>).
  4. PRAGMA quick_check not exactly one ok row → damage refusal (quick_check: <first row>).
  5. In one BEGIN IMMEDIATE transaction: create schema_versions if absent, then read each owner's version inside it. A stored version above owner.version → newer-version refusal. Below it → run its steps and set its version. A throw rolls back the whole transaction, every owner's steps included → migration refusal. Reading the versions inside the write transaction is what makes two daemons starting at once migrate once.

Before step 1, startDaemon probes the daemon socket (refuseWhenDaemonAnswers in src/daemon/connection-host.ts): a socket that answers throws DaemonAlreadyRunningError and nothing touches state.db, so a second daemon never migrates a live daemon's file. A lock error at steps 3, 4 or 5 past 5 s gives the lock message, never the damage one. A start that fails after the open (e.g. the socket claim is lost) closes state.db before it throws.

The device-data map comes from one function, resolveDeviceDataTables(options) in src/daemon/main.ts, which startDaemon calls once and passes to both openStateDatabase (each distinct table once, as an owner) and Registry.load. Its order: StartDaemonOptions.deviceDataTables (new, a ReadonlyMap<Platform, DeviceDataTable>; the 37 main.test.ts starts that inject drivers pass the fake table for both platforms, and so do the drivers-injecting starts in src/agent-device/edge.test.ts, src/agent-device/gateway-front.test.ts, src/daemon/gateway-fleet.e2e.test.ts, gateway-modules.test.ts, lease-requires.e2e.test.ts, optional-modules.test.ts, server.test.ts, stream-gateway.e2e.test.ts and worker-capabilities.e2e.test.ts); else, with SIMLOCK_DRIVERS_MODULE, the module's deviceDataTables export (the same map type), read before the state opens, and a module without it fails start, as a missing createDrivers does (F17); else productionDeviceDataTables(), which is empty in this task (task 2 fills it). statePath leaves StartDaemonOptions. The leftover sweep keeps dataDirectory only. state.db is never deleted or renamed by Simlock. state.close() runs last in each role's dispose, after eventHistory.stop() (src/daemon/main.ts:553-569, :904-909); dispose runs after stopAuxiliary has stopped HTTP and the uplinks.

Contract and event changes

None: no operation, schema, error code or event changes (searched src/contract for statePath, state.json: none). driverData stays in list.get and doctor answers, from the device-data row (F14).

Other code on the same state

Searched state.json, statePath, Registry over src/ and e2e/ (F22, F23, F24). Every registry write stays inside SerializedDecision.run as today (src/core/AGENTS.md rule). The commit is now synchronous, so no other commit can run between its read and its write. The CLI reads no registry state (src/cli imports no Registry). History writes its own file (ADR 0032). Tasks 3 and 4 add owners to the same state.db, through Shared values.

Failure modes

Call Outcome What happens Limit, counted from
openStateDatabase ok registry loads —
old file present refuse, nothing opened or created —
node:sqlite missing refuse —
locked refuse with the lock message 5 s, from the open
open fails, quick_check fails refuse with the damage message; the file is untouched —
newer version, migration throws refuse; the migration step rolled back —
Registry commit ok rows written, memory updated, listeners run —
SQLite throws (disk full, I/O) rollback, memory unchanged, the caller's operation fails as a failed write does today busy wait 5 s, from the statement
DeviceDataTable.write / read throws as a commit or load throw —
Registry.load bad row RegistryLoadError → start refuses —

Restart in the middle of a commit: the transaction either committed or did not (WAL). Two daemons on one folder: the second loses the socket claim as today, and its open waits on the lock at most 5 s.

Rules in play

  • ADR 0036 §1–§5: the whole task.
  • ADR 0019 §4, "the same … write that commits the lease marks that request granted": createLease stays one transaction.
  • ADR 0018 §2, "a grant and the device's move to leased are one … write": one transaction.
  • ADR 0022 §1, physical records "in their own top-level list": now devices.physical = 1, with enrolled apps in a child table.
  • ADR 0027, every component guarded: src/state is found by its index.ts.
  • Architecture rules 3 and 21, "Adding a driver must require zero core changes": a driver adds a table to the map.
  • Architecture rule 9, ports: SQLite only through SqliteOpener; src/ports/AGENTS.md "sqlite.ts is the only file that names node:sqlite".
  • Architecture rule 18: src/state exports one factory and its types.
  • Safety rule 1, registry-only destruction: nothing destroys a device that state.db does not hold; the upgrade step orphans nothing.
  • src/core/AGENTS.md rule "Only Registry writes state.json" becomes "Only Registry writes the registry tables".

Tests

Seams: Registry.load over openMemoryStateDatabase (model: src/core/registry.test.ts), openStateDatabase (new src/state/state.test.ts, model: src/history/history.test.ts, temp dir), and startDaemon (model: src/daemon/main.test.ts:2564).

  • A registry restarted on the same state.db returns every field of a virtual device (driverData: { fakeDeviceId }), a physical device (enrolled apps in order), a lease, a request in each of open, granted, failed, cancelled, and a component (receipt pairs) exactly as committed, with optional fields both set and absent (temp file; extends the restart test at src/core/registry.test.ts:195).
  • A renew with 1,000 settled requests stored changes exactly one row (SELECT total_changes() before and after).
  • A grant writes the lease, the device's move to leased and the request's granted result in one transaction: a DeviceDataTable.write that throws during the grant leaves no lease, the device ready and the request open, both in memory and after a restart.
  • A commit that throws in SQLite leaves snapshot and the commit listeners as before the call, and the caller's promise rejects.
  • Pruning deletes the rows of dropped requests and their grant and environment rows (retention and maxRecords cases; extends the existing pruning test).
  • A replayed answer's device is the current device record, before and after a restart, while its lease, environment and timing are as first granted.
  • Load refuses with RegistryLoadError naming the table and key for: a virtual device with no device-data row, a device whose platform has no table, a grant naming a missing device, a state value not in the type.
  • A physical device loads with driverData: {} and has no device-data row.
  • A commit's write runs with no await between reading the registry's current records and swapping them in memory: two transitions of two devices started in one tick both persist, in memory and after a restart. This replaces the F28 groups.
  • describeDeviceDataTable asserts, for each example: write then read returns it deep-equal with no undefined keys, a second write of another example replaces it, and read of a missing id throws. Run for fakeDeviceDataTable.
  • resolveDeviceDataTables answers the option's map when given, else the drivers module's export, else productionDeviceDataTables(). startDaemon passes the same map to the open and to Registry.load: a fake-table device committed before a restart loads after it.
  • openStateDatabase refuses with each Shared values message, word for word, for: an old file present (and creates no state.db), a file failing quick_check (bytes overwritten in the page area), a file that is not SQLite, a lock held past 5 s by another connection, a stored version one above the owner's, a migration step that throws (its version bump is rolled back).
  • openStateDatabase on an empty folder creates state.db mode 0600 and records each owner's version. A second open runs no migration.
  • The versions are read inside the write transaction: with an opener whose connection, on its BEGIN IMMEDIATE, first migrates the same file synchronously on a second raw connection (the owners' steps run there, versions set), the first open runs no migration step (steps counted).
  • A lock held past 5 s by another connection at the quick_check and at BEGIN IMMEDIATE gives the lock message, not the damage one.
  • A second startDaemon on a folder whose daemon runs rejects with DaemonAlreadyRunningError and leaves state.db's schema_versions rows and bytes unchanged, even with an owner at a newer version (a new case beside src/daemon/main.test.ts:2564).
  • startDaemon as a worker with state.json in its folder throws the old-file message with the worker's step, and creates no state.db.
  • The built daemon entry (node dist/daemon/main.js, SIMLOCK_HOME a temp dir holding state.json) exits 1 and its daemon.log ends with a Daemon failed to start line whose fields.message is that message (a new case beside e2e/daemon-folders.test.ts:170, which already runs the entry and checks its Daemon failed to start line).
  • startDaemon with a SIMLOCK_DRIVERS_MODULE that has no deviceDataTables export fails start naming it (extends src/daemon/main.test.ts:2564).
  • A worker started with startDaemon and fake drivers, leased, stopped and started again, lists the same device and lease (extends src/daemon/main.test.ts:897).
  • The e2e physical-device flows pass with seedPhysicalState, patchPhysicalRecords and physicalRecords reading and writing state.db (the 6 files of F23, unchanged assertions).

Done when

  • pnpm test:changed passes.
  • pnpm test:e2e e2e/physical-enrollment.test.ts e2e/daemon-folders.test.ts e2e/physical-devices-core.test.ts e2e/physical-leasing.test.ts e2e/physical-reclaim.test.ts e2e/physical-presence-loss.test.ts e2e/physical-presence-loss-recovery.test.ts e2e/physical-gateway.test.ts passes.
  • rg -n "state\.json" src docs e2e --glob '!docs/internal/adr/**' finds only the old-file check, its tests and the docs/CLI.md upgrade section.
  • docs/CLI.md has ### Breaking: state moves to state.db under simlock daemon, with the upgrade step (release every lease, simlock nuke --delete-devices with the older Simlock, remove state.json, add physical devices again with simlock device add), how to read the refusal (simlock daemon logs), and the downgrade step (release every lease and simlock nuke --delete-devices before starting an older Simlock).

Out of scope

  • The iOS and Android device-data tables, and filling the production map (task 2).
  • Tokens and tokens.json (task 3), drained workers and workers.json (task 4).
  • The CLI printing the refusal (task 5).
  • Any import of state.json.

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 20, 2026
  2. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 20, 2026
  3. self-assigned this
    on Oct 20, 2026
  4. 18 remaining items

  5. V3RON commented on Oct 20, 2026

    @V3RON
    ContributorAuthor

    Handoff

    Done. PR #775 (task/761, head d2b520d) has passed review round 2 and fix-checks through 6c4df08. The whole stack #782 (#755 → #775 → #783 → #784 → #785 → #786) is synced with no conflicts. Every other PR in it has passed review and is ready. The ADR PR #755 has passed its spec review.

    Not done. Commit fc5076f fixes R15 to R17 from the last fix-check. Nobody has reviewed R15 (src/daemon/optional-modules.test.ts:633, the race test split into "at the probe" and "at the claim" cases), because rule 14's round cap was used up. The implementer checked it with a mutation that fires onSocketClaimed when host.start throws, and the claim case went red. R16 and R17 (a flow doc line and a test comment) passed a coordinator diff check. The gate has not run.

    Findings. In delivery I added a socket probe before state.db opens (refuseWhenDaemonAnswers, R13). The decision is pending you; see the "Decision taken" comment on #755. A race between the probe and the claim remains when two binaries of different versions start at the same moment (review note).

    Blocked on. The maintainer checking R15, then .agents/scripts/merge-pr.sh 786.

    Written by an agent.

  6. added
    task:blockedAgent stopped on a blocker in its handoff. Maintainer re-adds the label it came from.
    and removed
    task:readySpec checked. An agent may implement it once its Depends on allow.
    on Oct 20, 2026
  7. removed their assignment
    on Oct 20, 2026
  8. added a commit that references this issue on Oct 20, 2026
    514b591
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    task:blockedAgent stopped on a blocker in its handoff. Maintainer re-adds the label it came from.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions