From f0fb09f94789188bc59de4de0ba3bb2e9ab39b8e Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:10:59 -0400 Subject: [PATCH 01/11] fix(opencode): batch snapshot revert checkout --- packages/opencode/src/snapshot/index.ts | 83 +++++++++++++++++++++---- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index d6bdf8a3c1d4..433af55d203a 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -300,27 +300,86 @@ export namespace Snapshot { const revert = Effect.fnUntraced(function* (patches: Snapshot.Patch[]) { return yield* locked( Effect.gen(function* () { + const byHash = new Map() const seen = new Set() for (const item of patches) { + const list = byHash.get(item.hash) ?? [] + byHash.set(item.hash, list) for (const file of item.files) { if (seen.has(file)) continue seen.add(file) - log.info("reverting", { file, hash: item.hash }) - const result = yield* git([...core, ...args(["checkout", item.hash, "--", file])], { - cwd: state.worktree, - }) - if (result.code !== 0) { - const rel = path.relative(state.worktree, file) - const tree = yield* git([...core, ...args(["ls-tree", item.hash, "--", rel])], { + list.push(file) + } + } + + const one = Effect.fnUntraced(function* (hash: string, file: string) { + log.info("reverting", { file, hash }) + const result = yield* git([...core, ...args(["checkout", hash, "--", file])], { + cwd: state.worktree, + }) + if (result.code === 0) return + const rel = path.relative(state.worktree, file).replaceAll("\\", "/") + const tree = yield* git([...core, ...args(["ls-tree", hash, "--", rel])], { + cwd: state.worktree, + }) + if (tree.code === 0 && tree.text.trim()) { + log.info("file existed in snapshot but checkout failed, keeping", { file, hash }) + return + } + log.info("file did not exist in snapshot, deleting", { file, hash }) + yield* remove(file) + }) + + for (const [hash, list] of byHash) { + for (let i = 0; i < list.length; i += 100) { + const chunk = list.slice(i, i + 100) + if (chunk.length === 1) { + yield* one(hash, chunk[0]!) + continue + } + + const rels = chunk.map((file) => [path.relative(state.worktree, file).replaceAll("\\", "/"), file] as const) + const tree = yield* git( + [...core, ...args(["ls-tree", "--name-only", hash, "--", ...rels.map(([rel]) => rel)])], + { + cwd: state.worktree, + }, + ) + + if (tree.code !== 0) { + for (const file of chunk) { + yield* one(hash, file) + } + continue + } + + const existing = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean)) + const restore: string[] = [] + const absent: string[] = [] + for (const [rel, file] of rels) { + if (existing.has(rel)) { + restore.push(file) + continue + } + absent.push(file) + } + + if (restore.length) { + log.info("reverting", { hash, files: restore.length }) + const result = yield* git([...core, ...args(["checkout", hash, "--", ...restore])], { cwd: state.worktree, }) - if (tree.code === 0 && tree.text.trim()) { - log.info("file existed in snapshot but checkout failed, keeping", { file }) - } else { - log.info("file did not exist in snapshot, deleting", { file }) - yield* remove(file) + if (result.code !== 0) { + for (const file of restore) { + yield* one(hash, file) + } } } + + for (const file of absent) { + log.info("file did not exist in snapshot, deleting", { file, hash }) + yield* remove(file) + } } } }), From 82b08439ce7fc29bce77d3a4a49c0f4b9273d928 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:18:50 -0400 Subject: [PATCH 02/11] refactor(opencode): clarify snapshot revert naming --- packages/opencode/src/snapshot/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 433af55d203a..8d45166ef459 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -300,15 +300,15 @@ export namespace Snapshot { const revert = Effect.fnUntraced(function* (patches: Snapshot.Patch[]) { return yield* locked( Effect.gen(function* () { - const byHash = new Map() + const groupByHash = new Map() const seen = new Set() for (const item of patches) { - const list = byHash.get(item.hash) ?? [] - byHash.set(item.hash, list) + const filesForHash = groupByHash.get(item.hash) ?? [] + groupByHash.set(item.hash, filesForHash) for (const file of item.files) { if (seen.has(file)) continue seen.add(file) - list.push(file) + filesForHash.push(file) } } @@ -330,7 +330,7 @@ export namespace Snapshot { yield* remove(file) }) - for (const [hash, list] of byHash) { + for (const [hash, list] of groupByHash) { for (let i = 0; i < list.length; i += 100) { const chunk = list.slice(i, i + 100) if (chunk.length === 1) { From 042462c7d6780b2855dd3867b4d9ffab56eefc8a Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:24:30 -0400 Subject: [PATCH 03/11] refactor(opencode): clarify single-file revert path --- packages/opencode/src/snapshot/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 8d45166ef459..269f97d4cf1e 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -312,7 +312,7 @@ export namespace Snapshot { } } - const one = Effect.fnUntraced(function* (hash: string, file: string) { + const revertSingle = Effect.fnUntraced(function* (hash: string, file: string) { log.info("reverting", { file, hash }) const result = yield* git([...core, ...args(["checkout", hash, "--", file])], { cwd: state.worktree, @@ -334,7 +334,7 @@ export namespace Snapshot { for (let i = 0; i < list.length; i += 100) { const chunk = list.slice(i, i + 100) if (chunk.length === 1) { - yield* one(hash, chunk[0]!) + yield* revertSingle(hash, chunk[0]!) continue } @@ -348,7 +348,7 @@ export namespace Snapshot { if (tree.code !== 0) { for (const file of chunk) { - yield* one(hash, file) + yield* revertSingle(hash, file) } continue } @@ -371,7 +371,7 @@ export namespace Snapshot { }) if (result.code !== 0) { for (const file of restore) { - yield* one(hash, file) + yield* revertSingle(hash, file) } } } From 3bd9964e7fe47b22a6c1ccfec164e18653211496 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:26:09 -0400 Subject: [PATCH 04/11] refactor(opencode): clarify grouped file naming --- packages/opencode/src/snapshot/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 269f97d4cf1e..537c8782f3df 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -330,9 +330,9 @@ export namespace Snapshot { yield* remove(file) }) - for (const [hash, list] of groupByHash) { - for (let i = 0; i < list.length; i += 100) { - const chunk = list.slice(i, i + 100) + for (const [hash, filesByHash] of groupByHash) { + for (let i = 0; i < filesByHash.length; i += 100) { + const chunk = filesByHash.slice(i, i + 100) if (chunk.length === 1) { yield* revertSingle(hash, chunk[0]!) continue From a426048290c00751b3256fa463f21060737166a3 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:30:09 -0400 Subject: [PATCH 05/11] refactor(opencode): clarify file chunk naming --- packages/opencode/src/snapshot/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 537c8782f3df..952322d48d73 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -332,13 +332,15 @@ export namespace Snapshot { for (const [hash, filesByHash] of groupByHash) { for (let i = 0; i < filesByHash.length; i += 100) { - const chunk = filesByHash.slice(i, i + 100) - if (chunk.length === 1) { - yield* revertSingle(hash, chunk[0]!) + const fileChunk = filesByHash.slice(i, i + 100) + if (fileChunk.length === 1) { + yield* revertSingle(hash, fileChunk[0]!) continue } - const rels = chunk.map((file) => [path.relative(state.worktree, file).replaceAll("\\", "/"), file] as const) + const rels = fileChunk.map( + (file) => [path.relative(state.worktree, file).replaceAll("\\", "/"), file] as const, + ) const tree = yield* git( [...core, ...args(["ls-tree", "--name-only", hash, "--", ...rels.map(([rel]) => rel)])], { @@ -347,7 +349,7 @@ export namespace Snapshot { ) if (tree.code !== 0) { - for (const file of chunk) { + for (const file of fileChunk) { yield* revertSingle(hash, file) } continue From 37028d6406ffbfc57700e8b0b07b99f0c64150ca Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:37:58 -0400 Subject: [PATCH 06/11] refactor(opencode): clarify checkout file naming --- packages/opencode/src/snapshot/index.ts | 33 +++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 952322d48d73..41cd8ca71499 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -338,11 +338,12 @@ export namespace Snapshot { continue } - const rels = fileChunk.map( - (file) => [path.relative(state.worktree, file).replaceAll("\\", "/"), file] as const, - ) + const paths = fileChunk.map((file) => ({ + rel: path.relative(state.worktree, file).replaceAll("\\", "/"), + file, + })) const tree = yield* git( - [...core, ...args(["ls-tree", "--name-only", hash, "--", ...rels.map(([rel]) => rel)])], + [...core, ...args(["ls-tree", "--name-only", hash, "--", ...paths.map((item) => item.rel)])], { cwd: state.worktree, }, @@ -355,30 +356,30 @@ export namespace Snapshot { continue } - const existing = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean)) - const restore: string[] = [] - const absent: string[] = [] - for (const [rel, file] of rels) { - if (existing.has(rel)) { - restore.push(file) + const snapshotPaths = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean)) + const filesToCheckout: string[] = [] + const missingFiles: string[] = [] + for (const item of paths) { + if (snapshotPaths.has(item.rel)) { + filesToCheckout.push(item.file) continue } - absent.push(file) + missingFiles.push(item.file) } - if (restore.length) { - log.info("reverting", { hash, files: restore.length }) - const result = yield* git([...core, ...args(["checkout", hash, "--", ...restore])], { + if (filesToCheckout.length) { + log.info("reverting", { hash, files: filesToCheckout.length }) + const result = yield* git([...core, ...args(["checkout", hash, "--", ...filesToCheckout])], { cwd: state.worktree, }) if (result.code !== 0) { - for (const file of restore) { + for (const file of filesToCheckout) { yield* revertSingle(hash, file) } } } - for (const file of absent) { + for (const file of missingFiles) { log.info("file did not exist in snapshot, deleting", { file, hash }) yield* remove(file) } From 4cc2a391eeff82d89ce46799141acb9f3c259636 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:39:31 -0400 Subject: [PATCH 07/11] refactor(opencode): clarify chunk path naming --- packages/opencode/src/snapshot/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 41cd8ca71499..750b148b5b31 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -338,12 +338,12 @@ export namespace Snapshot { continue } - const paths = fileChunk.map((file) => ({ + const chunkPaths = fileChunk.map((file) => ({ rel: path.relative(state.worktree, file).replaceAll("\\", "/"), file, })) const tree = yield* git( - [...core, ...args(["ls-tree", "--name-only", hash, "--", ...paths.map((item) => item.rel)])], + [...core, ...args(["ls-tree", "--name-only", hash, "--", ...chunkPaths.map((item) => item.rel)])], { cwd: state.worktree, }, @@ -359,7 +359,7 @@ export namespace Snapshot { const snapshotPaths = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean)) const filesToCheckout: string[] = [] const missingFiles: string[] = [] - for (const item of paths) { + for (const item of chunkPaths) { if (snapshotPaths.has(item.rel)) { filesToCheckout.push(item.file) continue From 86872401fb919a2a85290048f9bd89fb935f9cbc Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 14:55:38 -0400 Subject: [PATCH 08/11] refactor(opencode): clarify file chunk path naming --- packages/opencode/src/snapshot/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 750b148b5b31..995a3664372e 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -331,19 +331,19 @@ export namespace Snapshot { }) for (const [hash, filesByHash] of groupByHash) { - for (let i = 0; i < filesByHash.length; i += 100) { + for (let i = 0; i < filesByHash.length; i += 100) { //run git commands in 100 file chunks to prevent ARG_MAX errors const fileChunk = filesByHash.slice(i, i + 100) if (fileChunk.length === 1) { yield* revertSingle(hash, fileChunk[0]!) continue } - const chunkPaths = fileChunk.map((file) => ({ + const fileChunkPaths = fileChunk.map((file) => ({ rel: path.relative(state.worktree, file).replaceAll("\\", "/"), file, })) const tree = yield* git( - [...core, ...args(["ls-tree", "--name-only", hash, "--", ...chunkPaths.map((item) => item.rel)])], + [...core, ...args(["ls-tree", "--name-only", hash, "--", ...fileChunkPaths.map((item) => item.rel)])], { cwd: state.worktree, }, @@ -359,7 +359,7 @@ export namespace Snapshot { const snapshotPaths = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean)) const filesToCheckout: string[] = [] const missingFiles: string[] = [] - for (const item of chunkPaths) { + for (const item of fileChunkPaths) { if (snapshotPaths.has(item.rel)) { filesToCheckout.push(item.file) continue From 921c809ec34be4b44042bad9c0be10fa60fcc4af Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 15:07:02 -0400 Subject: [PATCH 09/11] refactor(opencode): polish snapshot batching flow --- packages/opencode/src/snapshot/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 995a3664372e..0505ff50258c 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -339,7 +339,7 @@ export namespace Snapshot { } const fileChunkPaths = fileChunk.map((file) => ({ - rel: path.relative(state.worktree, file).replaceAll("\\", "/"), + rel: path.relative(state.worktree, file).replaceAll("\\", "/"), file, })) const tree = yield* git( @@ -350,6 +350,7 @@ export namespace Snapshot { ) if (tree.code !== 0) { + log.info("batched ls-tree failed, falling back to single-file revert", { hash, files: fileChunk.length }) for (const file of fileChunk) { yield* revertSingle(hash, file) } @@ -373,6 +374,10 @@ export namespace Snapshot { cwd: state.worktree, }) if (result.code !== 0) { + log.info("batched checkout failed, falling back to single-file revert", { + hash, + files: filesToCheckout.length, + }) for (const file of filesToCheckout) { yield* revertSingle(hash, file) } From 8222e7284195d89be3eccbae84d8cb973de36050 Mon Sep 17 00:00:00 2001 From: Nate Williams <50088025+natewill@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:34:07 -0400 Subject: [PATCH 10/11] Update comments in snapshot index.ts Refactor comments for clarity and consistency in index.ts. --- packages/opencode/src/snapshot/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 123c1cda4464..d75a755a816b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -331,9 +331,8 @@ export namespace Snapshot { yield* remove(file) }) - // Preserve the old first-file-wins behavior, then batch the selected files by snapshot hash. for (const [hash, filesByHash] of groupByHash) { - // Run batched git commands in 100-file chunks to avoid oversized argv calls. + // run batched git commands in 100-file chunks to avoid oversized argv calls. for (let i = 0; i < filesByHash.length; i += 100) { const fileChunk = filesByHash.slice(i, i + 100) if (fileChunk.length === 1) { @@ -360,7 +359,6 @@ export namespace Snapshot { continue } - // Only checkout files that exist in this snapshot; files missing from the tree should be removed. const snapshotPaths = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean)) const filesToCheckout: string[] = [] const missingFiles: string[] = [] From 06f5cceb56a793d26f58519b6cb70de228f9a760 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 1 Apr 2026 18:58:49 -0400 Subject: [PATCH 11/11] refactor(opencode): simplify file path naming --- packages/opencode/src/snapshot/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index d75a755a816b..596f1ad2a095 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -340,12 +340,12 @@ export namespace Snapshot { continue } - const fileChunkPaths = fileChunk.map((file) => ({ + const filePaths = fileChunk.map((file) => ({ rel: path.relative(state.worktree, file).replaceAll("\\", "/"), file, })) const tree = yield* git( - [...core, ...args(["ls-tree", "--name-only", hash, "--", ...fileChunkPaths.map((item) => item.rel)])], + [...core, ...args(["ls-tree", "--name-only", hash, "--", ...filePaths.map((item) => item.rel)])], { cwd: state.worktree, }, @@ -362,7 +362,7 @@ export namespace Snapshot { const snapshotPaths = new Set(tree.text.trim().split("\n").map((item) => item.trim()).filter(Boolean)) const filesToCheckout: string[] = [] const missingFiles: string[] = [] - for (const item of fileChunkPaths) { + for (const item of filePaths) { if (snapshotPaths.has(item.rel)) { filesToCheckout.push(item.file) continue