charmbracelet/crush · error

failed to save todos: %w

Error message

failed to save todos: %w

What it means

Validating and building the todo list succeeded, but sessions.Save(ctx, currentSession) failed when persisting the session with its updated Todos. The DB error is wrapped with %w. The todo update is not applied.

Source

Thrown at internal/agent/tools/todos.go:101

						justCompleted = append(justCompleted, item.Content)
					}
				}

				if newStatus == session.TodoStatusInProgress {
					if !existed || oldStatus != session.TodoStatusInProgress {
						if item.ActiveForm != "" {
							justStarted = item.ActiveForm
						} else {
							justStarted = item.Content
						}
					}
				}
			}

			currentSession.Todos = todos
			_, err = sessions.Save(ctx, currentSession)
			if err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to save todos: %w", err)
			}

			response := "Todo list updated successfully.\n\n"

			pendingCount := 0
			inProgressCount := 0

			for _, todo := range todos {
				switch todo.Status {
				case session.TodoStatusPending:
					pendingCount++
				case session.TodoStatusInProgress:
					inProgressCount++
				}
			}

			response += fmt.Sprintf("Status: %d pending, %d in progress, %d completed\n",
				pendingCount, inProgressCount, completedCount)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Inspect the wrapped cause to identify the DB issue (locked vs I/O vs constraint)
  2. Ensure no other process holds a write lock on the SQLite DB; enable busy timeout
  3. Check disk space and that the DB file/path is writable
  4. Retry the tool call after resolving the environment issue

Example fix

// before
// save fails with 'database is locked'
// after
// close the second crush instance (or set busy_timeout) and retry the todos update
Defensive patterns

Strategy: retry

Try / catch

if err != nil && strings.Contains(err.Error(), "failed to save todos") {
    if errors.Is(err, sqlite.ErrLocked) || isTransientIO(err) {
        time.Sleep(backoff)
        return retry()
    }
    return err
}

Prevention

When it happens

Trigger: SQLite write failure during the todos tool: database file locked by another process, disk full, read-only filesystem, schema/migration mismatch, or the session row being deleted between Get and Save.

Common situations: Running two Crush instances against the same DB; disk quota exhausted; DB file moved or corrupted mid-session; running on a read-only mount.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/5d7f66e28e13210e. Report an issue: GitHub.