Skip to content

perf(engine): keep furrow detection off the session build path - #1954

Merged
santoshkumarradha merged 12 commits into
devfrom
perf/boot-no-furrow-detect
Oct 26, 2026
Merged

santoshkumarradha merged 12 commits into
devfrom
perf/boot-no-furrow-detect

Conversation

@santoshkumarradha

@santoshkumarradha santoshkumarradha commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Summary

A cold launch held the first frame behind two furrow subprocesses: the engine hello builds the agent, the agent builds its belt, and the belt asked furrow on the spot.

 bootEngine
   enginehost.(*Host).open
     session.newAgent
       (*Agent).belt                          (built once, at construction)
-        (*Agent).workspaceTools
-          furrow.Tools -> Open -> Detect
-            `furrow --version`               (blocks the hello)
-            `furrow --repo ws --json status` (2-3 s, blocks the hello)
+        (*Agent).workspaceTools
+          furrow.PeekTools                   (memory only, never a subprocess)
+      (*Agent).askFurrow                     (returns at once)
+        furrowRefresher.ask -> guard.Go      (joined by Close)
+          furrow.Detect                      (cancellable, one asker per folder)
+          armFurrowVerbs -> armFamily        (appends the 4 tools at the tail)
 ...
 first model request
+  settleFurrow                               (waits <= 200 ms, once, only if a question is running)
   completeWithModel(WithTools(beltDefinitions))

Design it twice:

  • A, chosen: detect in the background and arm late. The belt is built once, so a verb that is not on it at construction has to arrive the way every late capability does, through armFamily, which appends at the tail. An unwatched folder, the common case, never changes.
  • B, rejected: a lazily-resolving tool set fixed at build. The four tools would always be on the belt and ask furrow at first call. That breaks the absence law (a model is told it can restore a folder that is not watched) and puts four descriptions in every prompt.

What the review changed from the first push:

  • Detect no longer takes a plain mutex per folder. Later askers wait on the first asker's question but on their own context, so a session closing (or a cancelled ground-path caller) leaves at once instead of waiting out another session's furrow.
  • A question cut by cancellation is not cached; a furrow that fails or times out on its own is, for the TTL, so a hanging binary costs two launches per 30 s however many belts are built.
  • The first push never armed the verbs on the belt the model reads (the belt is built once, not per turn), so a watched folder lost them. They are now appended through armFamily.
  • Anchoring an owned conversation to a project asks furrow about the new folder through the same refresher (never blocking the anchor), and arms the verbs when it answers.
  • Immediate-send doors (--once, resume with a pending prompt, spawned workers): the first model request waits up to 200 ms for a furrow question that is already running, and only the first request, and never while the session is built, so the first frame is unaffected. Past the cap the request goes out without the verbs and they are armed when furrow answers.

Prompt-cache cost. On a watched folder the four definitions are appended at the end of the tool list once. If the answer lands before the first request (usual, and guaranteed when the 200 ms wait catches it) nothing is invalidated. Otherwise the request after the answer pays one invalidation at the tail, and nothing ahead of it moves. Tools never leave the list mid-conversation, so the TTL cannot flip attached to unattached under the model: a folder unwatched later keeps the tools and they answer with furrow's refusal. An unwatched folder pays nothing.

Evidence

  • Before: first frame 3.87 s on a quiet box; interleaved rig runs on the same box and profile: 4.91, 4.90, 5.07 s. Red tests on the test-only commit (598691cc8, the pre-fix tree):

    --- FAIL: TestWatchedFolderVerbsReachTheModel        the belt never carried the workspace verbs; has []
    --- FAIL: TestClosingOneSessionDoesNotWaitBehindAnothersFurrow   furrow was cut ... (the close waited out furrow's 5 s bound)
    --- FAIL: TestAWaiterCanAbandonAnotherAskersQuestion             a cancelled caller kept waiting behind another caller's furrow
    
  • After (against the furrow embedded before 0.3.3): first frame 0.62, 0.44, 0.50 s (cold engine per workspace, owner profile, Spark linux-arm64). Green with -race. Nothing is timed: furrow is a script whose status parks on a named pipe, and the assertions are about whether it was still alive.

    TestBuildingASessionDoesNotWaitForFurrow              build returned while furrow's status process was alive
    TestWatchedFolderVerbsReachTheModel                   beltTools and beltDefinitions carry the 4 verbs after the answer
    TestAnchoringAsksFurrowAboutTheNewFolder              anchoring to a watched project arms its verbs
    TestFurrowIsAskedOncePerSession                       2 launches across many belts
    TestClosingJoinsAFurrowThatIsStillAnswering           furrow pid gone when Close returns
    TestClosingOneSessionDoesNotWaitBehindAnothersFurrow  B.Close returns with A's furrow still parked
    TestTheFirstRequestSettlesAQuestionAlreadyRunning     released by the answer, verbs present
    TestTheFirstRequestDoesNotWaitForAParkedFurrow        cap path: no verbs, furrow untouched
    TestAFailingOrHangingFurrowIsAskedOncePerTTL          2 launches for 8 questions (failing and hanging)
    TestAWaiterCanAbandonAnotherAskersQuestion            cancelled waiter returns; the other question still answers
    
  • With furrow 0.3.3 merged from dev (Bump embedded furrow to v0.3.3 (fast store open) #1962): origin/dev alone now draws the first frame in 0.97, 0.43, 0.43 s and this branch in 0.43, 0.38, 0.38 s. The fast furrow removed most of the wait this PR was written for; what remains is that first-frame time no longer depends on furrow's speed or on a folder furrow handles badly, and that a Close never waits on another session's furrow.

  • Full make check on Spark via fleet, job 20261026-131849-002475, on the merge of dev f0cf232cf (head 359167f64, later commit 2d0db4833 is docs only): internal/session (8 shards), cmd/codeaf, internal/furrow, internal/guard, internal/tui3 ok. internal/release and internal/seniordev/util fail identically on clean origin/dev. Whole-package -race on internal/session shows one race, in TestAHeldPhaseKeepsSayingItselfAndStopsWhenItEnds (a test write against beatHeldPhase, no furrow code), and wall-clock or shared-spend failures (TestSessionCountersCostNothingBesideTheWork, TestTheDailyCeilingBindsTaskAndErrandTurnsToo). TestAStoppedTaskResultCannotAutoStartItsRequestAgain fails under -race on clean origin/dev as well (16 of 20 runs): the test marks a node failed after admit has already let the frontier goroutine start it.

Merge Danger

Door: two-way

Blast Radius: session belt

On a watched folder the four workspace tools can join the tool list after the session starts (one tail append, see the prompt-cache note). No tool text, schema or name changes, and an unwatched folder behaves as before.

@santoshkumarradha
santoshkumarradha merged commit b30e0b9 into dev Oct 26, 2026
6 checks passed
@santoshkumarradha
santoshkumarradha deleted the perf/boot-no-furrow-detect branch October 26, 2026 13:44
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