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
+50 -15
View File
@@ -86,6 +86,8 @@ type live struct {
wantCompact bool
rateLimited map[string]any
toolsSinceText bool
contextModel string
contextUsed int
contextWindow int
@@ -608,6 +610,7 @@ func (m *Manager) message(ctx context.Context, l *live, actor, text string, imag
l.wantCompact = false
l.compacting = compacting
l.rateLimited = nil
l.toolsSinceText = false
m.mu.Unlock()
return l.sess.Send(text, imgs...)
@@ -643,7 +646,10 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
owner: sess.Owner,
}
g := taskGuidance(task)
g, err := m.taskGuidance(ctx, task)
if err != nil {
return nil, err
}
tools := []mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.createTaskTool(l), m.setMergeTitleTool(l)}
hooks := map[string][]claude.HookMatcher{
"UserPromptSubmit": {{Func: remindGuidance(g)}},
@@ -675,9 +681,15 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
if err != nil {
return nil, err
}
if started {
switch {
case started:
opts.Resume = sess.VendorSessionId
} else {
case sess.ForkedFrom != nil:
opts.SessionID = sess.VendorSessionId
opts.Resume = *sess.ForkedFrom
opts.ResumeAt = *sess.ForkedAt
opts.ForkSession = true
default:
opts.SessionID = sess.VendorSessionId
}
@@ -691,9 +703,6 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
m.live[sess.Id] = l
go m.pump(l)
if l.owner == "" {
go m.snapshotTimer(l)
}
return l, nil
}
@@ -764,11 +773,29 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
if err != nil {
return err
}
uuid, err := ev.UUID()
if err != nil {
return err
}
for _, b := range blocks {
err = m.handleAssistantBlock(ctx, l, b, parent)
err = m.handleAssistantBlock(ctx, l, b, parent, uuid)
if err != nil {
return err
}
if parent != "" || l.owner != "" {
continue
}
switch {
case b.Type == "tool_use":
l.toolsSinceText = true
case b.Type == "text" && l.toolsSinceText:
l.toolsSinceText = false
seq, err := m.store.LastTaskSeq(ctx, l.taskID)
if err != nil {
return err
}
m.snapshotSoon(l, api.SnapshotTriggerMessage, seq)
}
}
if parent != "" {
return nil
@@ -792,6 +819,10 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
if err != nil {
return err
}
uuid, err := ev.UUID()
if err != nil {
return err
}
for _, b := range blocks {
if b.Type != "tool_result" {
continue
@@ -804,7 +835,7 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
"tool_use_id": b.ToolUseID,
"is_error": b.IsError,
"content": content,
}, parent))
}, parent, uuid))
if err != nil {
return err
}
@@ -838,7 +869,7 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
if sha != "" {
payload["commit"] = sha
}
m.snapshotSoon(l, api.SnapshotTriggerTurnEnd)
m.snapshotSoon(l, api.SnapshotTriggerTurnEnd, 0)
switch {
case handoff:
payload["handoff"] = true
@@ -957,16 +988,16 @@ func (m *Manager) updateContext(ctx context.Context, l *live, used, window int)
return m.emit(ctx, l, agentActor, "session.context", map[string]any{"model": l.contextModel, "used": used, "window": window})
}
func (m *Manager) handleAssistantBlock(ctx context.Context, l *live, b claude.Block, parent string) error {
func (m *Manager) handleAssistantBlock(ctx context.Context, l *live, b claude.Block, parent, uuid string) error {
switch b.Type {
case "text":
return m.emit(ctx, l, agentActor, "agent.message", withParent(map[string]any{"text": b.Text}, parent))
return m.emit(ctx, l, agentActor, "agent.message", withParent(map[string]any{"text": b.Text}, parent, uuid))
case "thinking":
if b.Thinking == "" {
return nil
}
return m.emit(ctx, l, agentActor, "agent.thinking", withParent(map[string]any{"text": b.Thinking}, parent))
return m.emit(ctx, l, agentActor, "agent.thinking", withParent(map[string]any{"text": b.Thinking}, parent, uuid))
case "tool_use":
input, err := decode(b.Input)
@@ -977,15 +1008,19 @@ func (m *Manager) handleAssistantBlock(ctx context.Context, l *live, b claude.Bl
"id": b.ID,
"name": b.Name,
"input": input,
}, parent))
}, parent, uuid))
}
return m.emit(ctx, l, agentActor, "agent.vendor", withParent(store.ToPayload(b), parent))
return m.emit(ctx, l, agentActor, "agent.vendor", withParent(store.ToPayload(b), parent, uuid))
}
func withParent(payload map[string]any, parent string) map[string]any {
func withParent(payload map[string]any, parent, uuid string) map[string]any {
if parent != "" {
payload["parent_tool_use_id"] = parent
return payload
}
if uuid != "" {
payload["vendor_uuid"] = uuid
}
return payload
}