Repository navigation
Conversation
SettleStep marks the context of the settle the turn loop takes after each tool batch, so a recording seat can choose its own saving cadence there while every other Settle caller keeps forcing its boundary. The marker rides the existing Settle path, so the seat wrappers need no new interface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every finished shell call reached a full-workspace capture at the end of its batch: the saving daemon stats every tracked file and all of .git, and the engine lists ignored files with ~20 git calls. Measured on an M4 Pro that is ~0.5 CPU-s in the daemon plus ~0.2 in codeaf app per command, a quarter of a core for an agent running a command every two seconds. A step settle now holds a batch with unknown effects in the WAL until the oldest unsaved call is 30 s old. Turn end, close, exit and every other forced settle still capture everything pending. Boundaries that read the head (take-over checks, rewind, process exit) force held calls into a capture between steps and never wait on an open step's tool. The WAL already makes each finished call durable, so a death before the save loses nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ause Publishing a local chat, reacquiring an expired hold and choosing the head to keep on displacement now settle held calls first, so another computer never gets a head that misses finished work. The ordinary per-command ownership check only joins a running capture. After the hold is lost, finished calls may still seal locally so displacement keeps them in the branch; new tools and bookkeeping-only seals stay refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Displaced returned at once when the hold was already stale, but a background heartbeat or follower sets stale before it finishes saving and branching the kept work. CheckLease could then return before the branch map was written. Displaced now joins lost under flushMu, whose own checks still prevent a second resolution, so the caller only returns once the kept branch exists. The window grew once displacement settles held calls before branching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
force-pushed
the
perf/save-at-pauses
branch
from
November 4, 2026 17:18
9cac8c2 to
0868e9a
Compare
…s quiet A setup turn's calls run deferred like any other turn-loop call, so the step settle now forces their capture instead of applying the save interval. A viewer's store refuses seals; that refusal no longer becomes the recorder's error, so take-back reaches its own WAL recovery as on app/v3. Drain leaves the held registry when the recorder closes, Unsaved reports how many calls a held recorder still owes, and take-over checks keep reporting completed WAL calls as local work when their capture fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If the save before branching fails, displacement no longer stays pending with nothing to retry it. It branches the turns already noted, and the unsaved calls stay completed in the local WAL, where take-over checks and take-back already keep them, as they do for an open step's calls. The notice then names only the turns it kept rather than saying every unsaved edit was kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record The save-at-pauses section said "call log" five times, which pulled the manual's "turn on logging" search toward sessions-and-rewind. No question the manual is tested on uses "ledger". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The desktop app spent a lot of CPU while agents worked. In the app, every finished tool batch ended in a full-workspace capture. A shell call's changed paths are unknown, so each capture made the saving daemon (
furrow-<ver> serve) stat every tracked file and all of.git.codeaf appalso listed ignored files with about 20gitcalls per capture.Measured on an M4 Pro (desk main + this engine, Ollama, side-by-side app with a throwaway home):
The UI's rewind only reaches the start of the person's turns, and it leaves files as they are. File seals serve moving, taking over and recovering a chat. The WAL already makes every finished call durable before any capture.
What changes
executor.SettleStepmarks the turn loop's per-batch settle (internal/session/loop.go). Every otherSettlecaller still forces a capture.saveInterval). Exact-path-only and transcript-only steps capture as before. Turn end, close, exit (Flush) and other forced settles capture everything pending.Captured/Awaitand their callers: take-over checks, rewind, upload, reacquire, displacement) force held calls into a capture between steps only. During an open step they keep today's behaviour and never wait on a running tool. A seal taken mid-step would carry a call's work without its result line. The per-command ownership check (CheckLease) only joins a running capture (WaitCapture).Batcher.BeforeResolve).stamping.Sealnow lets finished calls seal locally after the hold is lost, so displacement keeps them in the branch. New tools and bookkeeping-only seals stay refused.Batcher.Displacednow always joinslost(), soCheckLeasewaits for a branch a background heartbeat or follower is still writing. That race was latent; the longer pre-branch save exposed it.Tests
New contract tests:
TestSavePauses*in cellstore, cellsync, syncsetup, executor and session. They use an injected clock and a counting fake store. The contract:Status:
go build ./...andgo vetclean, and every focused group passes:TestSavePauses*in all 5 packages (syncsetup 3/3 runs), plus the cellstore, syncsetup and session rewind/displacement sets.make teston the 5 packages there: 10 failures. All 10 fail the same way on app/v3 59cdf22: the macOS SIGPIPE pipe-stage tests, the bash belt truncation tests, theTestNoRoadIn*complexity tests, programhold, the prefix budgets, and a known syncsetup R9 flake. None are new.TestNoRoadIn*on files this branch does not touch; app/v3's own gate fails the same tests.git range-diffshows only a context-line change in the syncsetup commit. On macOS at 0868e9a: build and vet are clean, every focused group passes (syncsetupTestSavePauses3/3), and Moving a chat says how far it has got, stops when stalled, and can be cancelled #2348's ownMove|Take|Stalltests pass (1 skip: a hosted-only proof). CI is re-running on 0868e9a.Open points
codeaf appburst was seen once, at a turn-end settle of 11 held calls in a large checkout. It did not recur in three later runs (two repeats with an unstripped build and sampling, plus a 66 s turn with a mid-turn save). Treated as a one-off..git(immutable packs included). Fewer captures make that rarer, but each one still costs the same.🤖 Generated with Claude Code