Fork tasks from snapshots; snapshot only when text follows tool calls
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user