loops: reorder rationale extraction — REORDER: markers logged per iteration
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 27s
ci / rust (push) Failing after 43s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Coordinator can now log WHY it worked on an INT-XX out of order
("REORDER: INT-05 before INT-04 because prereq X is unmet") and the
completion hook captures each rationale as an append-only event on
the loop. Sets up a reviewable timeline of when the plan was
adjusted, independent of the underlying `consumed_int_ids` advance.

Migration 0043:
- loops.reorder_events JSONB NOT NULL DEFAULT '[]'::jsonb — append-
  only array of {run_id, iteration, text, ts}. Kept on the loop row
  (rather than a dedicated table) so the mini-timeline is one read
  away from the loop card.

Backend:
- topology_worker::parse_reorder_rationale — line matcher symmetric
  with parse_completed_int_ids. Tolerates list dashes / prefixes /
  markdown emphasis; case-insensitive marker match, preserves case of
  the rationale text.
- cm_db::repo::loops::append_reorder_event — one INSERT-like append
  per rationale, uses jsonb_build_object with postgres now() so ts is
  wall-clock canonical (no client-clock skew).
- topology_runs::iteration_for_run — new helper so events carry the
  iteration index.
- routes::loops::compose_iteration_task — coordinator prompt now
  explicitly asks for `REORDER: <one-sentence>` at the top of the
  first substantive turn when working out of order, AND spells out
  that both markers must appear literally with colons (no bold, no
  code fence) so the line parser doesn't miss them.

Non-loop and standalone-loop runs are unaffected — the hook only
fires when the run belongs to a source-bound loop.

Follow-up: expose reorder_events on the loops list endpoint + render
a small collapsed timeline on the LoopsList card.
This commit is contained in:
Omar Sobh
2026-07-09 18:58:55 -07:00
parent a34be33261
commit 0c17de52dd
5 changed files with 121 additions and 7 deletions
+13
View File
@@ -194,6 +194,19 @@ pub async fn loop_id_for_run(pool: &PgPool, id: Uuid) -> Result<Option<Uuid>, Db
Ok(row.and_then(|r| r.try_get::<Option<Uuid>, _>("loop_id").ok().flatten()))
}
/// The iteration counter for a loop-bound run. Returns None for chat /
/// research runs (iteration column is nullable). Used by the reorder
/// rationale hook so the mini-timeline can order events by iteration.
pub async fn iteration_for_run(pool: &PgPool, id: Uuid) -> Result<Option<i32>, DbError> {
use sqlx::Row;
let row: Option<sqlx::postgres::PgRow> =
sqlx::query("SELECT iteration FROM topology_runs WHERE id = $1")
.bind(id)
.fetch_optional(pool)
.await?;
Ok(row.and_then(|r| r.try_get::<Option<i32>, _>("iteration").ok().flatten()))
}
/// Enqueue a durable run bound to a research topic. `research_topic_id` is
/// stored so `notify_run_completed` can flip the owning topic
/// `processing → reviewing` when its last run terminates (see