Fork tasks from snapshots; snapshot only when text follows tool calls

This commit is contained in:
Ian Gulliver
2026-10-15 11:36:51 -07:00
parent 3fe0374ac5
commit 0000db1f86
25 changed files with 893 additions and 421 deletions
+13
View File
@@ -33,6 +33,7 @@ type Options struct {
Model string
SessionID string
Resume string
ResumeAt string
ForkSession bool
GitName string
GitEmail string
@@ -154,6 +155,14 @@ func (e Event) ParentToolUseID() (string, error) {
return msg.ParentToolUseID, err
}
func (e Event) UUID() (string, error) {
msg := struct {
UUID string `json:"uuid"`
}{}
err := json.Unmarshal(e.Raw, &msg)
return msg.UUID, err
}
func (e Event) TextDelta() (string, error) {
msg := struct {
ParentToolUseID *string `json:"parent_tool_use_id"`
@@ -326,6 +335,10 @@ func (s *Session) buildArgs(opts Options) ([]string, error) {
args = append(args, "--resume="+opts.Resume)
}
if opts.ResumeAt != "" {
args = append(args, "--resume-session-at="+opts.ResumeAt)
}
if opts.ForkSession {
args = append(args, "--fork-session")
}
+61
View File
@@ -262,6 +262,67 @@ func TestResume(t *testing.T) {
}
}
func TestForkAt(t *testing.T) {
t.Parallel()
ctx := testContext(t)
first, err := claude.Start(ctx, options(t, "first", claude.Options{Cwd: scratchRepo(t)}))
if err != nil {
t.Fatalf("start: %v", err)
}
send(t, first, "Run the bash command `echo APPLE-1`. After it finishes, run the bash command `echo BANANA-2`. Then reply with the single word ok.")
cut := ""
waitResult(t, ctx, first, func(ev claude.Event) {
if cut != "" || ev.Type != "user" {
return
}
blocks, err := ev.Blocks()
if err != nil {
t.Fatalf("parse user: %v", err)
}
for _, b := range blocks {
if b.Type == "tool_result" && strings.Contains(string(b.Content), "APPLE-1") {
cut, err = ev.UUID()
if err != nil {
t.Fatalf("parse uuid: %v", err)
}
}
}
})
id := first.ID()
closeSession(t, first)
if cut == "" {
t.Fatalf("never saw the APPLE-1 tool result")
}
forkID := "8a1b2c3d-0000-4000-8000-" + strings.ReplaceAll(time.Now().Format("150405.000000"), ".", "")
fork, err := claude.Start(ctx, options(t, "fork", claude.Options{
Cwd: scratchRepo(t),
SessionID: forkID,
Resume: id,
ResumeAt: cut,
ForkSession: true,
}))
if err != nil {
t.Fatalf("fork: %v", err)
}
t.Cleanup(func() { closeSession(t, fork) })
send(t, fork, "Without running anything, list the exact output of every bash command you have run in this conversation, one per line, and nothing else.")
text := waitResult(t, ctx, fork, nil)
if !strings.Contains(text, "APPLE-1") {
t.Fatalf("fork lost the conversation before the cut; assistant said %q", text)
}
if strings.Contains(text, "BANANA-2") {
t.Fatalf("fork kept the conversation after the cut; assistant said %q", text)
}
if fork.ID() != forkID {
t.Fatalf("fork has session id %s, want %s", fork.ID(), forkID)
}
}
func testContext(t *testing.T) context.Context {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)