Deliver inbox answers mid-turn through the PostToolUse hook

This commit is contained in:
Ian Gulliver
2026-10-15 10:20:23 -07:00
parent c934b00737
commit 03b70bd44e
10 changed files with 186 additions and 21 deletions
+38 -2
View File
@@ -275,6 +275,7 @@ func (m *Manager) Recover(ctx context.Context) error {
l, err := m.ensureLive(ctx, sess, task)
if err != nil {
slog.Error("resume session", "session", sess.Id, "err", err)
m.logUndelivered(ctx, sess.Id, "resume failed")
err = m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateIdle, "byre", map[string]any{"error": err.Error()})
if err != nil {
return err
@@ -290,12 +291,33 @@ func (m *Manager) Recover(ctx context.Context) error {
return err
}
slog.Info("resumed session", "session", sess.Id, "task", task.Id, "owner", sess.Owner)
m.logUndelivered(ctx, sess.Id, "resumed with restart notice")
}
idle, err := m.store.SessionsInState(ctx, api.AgentSessionStateIdle)
if err != nil {
return err
}
for _, sess := range idle {
m.logUndelivered(ctx, sess.Id, "idle at startup")
}
go m.deliverAllLeads()
return m.drain(ctx)
}
func (m *Manager) logUndelivered(ctx context.Context, sessionID, reason string) {
pending, err := m.store.Undelivered(ctx, sessionID)
if err != nil {
slog.Error("list undelivered responses", "session", sessionID, "err", err)
return
}
if len(pending) == 0 {
return
}
slog.Info("undelivered responses", "session", sessionID, "reason", reason, "responses", responseIDs(pending))
}
func (m *Manager) Close() {
close(m.watchStop)
@@ -510,7 +532,17 @@ func (m *Manager) cancelQueued(ctx context.Context, sess api.AgentSession) (api.
m.capMu.Lock()
defer m.capMu.Unlock()
err := m.store.DeleteQueuedTurn(ctx, sess.Id)
queued, err := m.store.QueuedTurns(ctx)
if err != nil {
return api.AgentSession{}, err
}
for _, q := range queued {
if q.SessionID == sess.Id && strings.HasPrefix(q.Text, "Responses from your inbox:") {
slog.Info("cancelled queued turn carried inbox responses", "session", sess.Id, "text", q.Text)
}
}
err = m.store.DeleteQueuedTurn(ctx, sess.Id)
if errors.Is(err, store.ErrNotFound) {
return api.AgentSession{}, fmt.Errorf("%w: session %s has no queued turn", ErrConflict, sess.Id)
}
@@ -607,7 +639,7 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
hooks := map[string][]claude.HookMatcher{
"UserPromptSubmit": {{Func: remindGuidance(g)}},
"Stop": {{Func: m.bounceProseQuestion(l)}},
"PostToolUse": {{Func: m.snapshotAfterTool(l)}},
"PostToolUse": {{Func: m.afterTool(l)}},
}
if l.owner != "" {
l.dir = filepath.Join(m.leadRoot, url.PathEscape(l.owner))
@@ -682,6 +714,7 @@ func (m *Manager) pump(l *live) {
m.mu.Unlock()
if quiet {
m.logUndelivered(ctx, l.id, "agent process ended while closing or stopping")
return
}
@@ -695,6 +728,7 @@ func (m *Manager) pump(l *live) {
return
}
m.logUndelivered(ctx, l.id, "agent process exited mid-turn")
err = m.store.SetSessionState(ctx, l.id, api.AgentSessionStateIdle, agentActor, map[string]any{"error": "agent process exited mid-turn"})
if err != nil {
slog.Error("record session exit", "session", l.id, "err", err)
@@ -822,11 +856,13 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
slog.Error("start queued turns", "err", err)
}
if handoff {
m.logUndelivered(ctx, l.id, "turn ended in handoff")
go m.continueFromHandoff(l.id, res.Result)
return nil
}
mergeBy := m.takeMerge(l.taskID)
if mergeBy != "" && finished {
m.logUndelivered(ctx, l.id, "turn ended in merge")
go m.finishMerge(l, mergeBy)
return nil
}