You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Private research: agent-aware recompute batching (do not flip /v1/run default) #5
Agent /v1/run defaults to recompute=True after every script. Holding rebuilds across independent property edits is several× faster in-process, but FreeCAD already has SkipRecompute / freeze knobs — the open question is when an LLM agent path can use them safely.
What we measured (2026-09-18, live window, no checkout change)
Fixtures under %LOCALAPPDATA%\Temp\vibecad-recompute-bench\ (EngineBlock from tree examples; ShawnHymel public FCStds in public-cad\).
VibeCADAgentControl.run_script: after exec, if recompute then ActiveDocument.recompute(). With recompute:false, nothing after exec.
What the instrumented HTTP burst showed
Step
host recompute
http ms
pad
Body vol
Pad state
RecomputePending
CooperativeMutationActive
reset 2 mm
true
~1314
2.0
3625.9
Up-to-date
false
false
defer bump ×5
false
~7–63
2.25…3.25
3625.9
Touched
false
false
defer bump 6–7
false
~23
3.5…3.75
3625.9
Touched
true
true
final bump
true
8.3
4.0
3625.9 (stale)
Touched
true
true
measure-only
true
15.5
4.0
3625.9 (stale)
Touched
true
true
touch() + in-script recompute()
false
17
4.0
3625.9
Touched
true
true (rc=0 in 0.004 ms)
Smoking gun: once CooperativeMutationActive / RecomputePending latch mid-burst, later document.recompute()early-returns (~0–15 ms instead of ~1.2 s) and Body stays at baseline even though Pad length is already 4.0.
Recovery / control
~2 s later (poll): gates cleared; Body volume became 4844.6 without another Length edit (GUI follow-up drained).
Cross-call recompute:false is unsafe for Part Design not because Length fails to touch, but because VibeCAD’s GUI Document::recompute can defer under cooperative-mutation / pending-recompute gates; the next /v1/run with recompute:true does not wait for that rebuild. In-process batching on a quiet doc still works.
Fix direction (still private — no default flip)
After host recompute:true, wait until not RecomputePending and cooperative/presentation gates clear; re-issue if Pad/Body still Touched.
Serialize agent /v1/run with GUI recompute follow-ups so follow-up drains before the next call.
Do not treat script-side Shape reads as post-recompute unless that wait is guaranteed.
Still not for 10-X-eng until proposed behavior + approved wording.
Sketch: wait/drain after host recompute:true (fork-only, no code yet)
Goal
When /v1/run asks for recompute:true, the HTTP response must not return until Feature rebuild has actually finished (or we hard-fail with a clear error). Today the host calls ActiveDocument.recompute(), but GUI-thread Document::recompute can return 0 and queue a follow-up while CooperativeMutationActive / mutation-blocking presentation / coordinator is active — so Pad can already show the new Length while Body.Shape is still baseline.
Where
VibeCADAgentControl.run_script (after exec, only when recompute is true). Still marshalled via on_document_thread (already on the document/GUI owner thread).
Why a plain sleep is wrong
Follow-ups are queued on the GUI thread (scheduleGuiRecomputeFollowUp). We are already on that thread for /v1/run, so we must pump Qt events (same idea as the QEventLoop inside Document::recompute) until the follow-up drains — not sleep on the HTTP worker.
Proposed helper (pseudocode)
def_drain_recompute(document, *, timeout_s: float=30.0) ->dict:
"""Call recompute and wait until rebuild finished or timeout. Returns {"ok": bool, "recompute_calls": int, "ms": float, "error": str|None}. """deadline=time.monotonic() +timeout_scalls=0defgates_block() ->bool:
# Prefer the same predicates Document::recompute uses.coop=bool(getattr(document, "CooperativeMutationActive", False))
# Python may expose PresentationUpdateActive; C++ early-return also# checks mutation-blocking presentation depth + guiRecomputeCoordinatorActive.pres=bool(getattr(document, "PresentationUpdateActive", False))
pending=bool(getattr(document, "RecomputePending", False))
returncooporpending# + presentation/coordinator if exposeddefany_touched() ->bool:
returnany(
"Touched"inlist(getattr(obj, "State", []) or [])
forobjindocument.Objects
)
# Initial request (mirrors today’s host call).document.recompute()
calls+=1gui=FreeCADGui# if availablewhiletime.monotonic() <deadline:
# Pump so scheduleGuiRecomputeFollowUp can run.ifguiisnotNone:
try:
gui.updateGui() # or a short QEventLoop.processEventsexceptException:
passifgates_block():
continueifany_touched():
document.recompute()
calls+=1continuereturn {"ok": True, "recompute_calls": calls, "ms": ..., "error": None}
return {
"ok": False,
"recompute_calls": calls,
"ms": ...,
"error": "RECOMPUTE_DRAIN_TIMEOUT",
}
final ~553 ms, drain ~532, tip Cut003 Up-to-date (not ~8 ms stale)
In-process hold→recompute
volumes match eager/batch, ~9.0× (eager ~3563 ms vs batch ~395 ms)
Combined with fem_bracket
Drain holds across Part Design (propeller, fem_bracket) and Part boolean chain (EngineBlock). In-process batching speedup still ~8–9×. Default still recompute=True.
Hard matrix after drain (cursor/recompute-drain-2105 @ 0ef0140d)
Live HTTP on DESKTOP-ELN7SGD. Drain is present (recompute_drain_ms ~0.9–1.4s on finals). Default remains recompute=true.
Suite results
Suite
Result
propeller false×7 + true ×8
7/8 exact (~3925.23). t1 miss: ~3881 (not the old ~8 ms / permanently stale path; drain still ~900 ms)
fem_bracket ×8
8/8 under volume gate; one early trial showed intermediate ~4692 then settled to 4844.6
EngineBlock r=27.5 ×8
8/8
propeller back-to-back ×4
all passed loose gate; one soft ~3837 then 3925
measure-only (recompute:true noop after false burst)
works: pre Shape stale, drain ~1.2s, post measure correct
switch propeller→fem
pass
First-after-open dig (propeller PolarPattern)
Drain clears CooperativeMutationActive / RecomputePending and objects report Up-to-date, but Body/Tip volume can still be wrong on the first false-burst+drain after openDocument:
Seen wrong volumes: ~3837, ~3881, ~3793 (Pad Length already 8.0)
Second burst on the same open doc usually lands exact
Idle second recompute:true after a wrong result does not reliably fix (gates already clear; nothing left to drain)
So: drain fixed the catastrophic “~8 ms early-return + stale Shape until GUI poll” failure. Remaining flake looks like PartDesign PolarPattern / first rebuild after open reporting Up-to-date with an incomplete solid, not the latch the drain targets.
Still not for 10-X-eng
Do not file upstream until:
first-after-open propeller is understood or mitigated, and
Dev window on DESKTOP-ELN7SGD: VibeCAD DEV · f17f5152. AgentControl copied into pixi Mod; relaunch -SkipRebuild.
Propeller first-after-open (the flake)
Suite
Before harden
After harden
reopen ×8
6/8 (wrongs ~3837–3903, Up-to-date)
8/8 exact 3925.23
seek wrong for probes
easy
0/12 additional opens — no wrong to probe
drain_ms on final
~0.9–1.0s
~3.2–3.3s (forced dirty rebuild)
same-doc second burst
usually ok
ok
Pre-harden recovery (for the record): noop / double recompute did not fix; same-Length reassign or touch() + recompute did. Matches the forced rebuild in this commit.
Other matrix
Suite
Result
fem_bracket false×7+true ×8
8/8 vol 4844.56, drain ~1.3s
EngineBlock Cylinder.Radius edit ×8
8/8 Up-to-date (drain ~0.45s; no PartDesign tip path)
/v1/run with recompute: true waits out GUI follow-up and, for Part Design Body/pattern tips, dirties Tip/Pad/originals and rebuilds until Shape volume is stable. Default stays recompute: true. Live matrix on Windows 10 DEV stayed green (propeller first-after-open, fem_bracket, measure-only, EngineBlock cylinder radius, recompute: false still fast).
Still open on this private tracker (performance track)
Do not flip /v1/run default to recompute: false.
Safe speedup so far remains: batch edits inside one script, one doc.recompute() at end.
Decide later: burst API vs document sticky freeze vs docs-only guidance for agents.
HTTP recompute: false across calls is still the wrong default for Part Design; the drain makes the true path trustworthy so batching-inside-one-script stays the story.
Fork PR #6 is the same branch lineage and is being closed as superseded by 10-X-eng 10-X-eng#246 (not merged into AppSprout main as a separate landing).
Activity
jkbennitt commentedon Sep 18, 2026
Live RCA (2026-09-18, DESKTOP-ELN7SGD)
Fixture: ShawnHymel
fem_bracket.FCStd. Edit:Pad.Length2.0→4.0 mm (App.Units.Quantity), measureBody.Shape.Volume(tipMirrored).Log:
%LOCALAPPDATA%\Temp\vibecad-recompute-bench\http-flake-instrumentation.jsonlHost code
VibeCADAgentControl.run_script: afterexec, ifrecomputethenActiveDocument.recompute(). Withrecompute:false, nothing after exec.What the instrumented HTTP burst showed
touch()+ in-scriptrecompute()rc=0in 0.004 ms)Smoking gun: once
CooperativeMutationActive/RecomputePendinglatch mid-burst, laterdocument.recompute()early-returns (~0–15 ms instead of ~1.2 s) and Body stays at baseline even though Pad length is already 4.0.Recovery / control
doc.recompute():rc=13, ~1289 ms, volume 3625.9→4844.6,coop=false.Working conclusion
Cross-call
recompute:falseis unsafe for Part Design not because Length fails to touch, but because VibeCAD’s GUIDocument::recomputecan defer under cooperative-mutation / pending-recompute gates; the next/v1/runwithrecompute:truedoes not wait for that rebuild. In-process batching on a quiet doc still works.Fix direction (still private — no default flip)
recompute:true, wait untilnot RecomputePendingand cooperative/presentation gates clear; re-issue if Pad/Body still Touched./v1/runwith GUI recompute follow-ups so follow-up drains before the next call.Still not for 10-X-eng until proposed behavior + approved wording.
jkbennitt commentedon Sep 18, 2026
Sketch: wait/drain after host
recompute:true(fork-only, no code yet)Goal
When
/v1/runasks forrecompute:true, the HTTP response must not return until Feature rebuild has actually finished (or we hard-fail with a clear error). Today the host callsActiveDocument.recompute(), but GUI-threadDocument::recomputecan return 0 and queue a follow-up whileCooperativeMutationActive/ mutation-blocking presentation / coordinator is active — so Pad can already show the new Length whileBody.Shapeis still baseline.Where
VibeCADAgentControl.run_script(afterexec, only whenrecomputeis true). Still marshalled viaon_document_thread(already on the document/GUI owner thread).Why a plain
sleepis wrongFollow-ups are queued on the GUI thread (
scheduleGuiRecomputeFollowUp). We are already on that thread for/v1/run, so we must pump Qt events (same idea as theQEventLoopinsideDocument::recompute) until the follow-up drains — not sleep on the HTTP worker.Proposed helper (pseudocode)
Wire-in:
Optional response fields (debug):
recompute_drain_ms,recompute_calls.Acceptance (private, against fem_bracket)
Same HTTP burst as #5 RCA comment:
Pad.Lengthto 2.0 mm,recompute:true→ Body ~3625.9+0.25withrecompute:false+0.25withrecompute:true(pad → 4.0)Non-goals
recomputeto falserecompute:false“safe” for Shape reads across callsAgent guidance (docs only, after fix)
/v1/runscript, one recompute at the end (already correct today on a quiet doc).recompute:falseremains an advanced knob: do not readShape/volume until a laterrecompute:truecall that has drained.Open questions before coding
recompute:truewith no edits should also drain (yes — RCA needed it).Ready to implement on AppSprout-dev only when you say go; still no upstream PR.
jkbennitt commentedon Sep 18, 2026
Live retest on DESKTOP-ELN7SGD (drain branch)
Checkout:
cursor/recompute-drain-2105@0ef0140dInstalled Mod copy updated; relaunched with
-SkipRebuild.Same fem_bracket HTTP burst as the RCA (Pad 2→4 mm, seven
recompute:false, onerecompute:true):/v1/runhttp msrecompute_drain_ms~1204, calls=1)Drain fields present on success. Default still
recompute:true. Still not for 10-X-eng until wording approved.jkbennitt commentedon Sep 18, 2026
More live benches (drain branch
0ef0140d)Same machine,
-SkipRebuildinstall.Propeller (ShawnHymel) — Pad001 6→8 mm
EngineBlock — Cylinder+Cylinder001 Radius 25→27.5 (+0.25×10)
Combined with fem_bracket
Drain holds across Part Design (propeller, fem_bracket) and Part boolean chain (EngineBlock). In-process batching speedup still ~8–9×. Default still
recompute=True.Log:
%LOCALAPPDATA%\Temp\vibecad-recompute-bench\live-benches-after-drain.jsonjkbennitt commentedon Sep 18, 2026
Hard matrix after drain (
cursor/recompute-drain-2105@0ef0140d)Live HTTP on DESKTOP-ELN7SGD. Drain is present (
recompute_drain_ms~0.9–1.4s on finals). Default remainsrecompute=true.Suite results
recompute:truenoop after false burst)First-after-open dig (propeller PolarPattern)
Drain clears
CooperativeMutationActive/RecomputePendingand objects report Up-to-date, but Body/Tip volume can still be wrong on the first false-burst+drain afteropenDocument:recompute:trueafter a wrong result does not reliably fix (gates already clear; nothing left to drain)So: drain fixed the catastrophic “~8 ms early-return + stale Shape until GUI poll” failure. Remaining flake looks like PartDesign PolarPattern / first rebuild after open reporting Up-to-date with an incomplete solid, not the latch the drain targets.
Still not for 10-X-eng
Do not file upstream until:
Artifacts:
%LOCALAPPDATA%\Temp\vibecad-recompute-bench\hard-matrix-dig.jsonjkbennitt commentedon Sep 18, 2026
Live retest after harden (
f17f5152)Dev window on DESKTOP-ELN7SGD:
VibeCAD DEV · f17f5152. AgentControl copied into pixi Mod; relaunch-SkipRebuild.Propeller first-after-open (the flake)
Pre-harden recovery (for the record): noop / double
recomputedid not fix; same-Length reassign ortouch()+ recompute did. Matches the forced rebuild in this commit.Other matrix
Artifacts:
%LOCALAPPDATA%\Temp\vibecad-recompute-bench\polar-first-open-dig.json,hard-matrix-after-harden.json.Still not filing 10-X-eng until Jason wants wording. Cost note: patterned Body docs pay ~+2s on
recompute:truevs gate-only drain.jkbennitt commentedon Sep 18, 2026
Retest
ce89f450(touch-then-rebuild drain)Pulled over
f17f5152, pixi Mod copy,-SkipRebuild. Window:ce89f450.recompute_calls=2Still private-fork only; no 10-X-eng issue yet.
jkbennitt commentedon Sep 18, 2026
Status (2026-09-18)
The honesty half of this research is upstream now:
/v1/runwithrecompute: truewaits out GUI follow-up and, for Part Design Body/pattern tips, dirties Tip/Pad/originals and rebuilds until Shape volume is stable. Default staysrecompute: true. Live matrix on Windows 10 DEV stayed green (propeller first-after-open, fem_bracket, measure-only, EngineBlock cylinder radius,recompute: falsestill fast).Still open on this private tracker (performance track)
/v1/rundefault torecompute: false.doc.recompute()at end.recompute: falseacross calls is still the wrong default for Part Design; the drain makes the true path trustworthy so batching-inside-one-script stays the story.Fork PR #6 is the same branch lineage and is being closed as superseded by 10-X-eng 10-X-eng#246 (not merged into AppSprout
mainas a separate landing).