Skip to content

feat(api)!: list runs by stack - #366

Open
aaaaahaaaaa wants to merge 1 commit into
mainfrom
feat/retry-surfaces
Open

aaaaahaaaaa wants to merge 1 commit into
mainfrom
feat/retry-surfaces

Conversation

@aaaaahaaaaa

Copy link
Copy Markdown
Contributor

What

Phase 3 of the retry system, and the last: a stack reads as one piece of work in the API and the app, so a workload that eventually succeeded looks like a success.

Implements the retry spec section 9, following the phase 3 plan. Builds on #364 (core) and #365 (platform).

The rule

A listing shows each stack's latest attempt, and every filter reads that attempt. A stack whose first attempt failed and whose second succeeded is a success, which is what a reader means by "failed runs". ?root_run_id= asks for one stack's attempts instead, newest first.

In the app: a row shows how many attempts its work took, the run page walks the stack's attempts, and the docs gain a Retrying section in the execution guide and a Retries section in the hooks guide.

For the reviewer

Three decisions that differ from the plan, each for a reason worth checking:

No attempts field on RunResponse. The plan had one. A listing row is the stack's latest attempt, and attempts are consecutive, so attempt already says how many the stack took. A second field would duplicate it in a listing and lie when an older attempt is fetched by id. Dropped rather than shipped as a near-synonym.

The latest-attempt narrowing is a grouped join, not DISTINCT ON. DISTINCT ON is Postgres-only and the store tests run on SQLite, so this reuses the idiom _advance_backfill already established. It is also scoped to the organisation alone, deliberately: every attempt of a stack shares its target, its backfill and its org, so no other filter can change which attempt is latest. Narrowing by the caller's filters would quietly answer a different question, "the latest failed attempt" rather than "the stacks whose latest attempt failed".

The realtime path needed fixing, which the plan missed. A new attempt arriving over the realtime subscription would have been appended beside its predecessor, showing both attempts of one stack until the next refetch and breaking the invariant the listing promises. _upsert now supersedes the earlier attempt's row.

One existing API test needed its SimpleNamespace run stub extended with the two new fields.

Not verified

The UI is typechecked and linted but not visually confirmed. The plan's live step, standing up a seeded instance on :3100 to see the attempt chip and the stack switcher render, was not run: it goes against the shared dev database and needs a login session. The changes are small (a conditional chip in one cell renderer, a conditional link list in the run page header), but they have not been seen in a browser.

Verification

uv run ruff check, uv run ty check, uv run pytest: 2914 passed, 9 skipped. Frontend: pnpm exec nuxt typecheck exits 0, pnpm run lint reports 0 errors (4 pre-existing warnings).

After this

The retry system is complete end to end: operations retry in place, runs retry as stacks, backfills and hooks report the stack's verdict, and the surfaces read it as one piece of work. Nothing retries until a component declares a policy, since there is deliberately no instance-wide default; that, and per-attempt spans in traces, are recorded as follow-ups in the spec.

By Digitl

A stack is one piece of work, so a listing shows its latest attempt and every
filter reads that attempt: a stack whose first attempt failed and whose second
succeeded is a success, which is what a reader means by "failed runs". Passing
`root_run_id` asks for one stack's attempts instead, newest first.

The narrowing is a grouped join rather than `DISTINCT ON`, so it runs on
SQLite as well as Postgres, and it is scoped to the organisation alone: every
attempt of a stack shares its target, its backfill and its org, so no other
filter can change which attempt is the latest. Narrowing by the caller's
filters would answer a different question, "the latest failed attempt" rather
than "the stacks whose latest attempt failed".

`RunResponse` gains `root_run_id` and `scheduled_for`. It gains no attempt
count: a listing row is the stack's latest attempt, so `attempt` already is
how many the stack took, and a second field would only duplicate it, or lie
when an older attempt is fetched by id.

In the app a row shows how many attempts its work took, the run page walks the
stack's attempts, and the realtime path supersedes an earlier attempt's row
instead of appending beside it, which would have broken the one-row-per-stack
invariant the listing promises.

By Digitl
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant