loops: parse COMPLETED: INT-XX markers to advance loop pointer (option b)
ci / gates (push) Successful in 11s
ci / frontend (push) Successful in 26s
ci / rust (push) Failing after 56s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Closes the loop bridge — the missing piece from the previous commit.
Without this, `current_int_index` stayed at 0 forever and every
iteration re-worked INT-01. Now the topology_worker's completion hook
parses the run's final output for `COMPLETED: INT-<NN>` markers and
atomically advances the loop's consumed_int_ids + current_int_index.

Backend:
- topology_worker::advance_loop_after_completion — new post-terminal
  hook that fires alongside freeze_research_outcome. Reads loop_id_for_run
  (skips non-loop runs) + source_research_context (skips standalone
  loops without a bound source topic).
- parse_completed_int_ids — forgiving parser: matches `COMPLETED: INT-01`,
  `- COMPLETED: `INT-01``, `COMPLETED: INT-01, INT-02`, case-insensitive,
  tolerates list dashes / backticks / markdown emphasis. De-dupes within
  a single output.
- cm_db::repo::loops::advance_after_completion — atomic UPDATE that:
  · appends only NEW ids to consumed_int_ids (idempotent on re-runs)
  · bumps current_int_index by the count of new ids landed
  Set semantics via `SELECT DISTINCT unnest(...)` so ordering-based
  bugs can't accumulate duplicates.

Behavior end-to-end:
1. Loop wizard imports an integrations artifact (previous commit).
2. run_now / webhook_receive → compose_iteration_task prepends artifact
   + focus instruction ("address INT-<current+1>, log COMPLETED at end").
3. Coordinator run does the work, emits `COMPLETED: INT-<NN>`.
4. topology_worker completion hook parses the marker, advances the
   loop, and the NEXT iteration sees an updated `consumed:` list +
   incremented `current_int_index` in its focus instruction.

Follow-ups still queued:
- Loop card refresh button — pull latest artifact after reject-with-
  revision on the source topic (right now the prepend uses the LATEST
  outcome automatically, so refresh is UX only, not correctness).
- Reorder rationale extraction — coordinator emits "REORDER: INT-05
  before INT-04 because prereq X is unmet"; today that's just prose
  in the output, not indexed.
This commit is contained in:
Omar Sobh
2026-07-09 18:48:34 -07:00
parent ce73abe5ab
commit 4a140cb7db
2 changed files with 100 additions and 0 deletions
+36
View File
@@ -247,6 +247,42 @@ pub async fn set_zeroclaw_container(
Ok(())
}
/// Atomically append `completed` INT-XX ids to the loop's
/// `consumed_int_ids` array and bump `current_int_index` by the count
/// of NEW ids landed. Existing ids are not re-appended (idempotent on
/// re-runs). Called from the topology_worker completion hook after
/// parsing "COMPLETED: INT-XX" markers out of the run's final output.
pub async fn advance_after_completion(
pool: &PgPool,
loop_id: Uuid,
completed: &[String],
) -> Result<(), DbError> {
if completed.is_empty() {
return Ok(());
}
// Use array set semantics: append only ids not already present.
// The subquery computes the new list; length delta feeds the index bump.
sqlx::query(
"UPDATE loops
SET consumed_int_ids = (
SELECT ARRAY(
SELECT DISTINCT unnest(consumed_int_ids || $2::TEXT[])
)
),
current_int_index = current_int_index + (
SELECT count(*) FROM unnest($2::TEXT[]) AS n(v)
WHERE NOT (consumed_int_ids @> ARRAY[v])
),
updated_at = now()
WHERE id = $1",
)
.bind(loop_id)
.bind(completed)
.execute(pool)
.await?;
Ok(())
}
/// Bind (or unbind) a loop's source research topic. When set, the loop's
/// enqueue path prepends the topic's latest artifact + a "focus on the
/// next unconsumed INT" instruction to the coordinator task (option b,