Skip to content

mcp: batch-first apply_patch guidance - #3

Open
ActArtech wants to merge 1 commit into
fix/camera-framingfrom
docs/mcp-batch-first-guidance
Open

mcp: batch-first apply_patch guidance#3
ActArtech wants to merge 1 commit into
fix/camera-framingfrom
docs/mcp-batch-first-guidance

Conversation

@ActArtech

Copy link
Copy Markdown
Owner

What does this PR do?

Makes batch-first apply_patch usage the stated default across the MCP agent-facing guidance (idea P1 from content/ideas/mcp-speed-and-batching.md). The tool already validates every op before applying any (atomic, single undo step) — but nothing told agents to use that batch instead of looping single-op calls, which multiplies the O(N) snapshot/save pipeline per call.

Four guidance surfaces updated, no logic changes:

  • packages/mcp/src/tools/apply-patch.ts — tool description: batch-first is the default, do not loop one-op calls
  • packages/mcp/src/resources/agent-guide.ts — Scene Creation Rules bullet
  • packages/mcp/src/prompts/from-brief.ts — "One patch per phase" preamble
  • packages/mcp/README.md — apply_patch table row

How to test

  1. bunx tsc --noEmit -p packages/mcp (verified green on this branch)
  2. Run any MCP session; the apply_patch tool description and agent guide now state the batch-first rule

Screenshots / screen recording

N/A — guidance-text change only, no runtime behaviour change.

Checklist

  • I've tested this locally (typecheck green)
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets fix/camera-framing (stacked; will retarget once the base merges)

Tool description, agent guide, from-brief preamble, and README now instruct agents to compose one atomic apply_patch batch per phase instead of looping single-op calls. The tool already validates all ops before applying any; only the guidance was missing.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
ActArtech pushed a commit that referenced this pull request Sep 6, 2026
…Mesh containers (pascalorg#760)

* perf probe: drawComposition() — per-item/per-asset/per-kind draw census

Feeds charter backlog #3: projected draws for per-item merge vs
per-asset instancing, plus a meshes-by-kind bucket for the non-item
side of the budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* spike: BatchedMesh through the WebGPU pipeline (?spike=batch)

400-4,900 instances, two geometries, one material, shadows on. Verdict:
renders/shadows/post-FX clean; ~0.3us encode per instance (~10x cheaper
than a real mesh); per-instance frustum culling active (draw/tri track
the camera). WebGPU counts each multi-draw segment in info.render, so
the 3a metric is encode/frame-cpu ms, not draw calls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* Item draw batching v1: per-material BatchedMesh with wall-batch semantics (charter 3a)

Level-parented items render through one BatchedMesh per (level, material,
attribute-signature) — membership follows the dirty signal, lit items
release and draw themselves (outline/selection paint untouched), joins
wait for a quiet window, isolation stands the whole thing down, and
'thumbnail:before-capture' hands every item its meshes back so exports
never see a batch (belt: batches carry pascalExport='strip').

The container is incremental — instance add/delete per membership
change, geometry deduped per batch, capacity grown 2x on overflow. An
existing batch always accepts a rejoining item; only new batches need
MIN_BATCH_ENTRIES. v1 scope: items whose parent is a level; hosted
items (wall/ceiling/roof) move on host edits that never dirty the item,
so they keep drawing themselves. Interactive/animated/transparent items
excluded by the same rule as walls.

Rich 2x: 114 of 270 items batched (304 instances, 37 batches), idle
frame cpu 10.65 -> ~8.3ms. Full matrix + gates follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* item batch: prune batches orphaned by level-subtree remounts + probe surface

A React remount of a level subtree (thumbnail capture level shuffling,
tool-state swings) replaces the registry groups: imperatively-parented
batch meshes die with the old group while fresh source clones mount with
no layer hold. pruneDetached() spots the orphaned batches by parent
identity each frame and re-stales their items. ?perf probe gains
stats/wave/census/batchRender hooks for scripted verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* ?perf: draw row shows batched instance share

On WebGPU each batched instance still counts once in drawCalls (the
backend loops drawIndexed per visible instance), so a batched scene
looked no cheaper by the panel's DRAW number. The row now reads e.g.
'581 (272 batched · 2 mesh)' — live per sample, so per-instance frustum
culling is visible as the camera moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* ?perf: batch row shows membership truth, not per-pass culling

The first cut read _multiDrawCount, which snapshots whichever camera
culled the batch last — shadow vs main vs outline passes made the
number flip (206 -> 11 on hover) and read as items dropping out of the
batch. The batch system now publishes its membership (items/instances/
containers) to the panel store; the row is stable and only moves when
membership actually changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* Node batching 3b: doors, windows and columns join the batch (charter 3a+3b)

item-batch generalizes to shared/node-batch: same wall-batch semantics,
BatchedMesh containers per (level, material, attribute-signature), now
covering items, columns and wall-hosted openings (resolved to the host
wall's level). New release triggers: a dirty WALL cascades to its
openings (the wall edit moves them without marking them), a host wall
that is lit or mid-gesture releases its openings, and a door/window
whose animation record appears draws itself for the tween. Hitboxes
(material.visible=false) and glass (transparent) never batch. Bake
pages (?disable=draw) stand batching down entirely, and the release-
everything paths sweep every level subtree for stale 'batched' holds —
a system can rebuild a batched node's meshes and orphan the tracked
refs, and a stale hold is exactly what the GLB exporter would prune.

Rich 2x: 188 nodes / 1,108 instances batched, in-page A/B frame
9.1 -> 7.4ms (-19%); rich 4x: 2,280 instances, 19.3 -> 14.1ms (-27%).
Gates: e2e 8/8; bake tri+byte parity batching-on vs off; in-session
pixel diff 0.4% confined to seam AA and transparent blend order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* lint: sort node-batch imports/exports, format store

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* node batch: id-diff membership + per-batch geometry refcounts (Bugbot findings)

A same-size add-and-remove slipped past the count tell and left the
removed node's instances drawing as ghosts — membership now diffs ids
against last frame's registry. Released geometries drop their packed
mapping at refcount zero, so a rejoin re-packs current vertex content
(a rebuilt geometry can reuse its uuid) instead of the copy captured at
first join; the orphaned range stays until the existing overflow
rebuild reclaims it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* node batch: content-stamped geometry reuse + container culling off (Bugbot round 2)

The refcount fix traded one problem for another: dropping the packed
mapping at zero made every hover release/rejoin cycle re-pack the same
geometry, inflating used until premature overflow rebuilds. The mapping
now survives cycles and carries a content stamp (position version +
counts) — reuse is free, and a geometry rebuilt in place under the same
uuid re-packs instead of instancing stale vertices. Whole-container
frustumCulled goes off: its bounding sphere is computed at first cull,
so instances joining farther out later could vanish with the whole
batch — per-instance culling already owns visibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* node batch: hosted child nodes' subtrees stay out of the host's candidate walk (Bugbot round 3)

An item can host other items (a shelf's books) whose registered groups
mount inside the host's group — the walk packed those meshes as the
HOST's instances, freezing the child at join pose with no release of
its own. The walk now cuts at every hosted child's registered group;
hosted nodes keep drawing themselves (they are outside v1 batch scope
by the level-parent rule anyway).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* node batch: hidden-wall openings, autoplaying clips, late-copy gathering (Bugbot round 4)

- A hidden wall hides its openings through group visibility; batch
  instances hang off the level root and kept drawing them. Openings of
  an invisible wall are no longer candidates (the wall's visibility
  dirty-mark cascades the release).
- ItemAnimation autoplays a GLB's first clip even without an
  interactive effect; such items batched and froze mid-motion. The
  renderer stamps clip presence on the registry group and candidates
  exclude it.
- Below-threshold candidates were dropped after their wave, so copies
  placed more than a settle window apart never gathered into a batch.
  A new id now re-stales every unbatched node, letting the copy that
  crosses MIN_BATCH_ENTRIES pull earlier ones in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* node batch: partial members re-offer leftovers on new placements (Bugbot round 5) + import sort

store.has locked a partially-joined node (some meshes under the
new-batch threshold) out of every later wave. Partial members are now
tracked; a new placement releases them for a full re-collect, so the
copy that makes a leftover bucket viable pulls them in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* lint: format node-batch system

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

* node batch: overrides release + deferred-peer gathering (Bugbot round 6)

Live overrides now release an already-batched node (a collaborator's
remote drag or a programmatic move carries no local selection to tint
it) and defer, rather than drop, a stale one — the commit mark
re-offers it. Threshold-short candidates land in a leftover set that
re-offers as a group when a NEW leftover arrives, so a copy that was
deferred (selected, dirty, loading) while its peers' wave ran can still
gather them into a batch; a stable leftover set re-offers nothing and
small scenes stay quiet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK

---------

Co-authored-by: Claude Fable 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