Skip to content

docs(adr): 0042 simlock updates itself through a frozen call and a detached updater - #827

Merged
V3RON merged 17 commits into
mainfrom
feature/825-adr
Oct 22, 2026
Merged

V3RON merged 17 commits into
mainfrom
feature/825-adr

Conversation

@V3RON

@V3RON V3RON commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

ADR 0042 for #825: Simlock updates itself through a frozen daemon.update call and a detached updater.

Status

Implement: done Review: passed (coordinator check of the §1/§3 amendment at de80a5a) Hardware: n/a Gate: waiting for the stack
Next: merges with the #825 stack

Assumptions

none

Review

Spec review: 17 blocking, 16 fixed, 1 notes. Code review: skipped. Claims review: skipped.
Rounds: 2. Tokens: 8.3M.
Runs: 16. Cost USD: 4.3569. Agent seconds: 2127.

Assessment: complete

Rejected:

  • spec: F10 worker.disconnected wording for an incompatible worker — [nonblocking] true under the code's reading (worker-registry.ts:429-444)

Written by an agent.

@V3RON
V3RON marked this pull request as ready for review October 21, 2026 13:06
@V3RON
V3RON marked this pull request as draft October 21, 2026 13:06
@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review ledger

Round 1: a1cc563

Spec review (Opus/high, ADR-only, so no code or claims lens), 8 blocking findings. Assessment: complete.

ID Where Defect Evidence
R1 ADR 0042 Consequences :175-177 Worker and gateway on one machine share one global npm install; updating one replaces the other running daemon's files, and the ADR does not decide it ADR 0034 §2, 0042 §7
R2 0042 §8, §10 Rollback and by-name downgrade fail once the new version has migrated state.db or history.db; the old version refuses a newer schema ADR 0036 §5, src/daemon/AGENTS.md:136
R3 0042 §10 "The frozen call can fix again" is false for a downgrade to a version older than the one that adds daemon.update 0042 §1
R4 0042 Consequences :178 "Fires worker.connected as every restart does" is false when the worker comes back incompatible src/gateway/worker-registry.ts:268-305
R5 0042 §5, §7 version from the wire reaches npm i -g simlock@<version> with no stated shape check. Probe: npm view "simlock@>=0.1.0" resolves, so a range passes the "npm confirms it exists" step; an npm: alias is refused by npm. Safety rule 10 asks for structural validation. safety.md rule 10
R6 0042 §8 Undecided: a new daemon alive but not ready after 2 minutes; the rolled-back start logs "already running" and exits 0 ADR 0041 :88-91
R7 0042 §1, §11 Frozen answer lists update and lastUpdateFailure apart, while status.get's update carries the last failure; the frozen shape is ambiguous 0042 :117-121
R8 0042 §3 Skipping updating workers changes ADR 0009 stage 1 ("connected, not drained"), but the header and the README row do not record it ADR 0009 :69

Rerun: round 2 after fixes.

{
 "findings": [
  {
   "id": "F1",
   "role": "spec",
   "class": "adr contradiction: shared global install",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  },
  {
   "id": "F2",
   "role": "spec",
   "class": "adr contradiction: rollback vs schema refusal",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  },
  {
   "id": "F3",
   "role": "spec",
   "class": "false consequence: frozen call fixes downgrade",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  },
  {
   "id": "F4",
   "role": "spec",
   "class": "false consequence: worker.connected always fires",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  },
  {
   "id": "F5",
   "role": "spec",
   "class": "wire input unvalidated (safety 10)",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  },
  {
   "id": "F6",
   "role": "spec",
   "class": "undecided: alive but unready new daemon",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  },
  {
   "id": "F7",
   "role": "spec",
   "class": "undecided frozen answer shape",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  },
  {
   "id": "F8",
   "role": "spec",
   "class": "unrecorded change to ADR 0009 stage 1",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1"
  }
 ]
}```

*Written by an agent.*

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Decision taken

The spec review (R1, R2, R6) found three questions nobody decided. Delivery took the option closest to today and easiest to reverse. All three are in #825's Decisions, marked pending the maintainer.

1. A worker and gateway of one user share one global install (R1). Updating one replaces the other's files.

  • (a) Refuse while the other role's daemon runs: UPDATE_SHARED_INSTALL, nothing changes. The operator stops it first. Taken.
  • (b) The update restarts both daemons. Needs a second restart path and a lease wait for both.
  • (c) Ignore it. The other daemon runs on replaced files.

Why (a): nothing changes and nothing breaks. Undoing it means dropping one check in #836.

2. Rollback after the new version migrated state.db / history.db (R2). The old version refuses a newer stored version (ADR 0036 §5), so the rollback would fail.

  • (a) The updater copies the database files after the stop and restores them before the rollback start. Taken.
  • (b) Accept it: the machine stays down.
  • (c) Refuse an update when the schema version changes.

Why (a): the rollback keeps its promise. Undoing it means removing one step from the updater in #835.

3. A new daemon that answers but is not ready after 2 minutes (R6).

  • (a) Count it as not started: stop it, then roll back. Taken.
  • (b) Keep waiting.

Why (a): it uses the existing 2-minute limit and frees the socket for the old version. Undoing it means changing one branch in #835.

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review findings, round 2 (spec, ADR-only) at 44b8e4c

Previous R1-R8: all resolved in the ADR text (R1 §13, R2 §8/§10, R3 §10, R4 Consequences, R5 §5, R6 §8, R7 §1, R8 header and README row). R1, R2, R6 stay decisions pending the maintainer.

New blocking findings (round-2 siblings of R4 and R5, plus one CI break):

  • F9 ADR 0042 §8 (line 119): under a service manager (ADR 0041 restarts a failed daemon every 10 s) the updater's stop and database restore race the respawn. The ADR does not say how the crash loop is halted, so a respawned new daemon can migrate the restored copy again and the old version still refuses. [false consequence]
  • F10 Consequences (line 206): "an update restart fires worker.disconnected" is false for an incompatible worker, which §1 allows to be updated; EVENTS.md:190-192 says it never counted as connected. Same class as R4. [false consequence]
  • F11 §1 (line 61): "updated in any order" is false for a worker on the gateway's Mac; §13 refuses both updates while both run. [contradiction]
  • F12 §10 (line 128) and the diagram (line 170): "installs latest" and npm i -g simlock@latest contradict §5, which installs the resolved exact version. Same class as R5. [contradiction]
  • F13 docs/internal/adr/README.md:79-83: the 0042 row sits before 0039, so src/adr-index.test.ts "lists the rows in ascending order" fails (CI red on a1cc563 and still red). origin/main has no 0042 row, so the PR causes it. [testing: index order]

Fix impacts expected: F9-F12 acceptance+claims (ADR text only); F13 mechanical.

Assessment: complete. Spec review (Opus/high, ADR): 5 findings, 4 blocking spec plus 1 coordinator; claims and code lenses skipped (ADR-only). Round cap: this was round 2; one fix-check remains.
Findings file: /tmp/rv827/findings.json

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Decision taken

Review round 2 found two more questions nobody had decided (F9, F11). Both are now in #825's Decisions, marked pending the maintainer.

1. A new version that crash-loops under a service manager (F9). launchd or systemd restarts it while the rollback runs.

  • (a) Leave the service loaded. The rollback installs the old version and runs daemon start. Whichever daemon then answers with the old version ends the rollback, whether the service or the updater started it. Taken.
  • (b) Unload the service during the rollback, then load it again. This adds code to the updater that talks to the service manager, and the service is left unloaded if the updater dies in between.

Why (a): it needs no new code that talks to the service manager. After the reinstall, the service's next restart starts the old version anyway. Undoing it means adding an unload step in #835.

2. A worker and a gateway on one Mac (F11). §13 refuses an update while the other role runs.

  • (a) The operator updates by hand: stop one role, update the other, then start the stopped role. "Any order" holds only for machines that run a single role. Taken.
  • (b) One update restarts both roles.

Why (a): it is the same refusal as before, with no new restart path. Undoing it means option (b), added in #836.

Written by an agent.

@V3RON
V3RON added this pull request to stack #881 October 22, 2026 08:44
@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review ledger

Round 1: a1cc563
Round 2: 44b8e4c
Round fix-check: cac4085

Runs: 8. Cost USD: 3.1591. Agent seconds: 1473.
Tokens input 206, output 68067, cache read 4042688, cache write 381582.
- review 1 reviewer: claude-sonnet-5-5 434 s $0.2314
- review 1 reviewer spec: claude-opus-5-5 323 s $1.4617
- fix 1 implementer: claude-sonnet-5-5 89 s $0.16
- review 2 reviewer: claude-sonnet-5-5 209 s $0.2069
- review 2 reviewer spec: claude-opus-5-5 98 s $0.4035
- fix 2 implementer: claude-sonnet-5-5 101 s $0.145
- review fix-check reviewer: claude-sonnet-5-5 146 s $0.1762
- review fix-check spec-reviewer spec: claude-opus-5-5 73 s $0.3744
Finding outcomes: {"fixed":12,"nonblocking":2,"open":1}. Classes: {"ADR index rows out of ascending order (CI red)":1,"adr contradiction: rollback vs schema refusal":1,"adr contradiction: shared global install":1,"ambiguous event consequence (incompatible disconnect)":1,"false claim: any update order vs shared install":1,"false consequence: frozen call fixes downgrade":1,"false consequence: service manager restarts failed new daemon during rollback":1,"false consequence: update restart disconnect for incompatible worker":1,"false consequence: worker.connected always fires":1,"rollback restore races service-manager restart":1,"stale text: installs latest vs exact resolved version":1,"undecided frozen answer shape":1,"undecided: alive but unready new daemon":1,"unrecorded change to ADR 0009 stage 1":1,"wire input unvalidated (safety 10)":1}.
{
 "findings": [
  {
   "id": "F1",
   "role": "spec",
   "class": "adr contradiction: shared global install",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F2",
   "role": "spec",
   "class": "adr contradiction: rollback vs schema refusal",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F3",
   "role": "spec",
   "class": "false consequence: frozen call fixes downgrade",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F4",
   "role": "spec",
   "class": "false consequence: worker.connected always fires",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F5",
   "role": "spec",
   "class": "wire input unvalidated (safety 10)",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F6",
   "role": "spec",
   "class": "undecided: alive but unready new daemon",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F7",
   "role": "spec",
   "class": "undecided frozen answer shape",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F8",
   "role": "spec",
   "class": "unrecorded change to ADR 0009 stage 1",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "1",
   "fix": "44b8e4c72dcc92e1025b81a1fe523c268f1f336e"
  },
  {
   "id": "F9",
   "role": "spec",
   "class": "false consequence: service manager restarts failed new daemon during rollback",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "2",
   "fix": "cac4085a"
  },
  {
   "id": "F10",
   "role": "spec",
   "class": "false consequence: update restart disconnect for incompatible worker",
   "outcome": "nonblocking",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "2",
   "fix": "cac4085a"
  },
  {
   "id": "F11",
   "role": "spec",
   "class": "false claim: any update order vs shared install",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "2",
   "fix": "cac4085a"
  },
  {
   "id": "F12",
   "role": "spec",
   "class": "stale text: installs latest vs exact resolved version",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "2",
   "fix": "cac4085a"
  },
  {
   "id": "F13",
   "role": "coordinator",
   "class": "ADR index rows out of ascending order (CI red)",
   "outcome": "fixed",
   "path": "docs/internal/adr/README.md",
   "round": "2",
   "fix": "cac4085a"
  },
  {
   "id": "F14",
   "role": "spec",
   "class": "rollback restore races service-manager restart",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "fix-check"
  },
  {
   "id": "F15",
   "role": "spec",
   "class": "ambiguous event consequence (incompatible disconnect)",
   "outcome": "nonblocking",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md",
   "round": "fix-check"
  }
 ]
}

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Handoff

Done: three review checks of ADR 0042 (round 1, round 2, fix-check). 12 findings fixed, 2 left as notes. Five questions decided in delivery, each pending you: R1, R2, R6, F9, F11 (see the ## Decision taken comments).

Not done: F14 is still open, and the review has used all its rounds. §8 does not say when the restored database copy goes back relative to the rollback's npm install. A service manager can restart the failed new version in between, and that version migrates the restored database again. The old version then refuses to start.

Findings: ## Review ledger above.

Blocked on: your call on F14. Delivery's choice for #835 is to restore the copy only after the rollback install. From then on, every restart runs the old version. That choice is in #825 Decisions, pending you. Then the ADR needs one more check.

The PR stays in draft. The tasks stacked on it go on.

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Maintainer decision

On 2026-10-22 the maintainer confirmed every decision taken in delivery for #825.

  • F14: the rollback restores the database copy only after it reinstalls the old version. Before the restore it stops any daemon the service restarted, and waits until the socket refuses connections.
  • A new daemon that ignores daemon.stop is not killed. The SIGKILL for it comes out of §8.

The maintainer allowed one more fix and one more check past the round cap.

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review findings, fix-check (spec, ADR-only) at b90f033

F14 resolved: §8 now orders npm install, stop of a service-restarted daemon, restore, start (socket close follows the db close in src/daemon/server.ts:628-637).

Open blocking (both in ADR 0042 §8, same class: the rewritten text contradicts itself):

  • F16 line 119 says a daemon that ignores the stop "does not hold the rollback back", but lines 136-138 say a daemon that does not stop within 10 minutes means no restore, no start, exit 1. Either the rollback is held or the copy is restored over files an unstopped daemon still has open.
  • F17 line 129 waits until "its pid has exited", but line 119 says the updater has no pid for that daemon, and no daemon pid file exists (ADR 0041 §6: start goes through the service manager).

CI and test-lanes hunks in previous..head are a merge of main (coordinator: mechanical, not the PR's). Assessment: complete. Cap reached: PR stays in draft with the ledger; the maintainer decides.

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review ledger

Checks: Round 1 a1cc563, Round 2 44b8e4c, fix-check cac4085, fix-check b90f033, fix-check (coordinator diff check) a4a1bab.

Last check: git diff b90f0337 a4a1babf touches two places in ADR 0042 §8 and nothing else.

  • F16 fixed (a4a1bab): line 119 now says a daemon that ignores the stop ends the update with no rollback, pointing to the 10-minute rule at lines 136-138 (no restore, backup kept, no second start, exit 1). It agrees with the maintainer decision of 2026-10-22.
  • F17 fixed (a4a1bab): "or its pid has exited" is gone from the service-restarted daemon's wait; it waits only for the socket to refuse connections or the file to go. The updater has a pid only for the old daemon (§7 kill after 10 minutes), which matches.
  • Nothing else in §7 or §8 contradicts the 10-minute rule.

Ledger: F1-F9, F11-F14, F16, F17 fixed. F10, F15 nonblocking (recorded, not required). No open blocking findings.

Assessment: complete. No lens was run for this check (mechanical edits, coordinator diff check). Merge decision stays with the maintainer.

Written by an agent.

@V3RON
V3RON marked this pull request as ready for review October 22, 2026 09:44
@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review ledger

Checks: Round 1 a1cc563, Round 2 44b8e4c, fix-check cac4085, fix-check b90f033, fix-check (coordinator diff check) a4a1bab.

Round fix-check: de80a5a

Last check: git diff a4683387 de80a5ad touches two places in ADR 0042 (§1, §3) and nothing else; a468338 is a clean merge of main.

  • F16 fixed (a4a1bab): line 119 now says a daemon that ignores the stop ends the update with no rollback, pointing to the 10-minute rule at lines 136-138 (no restore, backup kept, no second start, exit 1). It agrees with the maintainer decision of 2026-10-22.
  • F17 fixed (a4a1bab): "or its pid has exited" is gone from the service-restarted daemon's wait; it waits only for the socket to refuse connections or the file to go. The updater has a pid only for the old daemon (§7 kill after 10 minutes), which matches.
  • F18 fixed (de80a5a): maintainer amendment of 2026-10-22. §1: a daemon.update query answers at once, also while health is starting; a call that starts an update still waits for ready. §3: the update hold refuses every lease request not yet provisioning (new or queued) with DAEMON_UPDATING; only provisioning ones finish. Nothing else in the ADR says query waits for readiness or that only queued requests are refused (§6, the §7 diagram and §8 checked).
  • Nothing else in §7 or §8 contradicts the 10-minute rule.

Ledger: F1-F9, F11-F14, F16-F18 fixed. F10, F15 nonblocking (recorded, not required). No open blocking findings.

Assessment: complete. Nonblocking: §3 line 76 of the ADR is now longer than the wrapped width; rewrap if the formatter wants it. No lens was run for this check (mechanical edits, coordinator diff check). Merge decision stays with the maintainer.

Written by an agent.

@V3RON
V3RON merged commit f3d1b44 into main Oct 22, 2026
27 checks passed
@V3RON
V3RON deleted the feature/825-adr branch October 22, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant