Skip to content

feat(updater): the updater, its npm port and launcher port, and the install check - #878

Merged
V3RON merged 40 commits into
feature/825-adrfrom
task/835
Oct 22, 2026
Merged

V3RON merged 40 commits into
feature/825-adrfrom
task/835

Conversation

@V3RON

@V3RON V3RON commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #835

Status

Implement: done (31/31 green) Review: passed (R16 accepted by the maintainer) Hardware: n/a Gate: waiting for the stack
Next: joins the #825 stack

Built: all Tests lines (updater table incl. database copy, starting-after-2-min and crash-loop cases; install check; NodeNpmCli), launcher port, entry, manuals. Audited: restore order asserted (live database still the new version's during the rollback install, restored before its start); added cases for a foreign running version, a new daemon ignoring daemon.stop, a stale -wal, a second copy under npm's root, and the launcher's detach and log append. Not tested: main.ts (the probe on the admin client; #836 tests it) and unref() in the launcher.

Done when

  • pnpm test:changed passes (fix run at 81ad946: 31/31 updater tests, src/ports green) with the lines above (src/updater/index.test.ts, src/ports/npm-cli.test.ts).
  • CI's check job passes (only red: src/daemon/relay-promises.timing.test.ts, outside this diff, rerun running). [x] manuals: src/updater/AGENTS.md exists (src/updater/AGENTS.md), and the ports table has the two new rows (src/ports/AGENTS.md).

Assumptions

  • Assumption: runUpdater also takes stderr — the failed-rollback message goes to daemon.log through it; the spec's option list had only stdout.
  • Assumption: rolledBack in the result file is always true — it is written before the rollback starts, so it records that one is under way.
  • Assumption: Filesystem gets copyFile — the database copy needs a byte copy and the port had none.
  • Assumption: the new daemon that ignores daemon.stop for 10 minutes is not killed — the probe knows no pid for it; the wait ends and the rollback goes on.
  • Assumption: if the rollback install fails, no start is tried and the backup stays; checkInstall answers ok: false when npm root -g fails.
  • Assumption: a new daemon answering with health: failed is stopped like one still starting.
  • Assumption: each probe call (stop, listening, state) is bounded at 30 s; past it listening counts as still listening — the spec gave no number.

Review

Spec review: 0 blocking, 0 fixed, 1 notes. Code review: 9 blocking, 8 fixed, 1 notes. Claims review: 1 blocking, 1 fixed, 0 notes.
Rounds: 2. Tokens: 8.3M.
Runs: 19. Cost USD: 8.5517. Agent seconds: 5067.

Assessment: complete

Rejected:

  • spec: R5 parseUpdaterArgs tests are unasked — [incorrect] always-in-scope items 4 and 8 ask for them
  • spec: R13 extra assertions in "old daemon stays up" are unasked — [incorrect] they prove the R4 fix (testing 9)
  • spec: R14 scenarios "holds the socket" and "daemon start never exits" are unasked — [incorrect] they prove the R1 fix (testing 9)
  • code: R16 two liveness guards have no red test — [nonblocking] accepted by the maintainer 2026-10-22; behaviour verified on real sockets

Written by an agent.

V3RON added 30 commits October 22, 2026 10:26
@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

Decision taken

The build left two questions open. Both are now in #825's Decisions and #835's body, marked pending the maintainer.

1. When a rollback puts the database copy back (#827's F14).

  • (a) After the npm install of the old version has finished, and then daemon start. From that point the service manager can only restart the old version. Taken.
  • (b) Before the install. A service restart of the failed new version in that window migrates the copy again, and the old version then refuses to start.
  • (c) Have the updater hold off the service while the rollback runs. That needs new code that talks to the service manager.

Why (a): it is one step moved, with no new machinery. Undoing it means moving that step in the updater.

2. A new daemon that ignores daemon.stop for 10 minutes.

  • (a) Leave it running and go on with the rollback. The probe has no pid for the new daemon. Taken (the build's assumption).
  • (b) Kill it. The daemon would first have to expose its pid.

Why (a): this case almost never happens, and (b) needs a change to what the daemon reports. ADR 0042 §8 still says SIGKILL. That text is part of #827, which is parked.

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review ledger

Round 1: e6c2555
Round 2: 41f3b2f
Round fix-check: bf82d12

Runs: 15. Cost USD: 7.2173. Agent seconds: 4323.
Tokens input 552, output 195494, cache read 17878801, cache write 969716.
- build implementer: claude-sonnet-5-5 1260 s $2.1506
- audit implementer: claude-sonnet-5-5 292 s $0.4097
- review 1 reviewer: claude-sonnet-5-5 490 s $0.3689
- review 1 spec: claude-sonnet-5-5 46 s $0.307
- review 1 code both: claude-opus-5-5 374 s $1.3548
- review 1 claims: claude-sonnet-5-5 34 s $0.1604
- fix 1 implementer: claude-sonnet-5-5 199 s $0.3355
- review 2 reviewer: claude-sonnet-5-5 527 s $0.4079
- review 2 claims: claude-sonnet-5-5 41 s $0.1153
- review 2 spec: claude-sonnet-5-5 47 s $0.1413
- fix 2 implementer: claude-sonnet-5-5 289 s $0.3443
- fix fix-check implementer: claude-sonnet-5-5 174 s $0.0951
- review fix-check reviewer: claude-sonnet-5-5 316 s $0.1796
- review fix-check code both: claude-opus-5-5 204 s $0.7434
- review fix-check claims: claude-sonnet-5-5 30 s $0.1035
Finding outcomes: {"duplicate":2,"fixed":7,"incorrect":3,"nonblocking":1,"open":2}. Classes: {"accepted ADR contradicted by decided behavior":2,"architecture 11":1,"liveness assumed":1,"liveness assumed from socket file":1,"testing 1":1,"testing 10":2,"testing 2":1,"testing 7":4,"undocumented decision":1,"user data overwritten, liveness assumed":1}.
{
 "findings": [
  {
   "id": "R1",
   "role": "code",
   "class": "architecture 11",
   "outcome": "fixed",
   "path": "src/updater/index.ts:156",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R2",
   "role": "code",
   "class": "testing 10",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:240",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R3",
   "role": "code",
   "class": "testing 10",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:290",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R4",
   "role": "code",
   "class": "user data overwritten, liveness assumed",
   "outcome": "fixed",
   "path": "src/updater/index.ts:104",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R5",
   "role": "spec",
   "class": "testing 7",
   "outcome": "incorrect",
   "path": "src/updater/index.test.ts:1116",
   "round": "1"
  },
  {
   "id": "R6",
   "role": "code",
   "class": "testing 2",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:283",
   "round": "2",
   "fix": "26ce7610"
  },
  {
   "id": "R7",
   "role": "code",
   "class": "testing 1",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:324",
   "round": "2",
   "fix": "26ce7610"
  },
  {
   "id": "R8",
   "role": "code",
   "class": "liveness assumed from socket file",
   "outcome": "fixed",
   "path": "src/updater/index.ts:148",
   "round": "2",
   "fix": "26ce7610"
  },
  {
   "id": "R9",
   "role": "claims",
   "class": "accepted ADR contradicted by decided behavior",
   "outcome": "open",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md:128",
   "round": "2"
  },
  {
   "id": "R10",
   "role": "spec",
   "class": "testing 7",
   "outcome": "duplicate",
   "path": "src/updater/index.test.ts:283",
   "round": "2",
   "canonicalId": "R9"
  },
  {
   "id": "R11",
   "role": "spec",
   "class": "accepted ADR contradicted by decided behavior",
   "outcome": "duplicate",
   "path": "src/updater/index.ts:126",
   "round": "2",
   "canonicalId": "R9"
  },
  {
   "id": "R12",
   "role": "spec",
   "class": "undocumented decision",
   "outcome": "nonblocking",
   "path": "src/updater/index.ts:126",
   "round": "2"
  },
  {
   "id": "R13",
   "role": "spec",
   "class": "testing 7",
   "outcome": "incorrect",
   "path": "src/updater/index.test.ts:287",
   "round": "2"
  },
  {
   "id": "R14",
   "role": "spec",
   "class": "testing 7",
   "outcome": "incorrect",
   "path": "src/updater/index.test.ts:121",
   "round": "2"
  },
  {
   "id": "R15",
   "role": "code",
   "class": "liveness assumed",
   "outcome": "open",
   "path": "src/updater/probe.ts:41",
   "round": "fix-check"
  }
 ]
}

Assessment: incomplete (R15 open at the round cap)

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 and #835.

They allowed one more fix and one more check past the round cap, for R15.

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review ledger

Round 1: e6c2555
Round 2: 41f3b2f
Round fix-check: bf82d12
Round fix-check: 81ad946

The second fix-check was the extra round the maintainer allowed. R16 was accepted as is by the maintainer (nonblocking).

Runs: 19. Cost USD: 8.5517. Agent seconds: 5067.
Tokens input 662, output 226817, cache read 19814060, cache write 1148205.
- build implementer: claude-sonnet-5-5 1260 s $2.1506
- audit implementer: claude-sonnet-5-5 292 s $0.4097
- review 1 reviewer: claude-sonnet-5-5 490 s $0.3689
- review 1 spec: claude-sonnet-5-5 46 s $0.307
- review 1 code both: claude-opus-5-5 374 s $1.3548
- review 1 claims: claude-sonnet-5-5 34 s $0.1604
- fix 1 implementer: claude-sonnet-5-5 199 s $0.3355
- review 2 reviewer: claude-sonnet-5-5 527 s $0.4079
- review 2 claims: claude-sonnet-5-5 41 s $0.1153
- review 2 spec: claude-sonnet-5-5 47 s $0.1413
- fix 2 implementer: claude-sonnet-5-5 289 s $0.3443
- fix fix-check implementer: claude-sonnet-5-5 174 s $0.0951
- review fix-check reviewer: claude-sonnet-5-5 316 s $0.1796
- review fix-check code both: claude-opus-5-5 204 s $0.7434
- review fix-check claims: claude-sonnet-5-5 30 s $0.1035
- fix fix-check implementer: claude-sonnet-5-5 155 s $0.2664
- review fix-check reviewer: claude-sonnet-5-5 319 s $0.1671
- review fix-check reviewer both: claude-opus-5-5 215 s $0.7622
- review fix-check reviewer claims: claude-sonnet-5-5 55 s $0.1387
Finding outcomes: {"duplicate":2,"fixed":9,"incorrect":3,"nonblocking":3}. Classes: {"accepted ADR contradicted by decided behavior":2,"architecture 11":1,"liveness assumed":1,"liveness assumed from socket file":1,"testing 1":1,"testing 1 fake diverges from probe":1,"testing 10":2,"testing 2":1,"testing 3 liveness guards unproven":1,"testing 7":4,"undocumented decision":1,"user data overwritten, liveness assumed":1}.
{
 "findings": [
  {
   "id": "R1",
   "role": "code",
   "class": "architecture 11",
   "outcome": "fixed",
   "path": "src/updater/index.ts:156",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R2",
   "role": "code",
   "class": "testing 10",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:240",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R3",
   "role": "code",
   "class": "testing 10",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:290",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R4",
   "role": "code",
   "class": "user data overwritten, liveness assumed",
   "outcome": "fixed",
   "path": "src/updater/index.ts:104",
   "round": "1",
   "fix": "41f3b2f0ce1d73edba859133e67b80a4cbaee6b1"
  },
  {
   "id": "R5",
   "role": "spec",
   "class": "testing 7",
   "outcome": "incorrect",
   "path": "src/updater/index.test.ts:1116",
   "round": "1"
  },
  {
   "id": "R6",
   "role": "code",
   "class": "testing 2",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:283",
   "round": "2",
   "fix": "26ce7610"
  },
  {
   "id": "R7",
   "role": "code",
   "class": "testing 1",
   "outcome": "fixed",
   "path": "src/updater/index.test.ts:324",
   "round": "2",
   "fix": "26ce7610"
  },
  {
   "id": "R8",
   "role": "code",
   "class": "liveness assumed from socket file",
   "outcome": "fixed",
   "path": "src/updater/index.ts:148",
   "round": "2",
   "fix": "26ce7610"
  },
  {
   "id": "R9",
   "role": "claims",
   "class": "accepted ADR contradicted by decided behavior",
   "outcome": "fixed",
   "path": "docs/internal/adr/0042-simlock-updates-itself-through-a-frozen-call-and-a-detached-updater.md:128",
   "round": "2",
   "fix": "#835 body amended 2026-10-22 11:15; ADR 0042 b90f0337"
  },
  {
   "id": "R10",
   "role": "spec",
   "class": "testing 7",
   "outcome": "duplicate",
   "path": "src/updater/index.test.ts:283",
   "round": "2",
   "canonicalId": "R9"
  },
  {
   "id": "R11",
   "role": "spec",
   "class": "accepted ADR contradicted by decided behavior",
   "outcome": "duplicate",
   "path": "src/updater/index.ts:126",
   "round": "2",
   "canonicalId": "R9"
  },
  {
   "id": "R12",
   "role": "spec",
   "class": "undocumented decision",
   "outcome": "nonblocking",
   "path": "src/updater/index.ts:126",
   "round": "2"
  },
  {
   "id": "R13",
   "role": "spec",
   "class": "testing 7",
   "outcome": "incorrect",
   "path": "src/updater/index.test.ts:287",
   "round": "2"
  },
  {
   "id": "R14",
   "role": "spec",
   "class": "testing 7",
   "outcome": "incorrect",
   "path": "src/updater/index.test.ts:121",
   "round": "2"
  },
  {
   "id": "R15",
   "role": "code",
   "class": "liveness assumed",
   "outcome": "fixed",
   "path": "src/updater/probe.ts:41",
   "round": "fix-check",
   "fix": "81ad9468"
  },
  {
   "id": "R16",
   "role": "code",
   "class": "testing 3 liveness guards unproven",
   "outcome": "nonblocking",
   "path": "src/updater/index.ts",
   "round": "fix-check",
   "reason": "maintainer 2026-10-22: accepted as is; code verified on real sockets"
  },
  {
   "id": "R17",
   "role": "code",
   "class": "testing 1 fake diverges from probe",
   "outcome": "nonblocking",
   "path": "src/updater/index.test.ts",
   "round": "fix-check"
  }
 ]
}

Written by an agent.

@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review notes

These do not block, and nobody has verified them. Each is one reviewer's claim.

  • spec: src/updater/index.ts R12: the branch that skips the restore and the second start appears in no failure-mode row of The updater installs a Simlock version with npm, restarts the daemon, and rolls back one that does not start #835.
  • code: src/updater/index.ts:62,87 R16: the ?? true default in boundProbe.listening and the --pid exit clause in waitUntilStopped have no test that fails without them.
  • code: src/updater/index.test.ts:63 R17: FakeProbe.listening() ignores the new daemon, so "new daemon holds the socket and never answers" does not model a held socket.

Written by an agent.

@V3RON
V3RON marked this pull request as ready for review October 22, 2026 09:47
@V3RON V3RON mentioned this pull request Oct 22, 2026
10 of 11 tasks
@V3RON
V3RON merged commit 230b487 into main Oct 22, 2026
32 checks passed
@V3RON
V3RON deleted the task/835 branch October 22, 2026 20:39
V3RON added a commit that referenced this pull request Oct 22, 2026
Closes #839

### Status
Implement: done Review: passed, round 2 Hardware: n/a Gate: the stack
gate (`merge-pr.sh 896`)
Next: final stack sync, then the gate

### Done when

- [x] `pnpm exec vitest run --project unit` on the changed `ui/src` test
files passes with the lines above.
- [x] `pnpm test:console --project=chromium e2e/console/update.spec.ts`
passes.
- [ ] CI's `pnpm check` is green, the dependency rules included.
- [x] `console.mdx` documents the notice, the Update and Update now
items, the `updating` state, the failure banner, the Inbox item and the
footer notice.

### Completion conditions (#825)

- [x] `simlock update` with no leases installs the latest and `simlock
status` prints it: `e2e/update/update.e2e.test.ts` (real npm via
verdaccio, #885).
- [x] With a lease held the update waits, a new lease gets
`DAEMON_UPDATING`, releasing it lets the update go on:
`src/daemon/server.test.ts` `daemon.update` table (#885).
- [x] A failed install leaves the old daemon serving and `simlock
status` prints `Last update failed (installing)`: server.test.ts
failed-line case and `src/cli/index.test.ts` (#885).
- [x] A new version that does not start is rolled back and the failure
shows with stage `restarting`: `src/updater/index.test.ts` (#878) and
the server.test.ts result-file case (#885).
- [x] `simlock worker update` updates a connected and an incompatible
worker, routes around it, and keeps a drain:
`src/gateway/service.test.ts`, `src/daemon/gateway-fleet.e2e.test.ts`
(#888).
- [x] `simlock status` and `simlock worker list` show the latest after
the check, and nothing with `update.check: false`:
`src/daemon/main.test.ts` version-check cases, CLI tables (#887, #888).
- [x] The console shows `<latest> available`, Update…, `updating` with
its stage, the failure banner and Inbox item, and the sidebar notice:
`ui/src` model and screen tests and `e2e/console/update.spec.ts` (this
PR).

## Assumptions

- Assumption: Update now answers `upToDate` with `<name> already runs
<version>` (the spec lists the toast for the answer; the force action
had a fixed success title).
- Assumption: the footer notice is a `UpdateNotice` in `shell.tsx` and
`watchMode` takes a `clock`, so its 10-minute timer is testable with
fake time.

- Assumption: the row's stage line shows only while State reads
`updating` — a red finding (disconnected, failed) hides it, as it hides
the amber word.
- Assumption: Update… and Update now sit after Drain/Undrain in the menu
(the picked prototype's order), so the page's primary button stays
Drain.
- Assumption: the Inbox fact When reads `<duration> ago`, as the
disconnected item's Last seen does.
- Assumption: `isNewerVersion` is imported by `version-order.ts`'s own
path (the spec's value import); the `past-index` rule allows that one
file, because the index carries zod's `eval`, which breaks the console's
CSP (maintainer-authorized 2026-10-22, #825 Decisions).

## Review

Spec review: 1 blocking, 1 fixed, 0 notes. Code review: 1 blocking, 1
fixed, 1 notes. Claims review: 1 blocking, 1 fixed, 0 notes.
Rounds: 2. Tokens: 45.0M.
Runs: 11. Cost USD: 6.9344. Agent seconds: 3898.

Assessment: complete

*Written by an agent.*
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.

The updater installs a Simlock version with npm, restarts the daemon, and rolls back one that does not start

1 participant