Skip to content

feat: Write the updater unit beside the service unit - #1839

Closed
huang195 wants to merge 3 commits into
rossoctl:mainfrom
huang195:feat/auto-update-1836-pP2
Closed

huang195 wants to merge 3 commits into
rossoctl:mainfrom
huang195:feat/auto-update-1836-pP2

Conversation

@huang195

@huang195 huang195 commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Part of #1836.

This is PR P2 of 2 in the plan docs/superpowers/plans/2026-11-12-auto-update-1836.md. The plan title for P2 is "feat: Start the updater once a day from the laptop service". This branch has only task T3 of P2, so the title names only T3. The PR is a draft because tasks T4 and T8 failed and their work is not in this branch (see "Not in this PR").

What changed

T3, the updater unit (cmd/agentop):

  • agentop service install writes a second unit beside the service unit:
    • darwin: ~/Library/LaunchAgents/io.rossoctl.cortex.update.plist, label io.rossoctl.cortex.update.
    • linux: ~/.config/systemd/user/cortex-update.service, Type=oneshot, with no [Install] section and no timer.
  • The unit runs agentop update --yes --auto and writes its output to ~/.cortex/update.log. In this PR, --auto is only a string. P1 (feat: Update a laptop service install automatically #1838) adds the flag.
  • On darwin, install bootstraps the updater only when the label is not loaded. Install never boots out the updater, because the updater job can be the process that runs the install.
  • If install cannot write or load the updater, it prints a warning. The install does not fail.
  • serviceIsCurrent returns false when the updater file is missing or different. So an install from before this change gets the updater at the next agentop service install or agentop setup.
  • Uninstall boots out the updater (darwin) and removes its file.
  • The setup service step's undo now also restores the updater file. TestSetupUpgradesAPreRenameSupervisedCortex found a leftover updater plist after a rollback.

The plan document is also in this PR.

How it was tested

  • New tests in cmd/agentop/cmd_service_updater_test.go: render (darwin and linux), install writes the unit, install never boots out the updater, uninstall removes the unit, and serviceIsCurrent is false without the updater unit.

  • The full plan gate passed on this branch tip (exit 0, cmd/agentop: 1033 tests in 8 shards, 157s):

    (cd core && GOWORK=off go vet ./... && GOWORK=off go test ./config/...) && (cd cmd/cortex && GOWORK=off go vet ./... && GOWORK=off go test ./...) && (cd cmd/agentop && GOWORK=off go vet ./... && GOWORK=off go test -race $(GOWORK=off go list ./... | grep -v 'agentop$')) && scripts/dev/go-test-shards.sh -race cmd/agentop && test -z "$(gofmt -l core cmd/cortex cmd/agentop)"
  • The linux paths ran only with stubs on darwin. TestServiceIsCurrentFalseWithoutUpdaterUnit covers both GOOS values. The install and uninstall tests use runtime.GOOS, so the CI Linux job runs their linux branch.

Size

  • Counted: 392 lines (376 added, 16 removed).
  • Exempt: 374 lines (the plan document).

Counted lines by file: cmd_service.go +32/-7, cmd_service_platform.go +79, cmd_service_updater_test.go +238, cmd_service_test.go +9/-8, setup_step_service.go +7/-1, and 11 lines in three integration and GOOS test files.

Not in this PR

These tasks failed. Their work is not in this branch:

  • T4, the trigger in the proxy (cmd/cortex/update_trigger.go). The commit is 8061ad30 in the auto-update-1836-tT4 worktree. Its gate run failed only in scripts/dev/go-test-shards.sh -race cmd/agentop, with timing failures (TestSetupReadyInLeavesOutThePrompt, TestStartUnsupervised, TestStartByHandRunsFromAShell) and one shard timeout, at a load average of 6 to 8.5. T4 changes no code in cmd/agentop. The three tests passed alone with -race. A clean gate run on that commit is still necessary.
  • T8, docs (docs/laptop-service.md, cmd/agentop/README.md, CONTRIBUTING.md). The commit is c24fa2e6. Its gate run failed for the same load-related timing reason. TestSetupReadyInLeavesOutThePrompt also failed 3 of 3 times on the PR base with no T8 change.

Without T4, nothing starts the updater unit. Do not merge this PR as the last part of #1836.

Review findings not fixed (all minor)

  • Gate: the full gate passed on a74a582. Every T3 item in the plan is present, including Review Focus tests 1 and 5. The one change outside the plan (the setup undo restores updaterFile) has a reason, given above.
  • cmd/agentop/cmd_service.go runServiceInstall: when loadService fails, install removes the unit file but leaves the new updater file. agentop service uninstall then prints "Nothing to do" and does not remove the updater file. On darwin the file is not loaded, and the next install writes it again, so the only effect is an orphan file. One os.Remove(p.updaterFile) beside the unit removal fixes it.
  • cmd/agentop/cmd_service.go removeService and cmd_uninstall.go (line ~886): an updater remove error goes into stampErr. So the message says "could not remove <proxy.sha256>: ...", and uninstall lists the stamp file in left, not the updater file. The os error text still names the updater path. The plan's "no signature change" rule causes this.
  • cmd/agentop/setup_step_service.go undo: on a first install, undo removes the updater file but does not boot out an updater that loadUpdater bootstrapped. So launchd keeps a loaded job with no plist. This is intentional, because setup can run inside the updater job. The next install sees the label is loaded and does not bootstrap it again. If the plist is different by then, the loaded definition stays old until uninstall. The plan accepts this shortcut.
  • The linux render, install and uninstall paths ran only with stubs on darwin. The CI Linux job runs the linux branch.

For a human after review

Needs T3, T4, T5 and T6 merged (both PRs).

  • Live check on a Mac. This restarts the proxy and cuts every attached session.
    1. Build the PR's agentop and cortex with -ldflags "-X main.version=v0.9.4", so that they read as a plain release older than Latest.
    2. Install them with agentop setup --from <dir> as a service install.
    3. Run launchctl kickstart gui/$(id -u)/io.rossoctl.cortex.update.
    4. Check that the proxy restarts on Latest, that update.json says updated, and that an open TUI shows restart agentop.
      Note: Latest must be more than 72 hours old for step 4; else expect waiting.
  • The same on Linux with systemd (not tested in the design work: podman has no user systemd).
  • Behind a corporate proxy: the launchd job does not inherit the user's HTTPS_PROXY. Decide whether that needs a fix.
  • Release order: do not tag a release that contains Automatic updates for the laptop service #1836 without Check build provenance before agentop update installs a release #1837. Do not tag one that has P1 without P2, or P2 without P1. Merge order does not matter.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
agentop service install now writes a second unit, the updater. It runs
"agentop update --yes --auto". The proxy starts it once a day.

- macOS: ~/Library/LaunchAgents/io.rossoctl.cortex.update.plist, with
  no RunAtLoad, KeepAlive or StartInterval. Install bootstraps it only
  when launchd does not have the label. Install never boots it out,
  because the updater job can be the process that runs the install.
- Linux: ~/.config/systemd/user/cortex-update.service, Type=oneshot,
  with no [Install] section and no timer.
- serviceIsCurrent is false when the updater file is missing or stale,
  so an older install gets the updater at the next install or setup.
- Uninstall boots out and removes the updater.
- The setup service step puts back the updater file on rollback.

Part of rossoctl#1836.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@coderabbitai

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@huang195

Copy link
Copy Markdown
Member Author

This PR is closed. Its commits (the updater unit, the proxy trigger and the docs) are now in #1838, which is one PR for all of #1836.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant