Skip to content

Let an idle desktop app's saving helper exit - #2466

Merged
AbirAbbas merged 2 commits into
app/v3from
perf/furrow-idle-exit
Nov 6, 2026
Merged

AbirAbbas merged 2 commits into
app/v3from
perf/furrow-idle-exit

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The desktop engine sends a health request to the furrow serve helpers it started every 20 seconds for as long as it runs (internal/cellstore/daemon_app.go, added in #2226). Each request resets furrow's one-minute idle clock, so the helper never exits, even on an idle app. While it is alive, furrow wakes about 28 times a second: a 50 ms accept poll plus two 250 ms writer and group-commit waits per open store. On a Mac test app, furrow-0.3.6 serve ran at 28 wakeups/s after a turn ended and stayed alive indefinitely.

Fix

The 20-second touch now runs only while the app has saving work:

  • a chat turn is running, counted by SealWatch.TurnStarted and TurnEnded;
  • calls are held or a capture is running (the capture registry);
  • a seal is still waiting for its engine sync.

An idle app sends nothing. Its helper exits through furrow's own one-minute idle path and stops waking. Touches still only dial, so they never start or restart a helper. Terminal and command-line helper lifetimes are unchanged.

Why this is safe

A stopped helper costs only latency, not a save. This was already the documented contract (daemonstart.go: "the value costs latency, never correctness"):

  • Daemon.Do calls connect, which dials and, if nothing listens, calls startAndDial. That starts furrow serve and waits up to 5 s for it to listen.
  • If a request reaches a helper while it exits, the health check fails before anything is written. Fallback then retries seal and sync once through the daemon, which starts a new helper. Other verbs spawn the engine directly.
  • The WAL keeps every call until its seal holds.
  • If a helper dies while a save is pending, that save starts a new one when it runs.

Remaining cost: the first save after an idle period starts the helper again. Furrow's own maintenance (index check and abandoned-plan pruning) runs only in a helper that is at least an hour old, so in the app it now runs less often, as it already did for terminal helpers.

Measurements

  • Standalone cold start, from starting furrow-0.3.6 serve on a fresh socket to its first health reply: 4.0 ms median, 4.2 ms max over 10 runs (darwin/arm64).
  • After (app A/B on the same Mac, 2026-11-06): one run per arm. Each arm is the same desk app on a fresh home, a New agent tab, one turn that runs ls -la in the work folder, then 3 min sampled every 5 s.
arm furrow serve after the turn engine CPU % / wakeups/s furrow CPU % / wakeups/s
before (3a6aa31) still alive at +182 s 0.83 / 102 0.12 / 28
this PR, furrow alive (0–66 s) exits at +66.4 s 1.00 / 126 0.14 / 28
this PR, after furrow exits (67–180 s) (gone) 0.74 / 92 0 / 0
  • A second turn after furrow had exited completed in 7.1 s. A new furrow serve started 6.9 s after send for the end-of-turn save. The turn log has its seal entries at turn end and +2.8 s, the same as with a warm helper. No seal error, failure or warning lines in the home's logs.

Tests

  • New TestAppTouchesHelpersOnlyForSavingWork uses a stub helper on a test socket. It checks that an idle app sends no health requests and that a running turn, held calls, a running capture or a seal still waiting for its sync send exactly one. It also checks that a failed finished capture or an exited helper gets none. It passes 20 runs in a row, with and without -race.
  • The Linux-only real-furrow lifetime tests now start a turn where they expect the helper to be kept. New TestDesktopIdleAppLetsHelperExit checks that an idle app's helper exits by its idle limit and that the next request starts a new one.
  • go vet ./internal/cellstore (darwin, linux, windows), gofmt, the SealWatch tests, and go test -short ./internal/cellstore -count=1 pass on macOS.

The manual section "Desktop app quit and saving helpers" is updated to match.

🤖 Generated with Claude Code

AbirAbbas and others added 2 commits November 6, 2026 11:42
The desktop engine sent a health request to its furrow helpers every 20 s
for as long as it ran, which reset furrow's one-minute idle clock, so the
helper never exited and kept waking about 28 times a second on an idle app.

Touch the helpers only while there is saving work: a chat turn running
(SealWatch start/end), held calls or a running capture, or a seal still
owing its engine sync. An idle app sends nothing; the helper exits through
its own idle path and the next save starts it again through Daemon.Do.
Touches stay dial-only, so they never start or restart a helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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