Codex mid-turn delivery deletes input before the steer is accepted

This commit is contained in:
Ian Gulliver
2026-10-16 14:18:40 -07:00
parent a0e6a3953f
commit 8c7c3bee2e
9 changed files with 118 additions and 33 deletions
+4 -3
View File
@@ -164,13 +164,14 @@ func (m *Manager) codexItemCompleted(ctx context.Context, l *live, s *codex.Sess
if l.reviewID != "" {
return nil
}
text := m.deliverMidTurn(ctx, l)
if text == "" {
mt := m.deliverMidTurn(ctx, l)
if mt.text == "" {
return nil
}
err = s.Steer(ctx, text)
err = s.Steer(ctx, mt.text)
if err != nil {
slog.Error("steer mid-turn delivery", "session", l.id, "err", err)
m.restoreMidTurn(ctx, l, mt)
}
return nil
}
+58 -18
View File
@@ -350,13 +350,24 @@ func (m *Manager) Deliver(ctx context.Context, sessionID string) error {
return m.store.MarkDelivered(ctx, ids)
}
func (m *Manager) deliverMidTurn(ctx context.Context, l *live) string {
type midTurn struct {
text string
taskInputs []store.TaskInput
leadInputs []store.LeadInput
responses []string
}
func (m *Manager) deliverMidTurn(ctx context.Context, l *live) midTurn {
if l.owner != "" {
msgs := m.takeLeadQueuedMidTurn(ctx, l)
if len(msgs) == 0 {
return ""
inputs := m.takeLeadQueuedMidTurn(ctx, l)
if len(inputs) == 0 {
return midTurn{}
}
return m.recordQueued(ctx, l, msgs)
msgs := []queued{}
for _, in := range inputs {
msgs = append(msgs, queued{actor: in.Actor, text: in.Text})
}
return midTurn{text: m.recordQueued(ctx, l, msgs), leadInputs: inputs}
}
unlock := m.lockDelivery(l.id)
@@ -365,49 +376,78 @@ func (m *Manager) deliverMidTurn(ctx context.Context, l *live) string {
discard, err := m.store.GetDiscard(ctx, l.taskID)
if err != nil {
slog.Error("read discard state", "task", l.taskID, "err", err)
return ""
return midTurn{}
}
if discard.State != store.DiscardNone {
return ""
return midTurn{}
}
mt := midTurn{}
parts := []string{}
msgs := m.takeQueuedMidTurn(ctx, l)
if len(msgs) > 0 {
mt.taskInputs = m.takeQueuedMidTurn(ctx, l)
if len(mt.taskInputs) > 0 {
msgs := []queued{}
for _, in := range mt.taskInputs {
msgs = append(msgs, queued{actor: in.Actor, text: in.Text})
}
parts = append(parts, m.recordQueued(ctx, l, msgs))
}
responses := m.responsesMidTurn(ctx, l)
responses, ids := m.responsesMidTurn(ctx, l)
if responses != "" {
parts = append(parts, responses)
mt.responses = ids
}
return strings.Join(parts, "\n\n")
mt.text = strings.Join(parts, "\n\n")
return mt
}
func (m *Manager) responsesMidTurn(ctx context.Context, l *live) string {
func (m *Manager) restoreMidTurn(ctx context.Context, l *live, mt midTurn) {
if len(mt.taskInputs) > 0 {
err := m.store.RestoreTaskInputs(ctx, mt.taskInputs)
if err != nil {
slog.Error("restore queued messages", "session", l.id, "err", err)
}
}
if len(mt.leadInputs) > 0 {
err := m.store.RestoreLeadInputs(ctx, mt.leadInputs)
if err != nil {
slog.Error("restore held lead messages", "session", l.id, "err", err)
}
}
if len(mt.responses) > 0 {
err := m.store.MarkUndelivered(ctx, mt.responses)
if err != nil {
slog.Error("mark responses undelivered", "session", l.id, "err", err, "responses", mt.responses)
}
}
slog.Info("mid-turn delivery restored", "session", l.id, "inputs", len(mt.taskInputs)+len(mt.leadInputs), "responses", mt.responses)
}
func (m *Manager) responsesMidTurn(ctx context.Context, l *live) (string, []string) {
pending, err := m.store.Undelivered(ctx, l.id)
if err != nil {
slog.Error("list undelivered responses", "session", l.id, "err", err)
return ""
return "", nil
}
if len(pending) == 0 {
return ""
return "", nil
}
ids := responseIDs(pending)
waiting, err := m.store.AwaitingHuman(ctx, l.id)
if err != nil {
slog.Error("count waiting questions", "session", l.id, "err", err)
return ""
return "", nil
}
if waiting > 0 {
slog.Info("deliver held mid-turn: question waiting", "session", l.id, "waiting", waiting, "responses", ids)
return ""
return "", nil
}
err = m.store.MarkDelivered(ctx, ids)
if err != nil {
slog.Error("mark responses delivered", "session", l.id, "err", err, "responses", ids)
return ""
return "", nil
}
text := deliveryMessage(pending)
@@ -416,7 +456,7 @@ func (m *Manager) responsesMidTurn(ctx context.Context, l *live) string {
slog.Error("record mid-turn delivery", "session", l.id, "err", err)
}
slog.Info("deliver sent mid-turn", "session", l.id, "responses", ids)
return text
return text, ids
}
func responseIDs(pending []store.Pending) []string {
+6 -6
View File
@@ -119,7 +119,7 @@ type queued struct {
text string
}
func (m *Manager) takeQueuedMidTurn(ctx context.Context, l *live) []queued {
func (m *Manager) takeQueuedMidTurn(ctx context.Context, l *live) []store.TaskInput {
m.mu.Lock()
compacting := l.compacting
m.mu.Unlock()
@@ -132,13 +132,13 @@ func (m *Manager) takeQueuedMidTurn(ctx context.Context, l *live) []queued {
slog.Error("list queued messages", "session", l.id, "err", err)
return nil
}
out := []queued{}
out := []store.TaskInput{}
ids := []int64{}
for _, in := range inputs {
if len(in.Images) > 0 {
break
}
out = append(out, queued{actor: in.Actor, text: in.Text})
out = append(out, in)
ids = append(ids, in.ID)
}
if len(ids) == 0 {
@@ -152,7 +152,7 @@ func (m *Manager) takeQueuedMidTurn(ctx context.Context, l *live) []queued {
return out
}
func (m *Manager) takeLeadQueuedMidTurn(ctx context.Context, l *live) []queued {
func (m *Manager) takeLeadQueuedMidTurn(ctx context.Context, l *live) []store.LeadInput {
m.mu.Lock()
compacting := l.compacting
m.mu.Unlock()
@@ -168,7 +168,7 @@ func (m *Manager) takeLeadQueuedMidTurn(ctx context.Context, l *live) []queued {
slog.Error("list held lead messages", "session", l.id, "err", err)
return nil
}
out := []queued{}
out := []store.LeadInput{}
ids := []int64{}
for _, in := range inputs {
if in.Kind != store.LeadMessage {
@@ -177,7 +177,7 @@ func (m *Manager) takeLeadQueuedMidTurn(ctx context.Context, l *live) []queued {
if len(in.Images) > 0 {
break
}
out = append(out, queued{actor: in.Actor, text: in.Text})
out = append(out, in)
ids = append(ids, in.ID)
}
if len(ids) == 0 {
+3 -3
View File
@@ -168,14 +168,14 @@ func (m *Manager) dropSnapshots(repo, taskID string) error {
func (m *Manager) afterTool(l *live) claude.HookFunc {
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
text := m.deliverMidTurn(ctx, l)
if text == "" {
mt := m.deliverMidTurn(ctx, l)
if mt.text == "" {
return map[string]any{}, nil
}
return map[string]any{
"hookSpecificOutput": map[string]any{
"hookEventName": "PostToolUse",
"additionalContext": text,
"additionalContext": mt.text,
},
}, nil
}