feat: protect agent plan edits and recover revisions (#3745)

This commit is contained in:
Carlos Florêncio
2026-09-17 10:43:28 +01:00
committed by GitHub
parent 1981b37338
commit b1b8857146
57 changed files with 5005 additions and 282 deletions
@@ -198,6 +198,12 @@ func (r *Repository) runMigrations() error {
r.migrate.Apply("task_plans.implementation_started_session_id", `ALTER TABLE task_plans ADD COLUMN implementation_started_session_id TEXT`)
r.migrate.Apply("task_plans.implementation_started_by", `ALTER TABLE task_plans ADD COLUMN implementation_started_by TEXT`)
_ = r.migrate.Apply("task_plans.comments_revision", `ALTER TABLE task_plans ADD COLUMN comments_revision INTEGER NOT NULL DEFAULT 0`)
if err := r.migrate.Apply("task_plans.write_version", `ALTER TABLE task_plans ADD COLUMN write_version TEXT NOT NULL DEFAULT ''`); err != nil {
return err
}
if err := r.backfillTaskPlanWriteVersions(); err != nil {
return err
}
// Authoritative per-message change signal (chat render-perf). SQLite forbids a
// non-constant default on ADD COLUMN, so the column is added nullable and
@@ -418,6 +424,44 @@ func (r *Repository) runMigrations() error {
return nil
}
// backfillTaskPlanWriteVersions assigns a fresh opaque token to legacy HEAD
// rows. The empty-value predicate makes startup replay and an interrupted
// backfill safe without changing an already assigned version.
func (r *Repository) backfillTaskPlanWriteVersions() error {
ctx := r.migrationContext()
rows, err := r.db.QueryxContext(ctx, r.db.Rebind(`
SELECT id FROM task_plans
WHERE COALESCE(write_version, '') = ''
`))
if err != nil {
return fmt.Errorf("list plans missing write versions: %w", err)
}
defer func() { _ = rows.Close() }()
var planIDs []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return fmt.Errorf("scan plan missing write version: %w", err)
}
planIDs = append(planIDs, id)
}
if err := rows.Err(); err != nil {
return fmt.Errorf("iterate plans missing write versions: %w", err)
}
for _, id := range planIDs {
if _, err := r.db.ExecContext(ctx, r.db.Rebind(`
UPDATE task_plans
SET write_version = ?
WHERE id = ? AND COALESCE(write_version, '') = ''
`), uuid.NewString(), id); err != nil {
return fmt.Errorf("backfill plan write version %s: %w", id, err)
}
}
return nil
}
func (r *Repository) backfillTaskSessionQueueIncarnations() error {
ctx := r.migrationContext()
rows, err := r.db.QueryxContext(ctx, `SELECT id FROM task_sessions WHERE queue_incarnation_id = ''`)