Skip to content

feat: run speed modes on the device that downloads - #646

Merged
linroid merged 4 commits into
mainfrom
feat/remote-speed-modes
Oct 24, 2026
Merged

linroid merged 4 commits into
mainfrom
feat/remote-speed-modes

Conversation

@linroid

@linroid linroid commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

Full speed, the Slow lane and Auto rules ran in the app and drove only the embedded engine, so the Speed page offered a remote device a plain speed limit ("No Slow lane or Auto for it yet."), and a phone that sleeps could not keep a NAS on a schedule. Now the engine of the device doing the downloads runs them, and every device's Speed page offers them.

Design: docs/plans/speed-modes.md.

  • library:api: SpeedModes (a ControlKey controller: state(), update(SpeedModeChange)), SpeedModeState, Interest.SpeedMode and KetchFeatures.SPEED_MODES (speed.modes). SpeedSettings, SpeedRule, SpeedLimitMode and Weekday move here from config, and SpeedScheduler from the app; [speed] reads as before.
  • library:core: Ketch(speedModes = SpeedModeOptions(...)) runs them over its global limit (SpeedModeKeeper): the rules re-evaluated as they start and end, the observed peak recorded from its own tasks, the standing cap kept current, also through updateConfig (another limit sets the cap; writing back the config read changes nothing).
  • REST: GET/PUT /api/speed, the event stream's speedMode interest, and RemoteKetch.controller(SpeedModes), which fails closed against older servers.
  • ketch server runs [speed] and saves changes back to its config file (the cap as [download] speedLimit only once it differs from the one a flag or env var gave it).
  • Apps: the desktop, Android and iOS hosts hand the speed modes to their engine (embeddedSpeedModes), so the app-side SpeedModeController becomes a view of any device's SpeedModes. InstanceManager keeps one per connected device; the Speed page reads a device the app does not keep connected once. The Speed page, Pulse pill and popover, device menus, palette, desktop menu bar and DevicePresence.speedMode work for remote devices; devices on an older Ketch keep the plain limit and their note.

A phone connected to ketch server can now switch it to Slow lane or Auto with rules, and the server follows them while the phone is offline (RemoteSpeedModesTest covers that end to end).

Snapshots

Speed settings of a remote device (NAS-Basement):

Before After
before after
before dark after dark

Phone:

Testing

  • SpeedModeKeeperTest (virtual time: rules starting and ending, cap, slow lane suggestion, peak, saving), KetchSpeedModesTest, SpeedSchedulerTest (moved).
  • RemoteSpeedModesTest: a client switches a real KetchServer to Slow lane and Auto, which the server applies and saves; it follows the interest; older servers fail closed and answer 404.
  • ServerSpeedModesTest (CLI saving), app SpeedModeControllerTest, SpeedSettingsModelTest (embedded, remote, unconnected remote, older remote), DeviceActionsTest, DevicePresenceTest, SpeedModeActionsTest, desktop DesktopCommandsTest.
  • ./gradlew allJvmTests passes; CLI, core JS/WasmWasi, web, Android and iOS compile; checkKotlinAbi updated.

Notes

  • ketch mcp --standalone still runs without speed modes.
  • The connections-per-download setting of a remote device is still not saved there; the note says so.

With #644 (saved download settings)

  • PUT /api/config hands onConfigChanged the standing cap rather than the limit in effect, which is the slow lane's while it holds, so no server saves it as the cap.
  • ketch server saves its speed modes through ServerConfigFile.update, under the lock save takes, so neither write drops the other's; a flag or KETCH_SPEED_LIMIT pinning the cap is warned about as save does.
  • The Speed page's note of a remote device picks among four texts, by speed.modes and config.persisted.

The Slow lane and Auto rules were applied by the app to its embedded
engine only, so a phone could give a NAS a plain speed limit and nothing
more, and a schedule would stop as the phone slept. The engine now runs
them itself, and every device's Speed settings offer them.

- library:api: SpeedModes (a ControlKey controller), SpeedModeState,
  SpeedModeChange, Interest.SpeedMode and the speed.modes feature; the
  [speed] types and SpeedScheduler move here from config and the app.
- library:core: Ketch(speedModes = SpeedModeOptions(...)) runs them over
  its global limit (SpeedModeKeeper): rules evaluated as they start and
  end, the observed peak recorded from its own tasks, and the standing
  cap kept current, also through updateConfig.
- REST GET/PUT /api/speed, the event stream's speedMode interest and
  RemoteKetch.controller(SpeedModes), which fails closed on older
  servers.
- ketch server runs [speed] and saves changes back to its config file.
- Apps: the hosts hand the speed modes to their engine; InstanceManager
  keeps a SpeedModeController per device, so the Speed page, the Pulse
  pill and popover, device menus and DevicePresence work for remote
  devices too. Older devices keep the plain limit and their note.

Design: docs/plans/speed-modes.md.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T03:31:56.290331Z 1028a8b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

 2 217 files  + 9   2 245 suites  +9   56m 58s ⏱️ - 1m 3s
15 508 tests +78  15 239 ✅ +77  269 💤 +1  0 ❌ ±0 
18 875 runs  +78  18 606 ✅ +77  269 💤 +1  0 ❌ ±0 

Results for commit 0e7ddd4. ± Comparison against base commit 18d1de8.

This pull request removes 82 and adds 160 tests. Note that renamed tests count towards both.
com.linroid.ketch.app.instance.DevicePresenceTest ‑ presence_hostSpeedMode_appliesToEmbeddedDeviceOnly[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ autoMode_ruleStartsAndEnds_appliesSlowLaneThenStandingCap[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ init_autoModeInsideRule_appliesSlowLane[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ init_savedFullSpeed_keepsDeviceLimit[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ init_savedSlowLaneBelowMinimum_raisedToMinimum[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ init_savedSlowLaneMode_appliesSlowLane[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ observedPeak_fasterSpeed_raisesPeak[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ observedPeak_inSlowLane_ignoresSpeed[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ observedPeak_peakOlderThanWeek_replacedBySlowerSpeed[jvm]
com.linroid.ketch.app.state.SpeedModeControllerTest ‑ observedPeak_slowerSpeedWithinWeek_keepsPeak[jvm]
…
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_adjacentRules_mergeIntoOneWindow[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_atEnd_fullSpeedUntilNextStart[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_beforeRule_fullSpeedUntilItStarts[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_everyDayAllDay_slowLaneWithoutEnd[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_fallBackNight_endKeepsLocalTime[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_fridayEvening_nextStartIsMonday[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_insideRule_slowLaneUntilItEnds[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_noRules_fullSpeedWithoutEnd[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_overnightRuleAfterMidnight_windowFromPreviousDayStillOpen[jvm]
com.linroid.ketch.api.SpeedSchedulerTest ‑ evaluate_overnightRuleBeforeMidnight_endsNextMorning[jvm]
…

♻️ This comment has been updated with latest results.

- KetchSpeedModesTest names a download folder, as Android host tests
  cannot resolve the default one.
- SpeedSchedulerTest runs on the JVM: Node.js has no time zones but UTC
  unless js-joda's database is loaded.
- ServerSpeedModesTest uses runBlocking, so an exception another CLI test
  leaks no longer fails it.
@linroid
linroid enabled auto-merge (squash) October 24, 2026 06:58
# Conflicts:
#	AGENTS.md
#	app/shared/src/commonMain/composeResources/values-de/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-es/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-fr/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-ja/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-ko/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-pt/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-ru/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-zh-rTW/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values-zh/strings_settings.xml
#	app/shared/src/commonMain/composeResources/values/strings_settings.xml
#	app/shared/src/commonMain/kotlin/com/linroid/ketch/app/ui/settings/SpeedSettingsPage.kt
#	app/shared/src/jvmTest/kotlin/com/linroid/ketch/app/snapshot/SampleData.kt
#	library/api/src/commonMain/kotlin/com/linroid/ketch/api/KetchFeatures.kt
# Conflicts:
#	AGENTS.md
#	app/shared/src/commonMain/kotlin/com/linroid/ketch/app/state/AppState.kt
#	app/shared/src/iosMain/kotlin/com/linroid/ketch/app/MainViewController.kt
#	app/shared/src/jvmTest/kotlin/com/linroid/ketch/app/snapshot/SettingsDeviceSnapshots.kt
#	cli/src/main/kotlin/com/linroid/ketch/cli/Main.kt
@linroid
linroid merged commit 3d1cf0f into main Oct 24, 2026
11 checks passed
@linroid
linroid deleted the feat/remote-speed-modes branch October 24, 2026 07:30
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