Skip to content

feat(daemon): simlock update and POST /v1/update update one daemon after its leases end - #885

Merged
V3RON merged 38 commits into
task/835from
task/836
Oct 22, 2026
Merged

V3RON merged 38 commits into
task/835from
task/836

Conversation

@V3RON

@V3RON V3RON commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #836

Status

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

  • pnpm test:changed passes (it runs the unit tests under src/daemon src/leasing src/contract src/cli src/http src/simlock-client src/gateway), and so does pnpm test:e2e e2e/update/update.e2e.test.ts. Delivered by: the tests in src/daemon/server.test.ts (describe("daemon.update")), src/contract/version-order.test.ts, src/contract/operations.test.ts, src/cli/index.test.ts (simlock update and simlock status), src/http/app.test.ts (POST /v1/update), e2e/update/update.e2e.test.ts.
  • CI's check job passes, including the protocol bump check. Delivered by: PROTOCOL_VERSION_RANGE 54 → 55 in src/contract/protocol.ts; version-order.ts is in the bump check's NOT_SHAPES.
  • website/src/content/docs/docs/reference/cli.mdx documents simlock update (section "simlock update" and the simlock status lines), reference/errors.mdx the codes (DAEMON_UPDATING, and "Updating Simlock": UPDATE_RESOLVE_FAILED, UPDATE_UNSUPPORTED_INSTALL, UPDATE_SHARED_INSTALL), reference/events.mdx and docs/internal/EVENTS.md the events (daemon.update-started, daemon.update-failed, and updating among lease.rejected's reasons), using-devices/programmatic-client.mdx the client method ("Update the daemon"); the route is declared (updateRoute in src/http/admin-declarations.ts, both 202 and 200), so the OpenAPI reference lists it (website/scripts/check-openapi.mjs passes).

Assumptions

  • Assumption: order of the checks in a new update — NpmCli.view first (UPDATE_RESOLVE_FAILED), then "same version" and "running newer than latest, no version named" answer upToDate without asking checkInstall, then checkInstall, then the other role's socket — cheapest first, and each refusal changes nothing.
  • Assumption: simlock update --version X when X is the running version prints simlock X is already running (not "latest", which would be false when X is not the newest).
  • Assumption: --json prints no progress and one line at the end: the daemon's final daemon.update answer (the first answer when nothing was needed); the exit code is the same as without it.
  • Assumption: Ctrl-C while following prints Stopped following. The update goes on; run simlock status to see it. and exits 0; it sends nothing to the daemon.
  • Assumption: an updater that exits non-zero with no failed line is a failure at the last stage it reported (installing by default), with the message the updater exited with code N.
  • Assumption: a request already being provisioned when the update begins is granted, so it is not cancelled midway; every other request, a retry after a failed provision included, gets DAEMON_UPDATING (maintainer decision, Simlock updates itself, and a gateway updates a chosen worker #825).
  • Assumption: with no daemon running, simlock update starts one first like any other command, then updates it.
  • Found by the real-npm e2e and fixed here: NodeUpdaterLauncher kept the daemon alive through the updater's stdout pipe, so the updater waited ten minutes for a daemon that could not exit; the pipe is now unref'd (src/ports/updater-launcher.ts).

Review

Spec review: 1 blocking, 1 fixed, 0 notes. Code review: 11 blocking, 10 fixed, 0 notes. Claims review: 4 blocking, 4 fixed, 1 notes.
Rounds: 2. Tokens: 27.7M.
Runs: 20. Cost USD: 26.8065. Agent seconds: 13732.

Assessment: complete

Rejected:

  • code: R13 the retry test passes against the old filter too (its request never sat in the queue) — [nonblocking] accepted by the maintainer 2026-10-22; R12's fix verified by the reviewer's probe on the real paths

Written by an agent.

V3RON added 24 commits October 22, 2026 11:59
…l version, and the updater probe reads through query (#836)
…te-failed, and a result file is reported at start (#836)
…simlock status shows the version, the update and the last failure (#836)
…00 when it followed one or nothing was needed (#836)
… local npm registry and refuses a missing version (#836)
@V3RON
V3RON added this pull request to stack #881 October 22, 2026 11:09
V3RON added 5 commits October 22, 2026 13:12
…dable result file, gateway refusal, error table, launcher pipe, CLI usage and give-up bound (#836)

Also drops the CLI's from/to match on a failure (the daemon clears the last failure when an update begins) and an unneeded default on the daemon.update frame payload.
…, and the result file is read before health is running (#836)
@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review ledger

Round 1: bb9fa36
Round 2: 3e8e0d2
Round fix-check: d0479f1
Round fix-check: 7b478f9

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

Runs: 20. Cost USD: 26.8065. Agent seconds: 13732.
Tokens input 1538, output 492905, cache read 123535558, cache write 2347258.
- build implementer: claude-sonnet-5-5 4895 s $12.5896
- audit implementer: claude-sonnet-5-5 3208 s $2.2886
- fix implementer: claude-sonnet-5-5 103 s $0.0824
- review 1 reviewer: claude-sonnet-5-5 777 s $0.3661
- review 1 reviewer spec: claude-sonnet-5-5 100 s $0.481
- review 1 reviewer both: claude-opus-5-5 646 s $3.4305
- review 1 reviewer claims: claude-sonnet-5-5 103 s $0.3107
- fix 1 implementer: claude-sonnet-5-5 483 s $0.7568
- review 2 reviewer: claude-sonnet-5-5 592 s $0.4596
- review 2 code both: claude-opus-5-5 416 s $1.7285
- review 2 claims: claude-sonnet-5-5 60 s $0.1714
- fix 2 implementer: claude-sonnet-5-5 535 s $0.7167
- fix fix-check implementer: claude-sonnet-5-5 81 s $0.1007
- review fix-check reviewer: claude-sonnet-5-5 596 s $0.4064
- review fix-check code both: claude-opus-5-5 439 s $1.5705
- review fix-check claims: claude-sonnet-5-5 94 s $0.2489
- fix fix-check implementer: claude-sonnet-5-5 147 s $0.2069
- review fix-check reviewer: claude-sonnet-5-5 259 s $0.2109
- review fix-check code-reviewer both: claude-opus-5-5 167 s $0.5759
- review fix-check claims-reviewer: claude-sonnet-5-5 31 s $0.1044
Finding outcomes: {"fixed":14,"nonblocking":2}. Classes: {"ADR 0042 1 vs 8: query parks":1,"ADR 0042 3 update hold skips new waiters":1,"ADR 0042 3 update hold: retry of once-queued waiter granted":1,"ADR 0042 3; false manual claim":1,"architecture 13 comment cites ADR contrary to code":1,"architecture 13 false claim":1,"architecture 13 false claim (broken anchor)":1,"client leaked per reconnect loop pass":1,"event emitted before subscribers released":1,"leasing manual hold omits new waiters":1,"manual says readiness runs for daemon.update query":1,"manual step 5 omits daemon.update query exception":1,"state read before startup report":1,"testing 3":2,"testing 3 hold refusal of retry untested":1}.
{
 "findings": [
  {
   "id": "R1",
   "role": "code",
   "class": "state read before startup report",
   "outcome": "fixed",
   "path": "src/daemon/server.ts",
   "round": "1",
   "fix": "3e8e0d2b13b60a529ded4a92550edd1def82b3f0"
  },
  {
   "id": "R2",
   "role": "code",
   "class": "ADR 0042 3; false manual claim",
   "outcome": "fixed",
   "path": "src/leasing/lease-acquisition-coordinator.ts",
   "round": "1",
   "fix": "3e8e0d2b13b60a529ded4a92550edd1def82b3f0"
  },
  {
   "id": "R3",
   "role": "code",
   "class": "testing 3",
   "outcome": "fixed",
   "path": "src/daemon/update-coordinator.ts",
   "round": "1",
   "fix": "3e8e0d2b13b60a529ded4a92550edd1def82b3f0"
  },
  {
   "id": "R4",
   "role": "code",
   "class": "testing 3",
   "outcome": "fixed",
   "path": "src/contract/operations.ts",
   "round": "1",
   "fix": "3e8e0d2b13b60a529ded4a92550edd1def82b3f0"
  },
  {
   "id": "R5",
   "role": "code",
   "class": "ADR 0042 1 vs 8: query parks",
   "outcome": "fixed",
   "path": "src/updater/probe.ts",
   "round": "1",
   "fix": "3e8e0d2b13b60a529ded4a92550edd1def82b3f0"
  },
  {
   "id": "R6",
   "role": "spec",
   "class": "architecture 13 false claim (broken anchor)",
   "outcome": "fixed",
   "path": "website/src/content/docs/docs/worker/running-the-daemon.mdx",
   "round": "1",
   "fix": "3e8e0d2b13b60a529ded4a92550edd1def82b3f0"
  },
  {
   "id": "R7",
   "role": "claims",
   "class": "architecture 13 false claim",
   "outcome": "fixed",
   "path": "website/src/content/docs/docs/reference/events.mdx",
   "round": "1",
   "fix": "3e8e0d2b13b60a529ded4a92550edd1def82b3f0"
  },
  {
   "id": "R8",
   "role": "code",
   "class": "client leaked per reconnect loop pass",
   "outcome": "fixed",
   "path": "src/cli/commands/update.ts",
   "round": "2",
   "fix": "3e8e0d2b..d0479f1e"
  },
  {
   "id": "R9",
   "role": "code",
   "class": "event emitted before subscribers released",
   "outcome": "fixed",
   "path": "src/daemon/server.ts",
   "round": "2",
   "fix": "3e8e0d2b..d0479f1e"
  },
  {
   "id": "R10",
   "role": "code",
   "class": "ADR 0042 3 update hold skips new waiters",
   "outcome": "fixed",
   "path": "src/leasing/lease-acquisition-coordinator.ts",
   "round": "2",
   "fix": "7b478f9f (as R12)"
  },
  {
   "id": "R11",
   "role": "claims",
   "class": "architecture 13 comment cites ADR contrary to code",
   "outcome": "fixed",
   "path": "src/daemon/dispatch.ts",
   "round": "2",
   "fix": "3e8e0d2b..d0479f1e"
  },
  {
   "id": "R12",
   "role": "code",
   "class": "ADR 0042 3 update hold: retry of once-queued waiter granted",
   "outcome": "fixed",
   "path": "src/leasing/lease-acquisition-coordinator.ts",
   "round": "fix-check",
   "fix": "7b478f9f"
  },
  {
   "id": "R13",
   "role": "code",
   "class": "testing 3 hold refusal of retry untested",
   "outcome": "nonblocking",
   "path": "src/leasing/lease-acquisition-coordinator.ts",
   "round": "fix-check",
   "reason": "maintainer 2026-10-22: accepted as is; R12 behaviour verified by the reviewer probe"
  },
  {
   "id": "R14",
   "role": "claims",
   "class": "manual says readiness runs for daemon.update query",
   "outcome": "fixed",
   "path": "src/contract/AGENTS.md",
   "round": "fix-check",
   "fix": "7b478f9f"
  },
  {
   "id": "R15",
   "role": "claims",
   "class": "manual step 5 omits daemon.update query exception",
   "outcome": "fixed",
   "path": "src/daemon/AGENTS.md",
   "round": "fix-check",
   "fix": "7b478f9f"
  },
  {
   "id": "R16",
   "role": "claims",
   "class": "leasing manual hold omits new waiters",
   "outcome": "nonblocking",
   "path": "src/leasing/AGENTS.md",
   "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.

  • code: src/leasing/create-leasing.test.ts:4619 R13: the retry test passes against the old filter too. A request queued first, then provisioning, then held, then failing, would make it bite. That one test would also cover the #defer re-queue path.
  • claims: src/leasing/AGENTS.md:30 R16: the manual line about the update hold was incomplete. It was reworded in 7b478f9.

Written by an agent.

@V3RON
V3RON marked this pull request as ready for review October 22, 2026 13:08
@V3RON V3RON mentioned this pull request Oct 22, 2026
10 of 11 tasks
@V3RON
V3RON merged commit 176b264 into main Oct 22, 2026
52 checks passed
@V3RON
V3RON deleted the task/836 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.

simlock update and POST /v1/update update one daemon after its leases end

1 participant