Skip to content

Commit 284c010

Browse files
committed
wip: more snapshot stuff
1 parent 22c9e29 commit 284c010

File tree

12 files changed

+340
-144
lines changed

12 files changed

+340
-144
lines changed

packages/opencode/src/cli/cmd/debug/snapshot.ts

Lines changed: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,76 +5,30 @@ import { cmd } from "../cmd"
55

66
export const SnapshotCommand = cmd({
77
command: "snapshot",
8-
builder: (yargs) =>
9-
yargs.command(CreateCommand).command(RestoreCommand).command(DiffCommand).command(RevertCommand).demandCommand(),
8+
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).demandCommand(),
109
async handler() {},
1110
})
1211

13-
const CreateCommand = cmd({
14-
command: "create",
12+
const TrackCommand = cmd({
13+
command: "track",
1514
async handler() {
1615
await bootstrap({ cwd: process.cwd() }, async () => {
17-
const result = await Snapshot.create()
18-
console.log(result)
19-
})
20-
},
21-
})
22-
23-
const RestoreCommand = cmd({
24-
command: "restore <commit>",
25-
builder: (yargs) =>
26-
yargs.positional("commit", {
27-
type: "string",
28-
description: "commit",
29-
demandOption: true,
30-
}),
31-
async handler(args) {
32-
await bootstrap({ cwd: process.cwd() }, async () => {
33-
await Snapshot.restore(args.commit)
34-
console.log("restored")
16+
console.log(await Snapshot.track())
3517
})
3618
},
3719
})
3820

39-
export const DiffCommand = cmd({
40-
command: "diff <commit>",
41-
describe: "diff",
21+
const PatchCommand = cmd({
22+
command: "patch <hash>",
4223
builder: (yargs) =>
43-
yargs.positional("commit", {
24+
yargs.positional("hash", {
4425
type: "string",
45-
description: "commit",
26+
description: "hash",
4627
demandOption: true,
4728
}),
4829
async handler(args) {
4930
await bootstrap({ cwd: process.cwd() }, async () => {
50-
const diff = await Snapshot.diff(args.commit)
51-
console.log(diff)
52-
})
53-
},
54-
})
55-
56-
export const RevertCommand = cmd({
57-
command: "revert <sessionID> <messageID>",
58-
describe: "revert",
59-
builder: (yargs) =>
60-
yargs
61-
.positional("sessionID", {
62-
type: "string",
63-
description: "sessionID",
64-
demandOption: true,
65-
})
66-
.positional("messageID", {
67-
type: "string",
68-
description: "messageID",
69-
demandOption: true,
70-
}),
71-
async handler(args) {
72-
await bootstrap({ cwd: process.cwd() }, async () => {
73-
const session = await Session.revert({
74-
sessionID: args.sessionID,
75-
messageID: args.messageID,
76-
})
77-
console.log(session?.revert)
31+
console.log(await Snapshot.patch(args.hash))
7832
})
7933
},
8034
})

packages/opencode/src/session/index.ts

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ export namespace Session {
661661
description: item.description,
662662
inputSchema: item.parameters as ZodSchema,
663663
async execute(args, options) {
664+
await processor.track(options.toolCallId)
664665
const result = await item.execute(args, {
665666
sessionID: input.sessionID,
666667
abort: abort.signal,
@@ -699,6 +700,7 @@ export namespace Session {
699700
const execute = item.execute
700701
if (!execute) continue
701702
item.execute = async (args, opts) => {
703+
await processor.track(opts.toolCallId)
702704
const result = await execute(args, opts)
703705
const output = result.content
704706
.filter((x: any) => x.type === "text")
@@ -814,7 +816,12 @@ export namespace Session {
814816

815817
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
816818
const toolCalls: Record<string, MessageV2.ToolPart> = {}
819+
const snapshots: Record<string, string> = {}
817820
return {
821+
async track(toolCallID: string) {
822+
const hash = await Snapshot.track()
823+
if (hash) snapshots[toolCallID] = hash
824+
},
818825
partFromToolCall(toolCallID: string) {
819826
return toolCalls[toolCallID]
820827
},
@@ -828,15 +835,6 @@ export namespace Session {
828835
})
829836
switch (value.type) {
830837
case "start":
831-
const snapshot = await Snapshot.create()
832-
if (snapshot)
833-
await updatePart({
834-
id: Identifier.ascending("part"),
835-
messageID: assistantMsg.id,
836-
sessionID: assistantMsg.sessionID,
837-
type: "snapshot",
838-
snapshot,
839-
})
840838
break
841839

842840
case "tool-input-start":
@@ -857,6 +855,9 @@ export namespace Session {
857855
case "tool-input-delta":
858856
break
859857

858+
case "tool-input-end":
859+
break
860+
860861
case "tool-call": {
861862
const match = toolCalls[value.toolCallId]
862863
if (match) {
@@ -892,15 +893,20 @@ export namespace Session {
892893
},
893894
})
894895
delete toolCalls[value.toolCallId]
895-
const snapshot = await Snapshot.create()
896-
if (snapshot)
897-
await updatePart({
898-
id: Identifier.ascending("part"),
899-
messageID: assistantMsg.id,
900-
sessionID: assistantMsg.sessionID,
901-
type: "snapshot",
902-
snapshot,
903-
})
896+
const snapshot = snapshots[value.toolCallId]
897+
if (snapshot) {
898+
const patch = await Snapshot.patch(snapshot)
899+
if (patch.files.length) {
900+
await updatePart({
901+
id: Identifier.ascending("part"),
902+
messageID: assistantMsg.id,
903+
sessionID: assistantMsg.sessionID,
904+
type: "patch",
905+
hash: patch.hash,
906+
files: patch.files,
907+
})
908+
}
909+
}
904910
}
905911
break
906912
}
@@ -921,15 +927,18 @@ export namespace Session {
921927
},
922928
})
923929
delete toolCalls[value.toolCallId]
924-
const snapshot = await Snapshot.create()
925-
if (snapshot)
930+
const snapshot = snapshots[value.toolCallId]
931+
if (snapshot) {
932+
const patch = await Snapshot.patch(snapshot)
926933
await updatePart({
927934
id: Identifier.ascending("part"),
928935
messageID: assistantMsg.id,
929936
sessionID: assistantMsg.sessionID,
930-
type: "snapshot",
931-
snapshot,
937+
type: "patch",
938+
hash: patch.hash,
939+
files: patch.files,
932940
})
941+
}
933942
}
934943
break
935944
}
@@ -1073,33 +1082,45 @@ export namespace Session {
10731082

10741083
export async function revert(input: RevertInput) {
10751084
const all = await messages(input.sessionID)
1076-
const session = await get(input.sessionID)
10771085
let lastUser: MessageV2.User | undefined
1078-
let lastSnapshot: MessageV2.SnapshotPart | undefined
1086+
const session = await get(input.sessionID)
1087+
1088+
let revert: Info["revert"]
1089+
const patches: Snapshot.Patch[] = []
10791090
for (const msg of all) {
10801091
if (msg.info.role === "user") lastUser = msg.info
10811092
const remaining = []
10821093
for (const part of msg.parts) {
1083-
if (part.type === "snapshot") lastSnapshot = part
1084-
if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) {
1085-
// if no useful parts left in message, same as reverting whole message
1086-
const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined
1087-
const snapshot = session.revert?.snapshot ?? (await Snapshot.create())
1088-
log.info("revert snapshot", { snapshot })
1089-
if (lastSnapshot) await Snapshot.restore(lastSnapshot.snapshot)
1090-
const next = await update(input.sessionID, (draft) => {
1091-
draft.revert = {
1092-
// if not part id jump to the last user message
1094+
if (revert) {
1095+
if (part.type === "patch") {
1096+
patches.push(part)
1097+
}
1098+
continue
1099+
}
1100+
1101+
if (!revert) {
1102+
if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) {
1103+
// if no useful parts left in message, same as reverting whole message
1104+
const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined
1105+
revert = {
10931106
messageID: !partID && lastUser ? lastUser.id : msg.info.id,
10941107
partID,
1095-
snapshot,
10961108
}
1097-
})
1098-
return next
1109+
}
1110+
remaining.push(part)
10991111
}
1100-
remaining.push(part)
11011112
}
11021113
}
1114+
1115+
if (revert) {
1116+
const session = await get(input.sessionID)
1117+
revert.snapshot = session.revert?.snapshot ?? (await Snapshot.track())
1118+
await Snapshot.revert(patches)
1119+
return update(input.sessionID, (draft) => {
1120+
draft.revert = revert
1121+
})
1122+
}
1123+
return session
11031124
}
11041125

11051126
export async function unrevert(input: { sessionID: string }) {

packages/opencode/src/session/message-v2.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ export namespace MessageV2 {
9494
})
9595
export type SnapshotPart = z.infer<typeof SnapshotPart>
9696

97+
export const PatchPart = PartBase.extend({
98+
type: z.literal("patch"),
99+
hash: z.string(),
100+
files: z.string().array(),
101+
}).openapi({
102+
ref: "PatchPart",
103+
})
104+
export type PatchPart = z.infer<typeof PatchPart>
105+
97106
export const TextPart = PartBase.extend({
98107
type: z.literal("text"),
99108
text: z.string(),
@@ -203,7 +212,7 @@ export namespace MessageV2 {
203212
export type User = z.infer<typeof User>
204213

205214
export const Part = z
206-
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart])
215+
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart])
207216
.openapi({
208217
ref: "Part",
209218
})

packages/opencode/src/snapshot/index.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Ripgrep } from "../file/ripgrep"
66
import { Log } from "../util/log"
77
import { Global } from "../global"
88
import { Installation } from "../installation"
9+
import { z } from "zod"
910

1011
export namespace Snapshot {
1112
const log = Log.create({ service: "snapshot" })
@@ -24,21 +25,9 @@ export namespace Snapshot {
2425
})
2526
}
2627

27-
export async function create() {
28-
log.info("creating snapshot")
28+
export async function track() {
2929
const app = App.info()
30-
31-
// not a git repo, check if too big to snapshot
32-
if (!app.git || !Installation.isDev()) {
33-
return
34-
const files = await Ripgrep.files({
35-
cwd: app.path.cwd,
36-
limit: 1000,
37-
})
38-
log.info("found files", { count: files.length })
39-
if (files.length >= 1000) return
40-
}
41-
30+
if (!app.git) return
4231
const git = gitdir()
4332
if (await fs.mkdir(git, { recursive: true })) {
4433
await $`git init`
@@ -51,33 +40,52 @@ export namespace Snapshot {
5140
.nothrow()
5241
log.info("initialized")
5342
}
54-
5543
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
56-
log.info("added files")
44+
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).text()
45+
return hash.trim()
46+
}
5747

58-
const result =
59-
await $`git --git-dir ${git} commit --allow-empty -m "snapshot" --no-gpg-sign --author="opencode <[email protected]>"`
60-
.quiet()
61-
.cwd(app.path.cwd)
62-
.nothrow()
48+
export const Patch = z.object({
49+
hash: z.string(),
50+
files: z.string().array(),
51+
})
52+
export type Patch = z.infer<typeof Patch>
6353

64-
const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
65-
if (!match) return
66-
return match![1]
54+
export async function patch(hash: string): Promise<Patch> {
55+
const app = App.info()
56+
const git = gitdir()
57+
const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(app.path.cwd).text()
58+
return {
59+
hash,
60+
files: files
61+
.trim()
62+
.split("\n")
63+
.map((x) => x.trim())
64+
.filter(Boolean)
65+
.map((x) => path.join(app.path.cwd, x)),
66+
}
6767
}
6868

6969
export async function restore(snapshot: string) {
7070
log.info("restore", { commit: snapshot })
7171
const app = App.info()
7272
const git = gitdir()
73-
await $`git --git-dir=${git} reset --hard ${snapshot}`.quiet().cwd(app.path.root)
73+
await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
74+
.quiet()
75+
.cwd(app.path.root)
7476
}
7577

76-
export async function diff(commit: string) {
78+
export async function revert(patches: Patch[]) {
79+
const files = new Set<string>()
7780
const git = gitdir()
78-
const result = await $`git --git-dir=${git} diff -R ${commit}`.quiet().cwd(App.info().path.root)
79-
const text = result.stdout.toString("utf8")
80-
return text
81+
for (const item of patches) {
82+
for (const file of item.files) {
83+
if (files.has(file)) continue
84+
log.info("reverting", { file, hash: item.hash })
85+
await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`.quiet().cwd(App.info().path.root)
86+
files.add(file)
87+
}
88+
}
8189
}
8290

8391
function gitdir() {

packages/sdk/.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 26
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-1efc45c35b58e88b0550fbb0c7a204ef66522742f87c9e29c76a18b120c0d945.yml
3-
openapi_spec_hash: 5e15d85e4704624f9b13bae1c71aa416
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-5748199af356c3243a46a466e73b5d0bab7eaa0c56895e1d0f903d637f61d0bb.yml
3+
openapi_spec_hash: c04f6b6be54b05d9b1283c24e870163b
44
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3

0 commit comments

Comments
 (0)