Run Claude sessions on tasks with event streaming and restart recovery
This commit is contained in:
@@ -29,6 +29,7 @@ type HookMatcher struct {
|
||||
type Options struct {
|
||||
Cwd string
|
||||
Model string
|
||||
SessionID string
|
||||
Resume string
|
||||
ForkSession bool
|
||||
GitName string
|
||||
@@ -45,12 +46,15 @@ type Event struct {
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
Name string `json:"name"`
|
||||
Input json.RawMessage `json:"input"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
IsError bool `json:"is_error"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
ToolUseID string `json:"tool_use_id"`
|
||||
Text string `json:"text"`
|
||||
Thinking string `json:"thinking"`
|
||||
Name string `json:"name"`
|
||||
Input json.RawMessage `json:"input"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
IsError bool `json:"is_error"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -138,6 +142,7 @@ func Start(ctx context.Context, opts Options) (*Session, error) {
|
||||
pending: map[string]chan controlResult{},
|
||||
hooks: map[string]HookFunc{},
|
||||
servers: map[string]*mcp.Server{},
|
||||
id: initialID(opts),
|
||||
}
|
||||
|
||||
args, err := s.buildArgs(opts)
|
||||
@@ -179,6 +184,16 @@ func Start(ctx context.Context, opts Options) (*Session, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func initialID(opts Options) string {
|
||||
if opts.SessionID != "" {
|
||||
return opts.SessionID
|
||||
}
|
||||
if opts.ForkSession {
|
||||
return ""
|
||||
}
|
||||
return opts.Resume
|
||||
}
|
||||
|
||||
func (s *Session) buildArgs(opts Options) ([]string, error) {
|
||||
settings, err := json.Marshal(map[string]any{
|
||||
"attribution": map[string]any{"commit": "", "pr": ""},
|
||||
@@ -190,7 +205,7 @@ func (s *Session) buildArgs(opts Options) ([]string, error) {
|
||||
servers := map[string]any{}
|
||||
for _, srv := range opts.MCPServers {
|
||||
s.servers[srv.Name] = srv
|
||||
servers[srv.Name] = map[string]any{"type": "sdk", "name": srv.Name}
|
||||
servers[srv.Name] = map[string]any{"type": "sdk", "name": srv.Name, "alwaysLoad": true}
|
||||
}
|
||||
|
||||
mcpConfig, err := json.Marshal(map[string]any{"mcpServers": servers})
|
||||
@@ -212,6 +227,10 @@ func (s *Session) buildArgs(opts Options) ([]string, error) {
|
||||
args = append(args, "--model", opts.Model)
|
||||
}
|
||||
|
||||
if opts.SessionID != "" {
|
||||
args = append(args, "--session-id="+opts.SessionID)
|
||||
}
|
||||
|
||||
if opts.Resume != "" {
|
||||
args = append(args, "--resume="+opts.Resume)
|
||||
}
|
||||
@@ -342,6 +361,12 @@ func (s *Session) Close() error {
|
||||
return s.cmd.Wait()
|
||||
}
|
||||
|
||||
func (s *Session) Kill() {
|
||||
s.cmd.Process.Kill()
|
||||
<-s.done
|
||||
s.cmd.Wait()
|
||||
}
|
||||
|
||||
func (s *Session) write(v any) error {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -70,6 +71,9 @@ func TestCommitIdentityHooksAndReport(t *testing.T) {
|
||||
if len(tools) == 0 {
|
||||
t.Fatalf("PostToolUse hook never fired")
|
||||
}
|
||||
if slices.Contains(tools, "ToolSearch") {
|
||||
t.Fatalf("byre MCP tools were deferred behind ToolSearch")
|
||||
}
|
||||
|
||||
log := git(t, repo, "log", "-1", "--format=%an <%ae>%n%cn <%ce>%n%B")
|
||||
want := gitName + " <" + gitEmail + ">\n" + gitName + " <" + gitEmail + ">\nadd hello"
|
||||
@@ -119,13 +123,26 @@ func TestInterrupt(t *testing.T) {
|
||||
ctx := testContext(t)
|
||||
s := start(t, ctx, claude.Options{Cwd: scratchRepo(t)})
|
||||
|
||||
send(t, s, "Run the bash command `sleep 120` in the foreground (not in the background), then say done.")
|
||||
send(t, s, "Run the bash command `ping -c 120 127.0.0.1` in the foreground (not in the background) and wait for it to finish, then say done.")
|
||||
|
||||
interruptedAt := time.Time{}
|
||||
waitResult(t, ctx, s, func(ev claude.Event) {
|
||||
if ev.Type == "user" {
|
||||
blocks, err := ev.Blocks()
|
||||
if err != nil {
|
||||
t.Fatalf("parse user: %v", err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
if b.Type == "tool_result" && (!b.IsError || strings.Contains(string(b.Content), "Blocked")) {
|
||||
t.Fatalf("command finished or was refused instead of being interrupted: %s", b.Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !interruptedAt.IsZero() || !hasToolUse(t, ev) {
|
||||
return
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
interruptedAt = time.Now()
|
||||
err := s.Interrupt(ctx)
|
||||
if err != nil {
|
||||
|
||||
+39
-49
File diff suppressed because one or more lines are too long
+19
-19
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15
-15
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user