add supersede semantics on (repo, ref)
When a new push arrives for a (repo, ref) that already has a pending or
active run, the old run is marked superseded. Pending runs are transitioned
directly; active runs have their container killed first. Different refs are
unaffected. The new run takes over as the only pending run for that ref.

Supersede is wired into Runs::create so it happens atomically before the
new run is inserted — the new run is never caught by its own supersede
query. Transition rules updated to allow Pending/Active -> Superseded.
Aggregate state for superseded runs is 'superseded', not 'failed'.

Assisted-by: Owl Alpha via pi
change ttwmxsltlnmuzskywlyyvolrrwtxzuoy
commit 9212969baaf38067762d4266b1fb7572071201fb
author Alpha Chen <alpha@kejadlen.dev>
date
parent soyktwpn
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 46a18af..5ee9b4e 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -89,6 +89,11 @@ impl Runs {
 
     /// Create a new run record in the `pending` state.
     ///
+    /// Before inserting, supersedes any existing `pending` or `active`
+    /// run for the same `(repo, ref)`. Pending runs are marked
+    /// superseded directly; active runs have their container killed
+    /// first, then marked superseded.
+    ///
     /// Inserts a row into `runs` and creates the run directory for
     /// workspace materialization and log storage.
     pub fn create(&self, meta: &RunMeta) -> Result<Run> {
@@ -96,6 +101,12 @@ impl Runs {
         let workspace_path = self.base_dir.join(&id).join("workspace");
 
         let db = crate::db::open(&self.db_path)?;
+
+        // Supersede any existing pending or active run for (repo, ref).
+        // Do this before inserting the new run so the new run is never
+        // caught by its own supersede query.
+        self.supersede_existing(&db, &meta.r#ref)?;
+
         db.execute(
             "INSERT INTO runs (id, repo, ref_name, sha, pushed_at_ms, state, queued_at_ms, workspace_path)
              VALUES (?1, ?2, ?3, ?4, ?5, 'pending', ?6, ?7)",
@@ -123,6 +134,57 @@ impl Runs {
             base_dir: self.base_dir.clone(),
         })
     }
+
+    /// Supersede any existing `pending` or `active` run for
+    /// `(repo, ref)`.
+    ///
+    /// Pending runs are transitioned directly to `superseded`. Active
+    /// runs have their container killed via `docker kill` before
+    /// transition. Different refs are unaffected.
+    fn supersede_existing(&self, db: &rusqlite::Connection, ref_name: &str) -> Result<()> {
+        let now = Timestamp::now().as_millisecond();
+
+        // Handle active runs first: kill the container, then mark superseded.
+        let active_rows: Vec<(String, Option<String>)> = db
+            .prepare(
+                "SELECT id, container_id FROM runs
+                 WHERE repo = ?1 AND ref_name = ?2 AND state = 'active'",
+            )?
+            .query_map(rusqlite::params![&self.repo, ref_name], |row| {
+                Ok((row.get(0)?, row.get(1)?))
+            })?
+            .collect::<std::result::Result<_, _>>()?;
+
+        for (run_id, container_id) in &active_rows {
+            if let Some(cid) = container_id {
+                tracing::info!(run_id = %run_id, container_id = %cid, "killing superseded container");
+                let kill_status = std::process::Command::new("docker")
+                    .args(["kill", cid])
+                    .status();
+                if let Err(e) = kill_status {
+                    tracing::warn!(run_id = %run_id, error = %e, "docker kill failed");
+                }
+            }
+            db.execute(
+                "UPDATE runs SET state = 'superseded', finished_at_ms = ?1, container_id = NULL
+                 WHERE id = ?2",
+                rusqlite::params![now, run_id],
+            )?;
+            tracing::info!(run_id = %run_id, "superseded active run");
+        }
+
+        // Handle pending runs: just mark superseded.
+        let pending_count = db.execute(
+            "UPDATE runs SET state = 'superseded', finished_at_ms = ?1
+             WHERE repo = ?2 AND ref_name = ?3 AND state = 'pending'",
+            rusqlite::params![now, &self.repo, ref_name],
+        )?;
+        if pending_count > 0 {
+            tracing::info!(count = pending_count, "superseded pending run(s)");
+        }
+
+        Ok(())
+    }
 }
 
 /// Move every `pending` or `active` run to `failed` with
@@ -362,7 +424,12 @@ impl Run {
         use RunState::*;
         let allowed = matches!(
             (self.state, to),
-            (Pending, Active) | (Pending, Complete) | (Active, Complete) | (Active, Failed)
+            (Pending, Active)
+                | (Pending, Complete)
+                | (Pending, Superseded)
+                | (Active, Complete)
+                | (Active, Failed)
+                | (Active, Superseded)
         );
         if !allowed {
             return Err(Error::InvalidTransition {
@@ -383,7 +450,7 @@ impl Run {
                     rusqlite::params![now, &self.id],
                 )?;
             }
-            Complete | Failed => {
+            Complete | Failed | Superseded => {
                 db.execute(
                     "UPDATE runs SET state = ?1, \
                         started_at_ms = COALESCE(started_at_ms, ?2), \
@@ -393,7 +460,7 @@ impl Run {
                     rusqlite::params![to.as_str(), now, now, &self.id],
                 )?;
             }
-            _ => unreachable!("checked by allowed match above"),
+            Pending => unreachable!("transition to Pending is not valid"),
         }
 
         self.state = to;
@@ -1004,4 +1071,144 @@ mod tests {
             .unwrap();
         assert_eq!(count, 0);
     }
+
+    #[test]
+    fn create_supersedes_pending_run_on_same_ref() {
+        let (_dir, quire) = tmp_quire();
+        let runs = test_runs(&quire);
+
+        // Create first run.
+        let run1 = runs.create(&test_meta()).expect("create run1");
+        let run1_id = run1.id().to_string();
+        assert_eq!(run1.state(), RunState::Pending);
+
+        // Create second run for same (repo, ref) — should supersede the first.
+        let meta2 = RunMeta {
+            sha: "def456".to_string(),
+            r#ref: "refs/heads/main".to_string(),
+            pushed_at: "2026-04-28T13:00:00Z".parse().unwrap(),
+        };
+        let run2 = runs.create(&meta2).expect("create run2");
+        assert_eq!(run2.state(), RunState::Pending);
+
+        // First run should now be superseded.
+        let reopened = Run::open(quire.db_path(), run1_id, runs.base_dir.clone()).expect("reopen");
+        assert_eq!(reopened.state(), RunState::Superseded);
+        assert!(
+            reopened.read_finished_at().expect("read").is_some(),
+            "superseded run should have finished_at"
+        );
+    }
+
+    #[test]
+    fn create_supersedes_active_run_on_same_ref() {
+        let (_dir, quire) = tmp_quire();
+        let runs = test_runs(&quire);
+
+        // Create and activate first run.
+        let mut run1 = runs.create(&test_meta()).expect("create run1");
+        let run1_id = run1.id().to_string();
+        run1.transition(RunState::Active).expect("to active");
+
+        // Create second run for same (repo, ref).
+        let meta2 = RunMeta {
+            sha: "def456".to_string(),
+            r#ref: "refs/heads/main".to_string(),
+            pushed_at: "2026-04-28T13:00:00Z".parse().unwrap(),
+        };
+        let run2 = runs.create(&meta2).expect("create run2");
+        assert_eq!(run2.state(), RunState::Pending);
+
+        // First run should be superseded.
+        let reopened = Run::open(quire.db_path(), run1_id, runs.base_dir.clone()).expect("reopen");
+        assert_eq!(reopened.state(), RunState::Superseded);
+        assert!(
+            reopened.read_finished_at().expect("read").is_some(),
+            "superseded run should have finished_at"
+        );
+    }
+
+    #[test]
+    fn create_does_not_supersede_different_ref() {
+        let (_dir, quire) = tmp_quire();
+        let runs = test_runs(&quire);
+
+        // Create run for main.
+        let run1 = runs.create(&test_meta()).expect("create run1");
+        let run1_id = run1.id().to_string();
+
+        // Create run for a different ref.
+        let meta2 = RunMeta {
+            sha: "def456".to_string(),
+            r#ref: "refs/heads/feature".to_string(),
+            pushed_at: "2026-04-28T13:00:00Z".parse().unwrap(),
+        };
+        let _run2 = runs.create(&meta2).expect("create run2");
+
+        // First run should still be pending.
+        let reopened = Run::open(quire.db_path(), run1_id, runs.base_dir.clone()).expect("reopen");
+        assert_eq!(reopened.state(), RunState::Pending);
+    }
+
+    #[test]
+    fn create_does_not_supersede_complete_or_failed_runs() {
+        let (_dir, quire) = tmp_quire();
+        let runs = test_runs(&quire);
+
+        // Create and complete first run.
+        let mut run1 = runs.create(&test_meta()).expect("create run1");
+        let run1_id = run1.id().to_string();
+        run1.transition(RunState::Active).expect("to active");
+        run1.transition(RunState::Complete).expect("to complete");
+
+        // Create second run for same (repo, ref).
+        let meta2 = RunMeta {
+            sha: "def456".to_string(),
+            r#ref: "refs/heads/main".to_string(),
+            pushed_at: "2026-04-28T13:00:00Z".parse().unwrap(),
+        };
+        let _run2 = runs.create(&meta2).expect("create run2");
+
+        // First run should still be complete.
+        let reopened = Run::open(quire.db_path(), run1_id, runs.base_dir.clone()).expect("reopen");
+        assert_eq!(reopened.state(), RunState::Complete);
+    }
+
+    #[test]
+    fn transition_allows_pending_to_superseded() {
+        let (_dir, quire) = tmp_quire();
+        let runs = test_runs(&quire);
+        let mut run = runs.create(&test_meta()).expect("create");
+        run.transition(RunState::Superseded).expect("to superseded");
+        assert_eq!(run.state(), RunState::Superseded);
+    }
+
+    #[test]
+    fn transition_allows_active_to_superseded() {
+        let (_dir, quire) = tmp_quire();
+        let runs = test_runs(&quire);
+        let mut run = runs.create(&test_meta()).expect("create");
+        run.transition(RunState::Active).expect("to active");
+        run.transition(RunState::Superseded).expect("to superseded");
+        assert_eq!(run.state(), RunState::Superseded);
+    }
+
+    #[test]
+    fn supersede_sets_finished_at() {
+        let (_dir, quire) = tmp_quire();
+        let runs = test_runs(&quire);
+        let mut run = runs.create(&test_meta()).expect("create");
+        run.transition(RunState::Active).expect("to active");
+
+        assert!(
+            run.read_finished_at().expect("read").is_none(),
+            "should not have finished_at before supersede"
+        );
+
+        run.transition(RunState::Superseded).expect("to superseded");
+        assert!(
+            run.read_finished_at().expect("read").is_some(),
+            "superseded run should have finished_at"
+        );
+    }
 }