Skip to content

feat: Make the auto-update wait and check interval configurable - #1841

Open
huang195 wants to merge 2 commits into
rossoctl:mainfrom
huang195:feat/update-timing
Open

huang195 wants to merge 2 commits into
rossoctl:mainfrom
huang195:feat/update-timing

Conversation

@huang195

Copy link
Copy Markdown
Member

Summary

Auto-update (#1838) waited 72 hours after a release and checked once a day. So a new release reached a laptop 3 to 4 days after its publication. This PR makes the defaults shorter and makes both values configurable in ~/.cortex/config.yaml:

update:
  wait: 24h        # default; 0 or more; "0" installs at once
  check_every: 6h  # default; from 1h to 24h
Field Who reads it When a change applies
wait agentop update --auto, on each run at the next check
check_every the proxy (cmd/cortex/update_trigger.go), at boot after a restart. A hot reload refuses the change

With the defaults, a laptop that is on installs a release 24 to about 30 hours after its publication.

What changed:

  • core/config: UpdateConfig.Wait, UpdateConfig.CheckEvery and Times(). Validate refuses a bad value.
  • core/reloader: validateReloadable refuses an update.check_every change. auto and wait still reload.
  • cmd/cortex: the trigger uses check_every in place of the fixed 24 hours.
  • cmd/agentop: --auto reads update.wait. It reads only the update: block, as before. A bad wait gives the default and does not cancel update.auto: false. agentop doctor shows a bad value on the updates row.
  • Docs: docs/laptop-service.md (a table of the fields), CONTRIBUTING.md (stopping a bad release), cmd/agentop/README.md, the agentop update help text, and the restart list in CLAUDE.md.

Why the range of check_every:

  • At least 1h, because the trigger looks once an hour.
  • At most 24h, because the title bar and doctor report "no update check" after 72 hours.

Not changed: the 72-hour "no update check since" limit in the TUI and in doctor. With a check at most every 24 hours, it still means 3 missed checks or more.

How it was tested

  • core: go vet ./..., go test ./config/... ./reloader/.... New tests: TestUpdateTimes, TestValidateRefusesBadUpdateTimes, TestValidateReloadable_UpdateCheckEveryNeedsRestart.
  • cmd/cortex: go vet ./..., go test ./.... The trigger tests use a 6-hour interval. TestUpdateTriggerUsesEvery checks a 24-hour interval.
  • cmd/agentop: scripts/dev/go-test-shards.sh -race cmd/agentop (1051 tests in 8 shards, passed) and go test -race of the subpackages (passed). New rows in TestAutoUpdateDecisions: wait set, wait 0, wait invalid, wait invalid, disabled. New test: TestDoctorUpdatesBadTimes.
  • gofmt -l is clean. pre-commit run on the changed files passed.

Not tested: a live update on a Mac or on Linux.

Note for #1840

This PR changes the --auto help line in cmd/agentop/cmd_update.go. If #1840 changes the same text, the second PR to merge must resolve a small conflict.

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

Auto-update waited 72 hours after a release and checked once a day, so a
new release reached a laptop 3 to 4 days after its publication.

- update.wait (default 24h, 0 or more): how long after its publication
  agentop update --auto waits before it installs a release. agentop reads
  it on each run.
- update.check_every (default 6h, 1h to 24h): how often the proxy starts
  the updater. The proxy reads it at boot, so a hot reload refuses a
  change.
- Validate refuses a bad value. agentop doctor shows it on the updates row.
  A bad wait does not cancel update.auto: false.

With the defaults, a laptop that is on installs a release 24 to about 30
hours after its publication.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195
huang195 requested a review from a team as a code owner November 13, 2026 01:10
@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4b499b6f-8f51-4e65-a623-1620ac6a20e6
Commits

Reviewing files that changed from the base of the PR and between ccff43e and 503f39e.

Files selected for processing: 18

  • CLAUDE.md
  • CONTRIBUTING.md
  • cmd/agentop/README.md
  • cmd/agentop/cmd_doctor_checks.go
  • cmd/agentop/cmd_doctor_updates_test.go
  • cmd/agentop/cmd_service_platform.go
  • cmd/agentop/cmd_update.go
  • cmd/agentop/cmd_update_auto.go
  • cmd/agentop/cmd_update_auto_test.go
  • cmd/cortex/main.go
  • cmd/cortex/update_trigger.go
  • cmd/cortex/update_trigger_test.go
  • core/config/config.go
  • core/config/update_test.go
  • core/config/validate.go
  • core/reloader/reloader.go
  • core/reloader/reloader_test.go
  • docs/laptop-service.md
  • 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

@github-actions github-actions Bot added the ready-for-ai-review Request automated AI code review from clawgenti label Nov 13, 2026

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The timing knobs work as described: I probed Times() through config.Load with 0, "0", 0s, -0, -1h, 1d, 24 (no unit), 1.5h, true, null, "", a leading space, 59m, 25h and 300000h. Each one is accepted or refused as the docs say, and the boot/reload split for check_every is right. There is one must-fix. Because autoUpdateOff now shares a decode with the new wait/check_every fields, a wrong-type value in either field (a list or a map) silently turns an explicit update.auto: false back on. On the base commit the same config stays disabled. Details are inline.

Author: huang195 (MEMBER — maintainer)
Areas reviewed: Go (core/config, core/reloader, cmd/cortex, cmd/agentop), Docs
Agent/IDE config (.claude/.vscode): none
Commits: 1 commit, all signed-off: yes
CI status: passing

Local checks at c5f9e70: go test ./config/ ./reloader/ (core), the UpdateTrigger tests (cmd/cortex) and the Update tests (cmd/agentop) pass, and gofmt -l is clean. The PR also merges cleanly with main after #1840.

@@ -108,9 +116,9 @@ func autoUpdateOff(path string) bool {
return "${" + key + "}"
})
if yaml.Unmarshal([]byte(expanded), &c) != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must-fix: a wrong-type wait or check_every cancels an explicit update.auto: false.

autoUpdateOff used to decode a struct that had only auto. Now it shares readUpdateConfig, which decodes the whole UpdateConfig, so a type error in either new string field makes yaml.Unmarshal return an error. This line then returns nil, and nil means "auto on". I probed runUpdate --auto with the autoUpdateRows harness, a release 100h old:

config base 608e8d2 head c5f9e70
auto: false + wait: [24h] disabled updated, installer ran
auto: false + wait: {h: 24} disabled updated, installer ran
auto: false + check_every: [6h] disabled updated, installer ran
auto: false + wait: soon disabled disabled

This breaks the PR's own claim ("A bad wait gives the default and does not cancel update.auto: false") and the comment on readUpdateConfig ("an error in another field does not cancel the opt-out"). The new wait invalid, disabled row only covers a scalar. agentop doctor then shows "auto on" too, because Times() on a nil block reports no error. The proxy will not start with such a file, but a hot-reload edit leaves the old proxy running, and then the updater installs a release and restarts the proxy against the user's opt-out.

Two possible fixes:

  • Keep a separate auto-only decode for autoUpdateOff, as before, and use readUpdateConfig only for the timings.
  • Or treat a *yaml.TypeError as a partial decode. yaml.v3 keeps decoding the other fields after a type error, so c.Update.Auto is still set. In that case return the block (or a marker) so that Times()/doctor can report the bad field.

Please add a list-valued wait row with auto: false to autoUpdateRows.

Comment thread core/reloader/reloader.go
// validateReloadable returns an error when a newly-loaded config
// differs from the active one on a field that cannot be reloaded
// under the running listeners.
func updateCheckEvery(c *config.Config) string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the new helper sits between the validateReloadable doc comment (lines 578-580) and the function it describes. So godoc now attaches that comment to updateCheckEvery, and validateReloadable has none. Move the helper above the comment, or below validateReloadable.

Comment thread core/reloader/reloader.go
}
// update.check_every too: the update trigger reads it once at boot. update.auto
// and update.wait reload, because agentop reads them on each run.
if updateCheckEvery(active) != updateCheckEvery(next) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this compares the raw strings, so equivalent values are refused as changes. Examples: unset to 6h (the default), and 6h to 360m. The safe direction is to refuse, so this is fine. If you want the message to match the behavior, compare the durations from Times() instead (both configs passed Validate already).

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review at 503f39e. This commit only merges main (#1840) into the branch. The PR's own changes are byte-identical to c5f9e70, and the merge left no stale "72 hours" or "daily" text in the update docs or code. The must-fix from my previous review is still open: a wrong-type update.wait or update.check_every (a list or a map) makes readUpdateConfig return nil, and that silently turns an explicit update.auto: false back on. I added two rows to autoUpdateRows at 503f39e (auto: false with wait: [24h], and auto: false with check_every: [6h], a release 100h old). Both give result "updated" ran true, want "disabled". On base, the same configs stay disabled. The fix options are in the earlier inline thread on cmd/agentop/cmd_update_auto.go, and one new inline comment below marks the line. The two nits in core/reloader/reloader.go (the misplaced doc comment, and the raw-string compare of check_every) are still open. They do not block.

Author: @huang195 (MEMBER — maintainer)
Areas reviewed: Go (core/config, core/reloader, cmd/cortex, cmd/agentop), Docs
Agent/IDE config (.claude/.vscode): none
Commits: 1 commit plus 1 merge commit. The feature commit is signed off, and DCO passes.
CI status: 34 pass, 1 pending, 2 skipping, none failing

Local checks at 503f39e: go test ./config/ ./reloader/ (core), go test -run UpdateTrigger (cmd/cortex) and go test -run 'Update|Doctor' (cmd/agentop) pass, and gofmt -l is clean.

@@ -108,9 +116,9 @@ func autoUpdateOff(path string) bool {
return "${" + key + "}"
})
if yaml.Unmarshal([]byte(expanded), &c) != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must-fix (still open at 503f39e): when wait or check_every has the wrong type, yaml.Unmarshal returns a *yaml.TypeError. This line then returns nil, so autoUpdateOff reports auto on even with auto: false. I reproduced it at this head with auto: false + wait: [24h] and with auto: false + check_every: [6h]: the installer ran in both cases. Please keep an auto-only decode for the opt-out, or treat a *yaml.TypeError as a partial decode (yaml.v3 still sets Auto). Please also add a list-valued row with auto: false to autoUpdateRows.

This branch has not been deployed

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

Labels

ready-for-ai-review Request automated AI code review from clawgenti

Projects

Status: New/ToDo

Development

Successfully merging this pull request may close these issues.

2 participants