Repository navigation
perf(core): speed up and harden snapshot capture - #51996
Merged
Merged
Conversation
- Scan the temporary index that the capture writes, never the shared one, so a concurrent process cannot roll entries back into a snapshot. - Sweep abandoned temporary indexes by the creation time in their name; a hard link keeps the store index's old mtime while in use. - Rebuild the index only when Git itself reports it unreadable, and install the source index atomically instead of deleting first. - Record a tracked file replaced by an embedded repository as a gitlink. - Drop exclude mirroring, which changed how source negations apply. - Keep step cancellation responsive while the start snapshot is captured. - Restore checkouts before removals so one failed removal cannot block the rest. - Rename internals: objects.pack (not compact), withTemporaryIndex, lastCaptures, attemptCapture, canBatchRestore, pendingSnapshot; fold the capture's seed into ignores.
…ntracked paths - Without exclude mirroring, a path ignored only by the source's info/exclude is listed as untracked on every scan, which defeated the clean-capture memo and cost check-ignore, update-index, and write-tree on every step. When the index is unchanged and nothing tracked changed, one check-ignore confirms every untracked path is ignored and returns the remembered tree; the result is reused on a miss. - Only tracked paths that became ignored need --force-remove; untracked paths are never in the index.
…he Windows command-line limit - Record an embedded repository's gitlink by repeating its path in the same update-index call instead of a second pass. - Read pack index object IDs in-process; packing runs a single pack-objects (show-index only for v1 or >2 GiB packs). - Restore runs one ls-tree per source tree: paths as arguments while they fit in 24,000 characters, otherwise the whole tree, so Windows' 32,767-character limit is never hit. - Diff passes --literal-pathspecs once instead of prefixing every path, keeping the command line as long as before.
Revert diffs every restored path, passing them as git diff arguments. Past Windows' 32,767-character command line the spawn failed, after the files were already restored. On Windows, a selection over 24,000 characters is now diffed in byte-sorted groups that share the patch cap, so output matches a single call. Other platforms keep a single call.
This was referenced Oct 8, 2026
3 of 6 tasks
2 of 6 tasks
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 change
Snapshots added 153 to 363 ms of Git work to every session step, could break for good after a crash, when two processes captured at once, or under some Git settings, and grew the store one loose file per object forever. This roughly halves the per-step cost, makes capture self-healing, and keeps memory flat and the store packed, without changing what a snapshot records.
Special things to note
v2across 32 edge-case scenarios under 7 Git configurations (script/snapshot-parity-run.sh). The only differences are cases wherev2failed or lost data: names starting with:(v2fails the capture and undo deletes the file), a globalcore.splitIndex=true(v2writes empty trees; this PR writes none), a corrupt index, a staleindex.lock, concurrent processes, and..scopedirectories.Memory and disk
Every step writes new objects into the store, and
v2never packed them, so stores grew one loose file per object (the largest real one had 23,924 loose objects, 198 MiB) and lookups slowed as they grew. Deleting old snapshots is a separate product call, so lossless packing is the only disk fix that keeps every snapshot. The memory work keeps the new design from costing anything that grows over a long session.update-indexon stdin and are filtered with sets, replacinggit addwith a path list (quadratic in paths) and anincludesscan over arrays.pack-objectsruns in the background with 2 threads and a 64 MiB window (about 67 MB peak on the largest real store), at most every 10 minutes, only past 2048 loose objects or 16 packs, and never blocks a capture.git gcwould treat every snapshot as garbage. Loose objects and old packs are removed only after the new pack index lists every one of them. On copies of real stores every object survived, and the largest went from 198 MiB to 88 MiB with 0 loose files left.API changes for direct
@opencode/coreimportersSchema, protocol, the server API, session events, and the generated client are unchanged, so SDK users see nothing. Code importing
@opencode/coremodules directly sees three changes:Git.Interfacegainsobjects.pack. Hand-writtenGit.Servicefakes need it; the one in this repo spreads the real service.createLLMEventPublisherinputsnapshot?: Snapshot.IDbecomespendingSnapshot?: Effect<Snapshot.ID | undefined>.Git.OperationError["operation"]gains"pack".Gittree functions now treat paths literally;v2read*,?,[and a leading:as pathspec magic, which is how undo deleted:colon.txt. Snapshot stores on disk stay readable by older releases.Change outline
Every write goes through a temporary index that is scanned, updated, and then renamed over the store's index, so processes never share
index.lockand a killed writer never leaves a broken index.Restore batches work when the paths are independent of each other, and otherwise falls back to the original per-path order.
The start snapshot overlaps the model request. It is still awaited before
Step.Started, so it always exists before any local tool runs, and the wait stays cancellable.Files and responsibilities:
packages/core/src/ ├── git.ts # temporary-index capture, clean-capture memo, index recovery, │ # atomic store creation, batched restore, objects.pack ├── snapshot.ts # ..scope fix, rate-limited background packing └── session/runner/ ├── step.ts # start capture overlaps the provider request └── publish-llm-event.ts # Step.Started awaits the pending snapshot packages/core/script/ +├── snapshot-parity*.ts / snapshot-parity-run.sh # differential corpus vs a baseline worktree +├── benchmark-snapshot*.ts # engine, session, and packing benchmarksMeasured against
origin/v2, back to back on the same machine with the same scripts (benchmark-snapshot.ts,benchmark-snapshot-session.ts):