[AI-7065] Refresh flow resume state in the Togo TUI without restarting the command - #24701
Open
luisorofino wants to merge 3 commits into
Open
[AI-7065] Refresh flow resume state in the Togo TUI without restarting the command#24701luisorofino wants to merge 3 commits into
luisorofino wants to merge 3 commits into
Conversation
Contributor
Author
|
@codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 6c226de | Docs | Datadog PR Page | Give us feedback! |
luisorofino
force-pushed
the
loa/refresh-flow-resume
branch
from
July 28, 2026 11:58
6dca3f5 to
5b2a117
Compare
luisorofino
marked this pull request as ready for review
July 28, 2026 12:01
Contributor
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
3 tasks
Resume availability is derived from the run's checkpoint file, so the data on disk is always accurate. The TUI only read it while first building a screen, which left the display stale for the rest of the session: a flow that failed mid-session kept showing no resumable label on its tile, and returning to the flow screen did not reveal the Resume button until the screen was rebuilt from the grid. Recompute on ScreenResume, which Textual posts to a screen that becomes active again. FlowCard.resumable is now reactive so MainScreen can reassign it in place without recomposing the grid, preserving focus and scroll. Both the tile label and the Resume button now follow the state in both directions, so they also disappear once a run completes. Collapse the resume-state API onto one entry point while here. Reading it took three checkpoint reads per flow screen and left the "is this resumable?" policy in the TUI, duplicating orchestration semantics. CheckpointManager.resume_state now reads once and returns a ResumeState of completed and frontier phases; is_resumable is simply a non-empty frontier, since a flow that never ran has nothing to start from. That replaces has_resumable_run, resume_completed_phases, and resolve_resume_state, and drops the runtime module's import of the config error type. Two consequences worth noting: - Resuming with nothing left to resume is now refused instead of silently doing something else. Previously, no checkpoints ran every phase as a plain launch would, and an already-finished run skipped every phase and reported instant success without doing any work. - read() raised an unhandled AttributeError on a checkpoint file holding valid YAML that is not a mapping. The TUI's bare except had been masking it; it now raises CheckpointReadError like every other unreadable file. Follow-up in AI-7066: interrupting a run before any phase completes writes no checkpoint at all, so the interrupted run cannot be told apart from one that never started.
luisorofino
force-pushed
the
loa/refresh-flow-resume
branch
from
July 30, 2026 13:19
5b2a117 to
d758fa7
Compare
lucia-sb
requested changes
Aug 5, 2026
Addresses review feedback on the resume-state refactor. Distinguish "nothing recorded" from "could not be read". `ResumeState` gains an `error` field. `CheckpointManager.resume_state` still raises, so the orchestrator keeps its actionable message; `flow_resume_state` converts the `CheckpointReadError` into `error` rather than discarding it, so one corrupt file still cannot crash a grid of flows but no longer reads as a clean slate. `FlowCard` renders a third footer state for it, making the remedy discoverable from the grid instead of unreachable. Re-check before committing to a resume. `_do_resume` validated nothing beyond `values is not None`, and `Screen.dismiss` invokes the result callback before popping, so the push happened a tick ahead of the `ScreenResume` refresh: the app computed the correct answer immediately after acting on the stale one. The user landed on an execution screen of phases that would never run, with the orchestrator's `ConfigError` wrapped as an `on_initialize` hook failure. Both `_do_resume` and its dismiss callback now confirm resumability, resync the button, and report inline. The orchestrator check stays as the CLI backstop. Drop the dead `on_mount`. Textual posts `ScreenResume` on push as well as pop, so both handlers fired on every open and `checkpoints.yaml` was read twice per screen. `on_screen_resume` alone covers "screen becomes active", which also removes the two-handlers-must-agree hazard and matches `MainScreen`. `FlowCard` carries one `resume_state` reactive rather than parallel resumable and error flags that would have to be kept in sync.
lucia-sb
requested changes
Aug 7, 2026
…kpoint
`Screen.notify` defaults to `markup=True` and `Toast.render` passes the message
through `Content.from_markup`, so embedding raw exception text made the toast
content attacker-shaped by accident. A checkpoint file that is valid YAML but an
invalid checkpoint entry — schema drift between ddev versions, or a partial
write — yields pydantic text containing `[type=missing, input_value={...},
input_type=dict]`. A bare word before `=` inside brackets is not valid markup, so
`MarkupError` escapes the compositor reflow and takes the app down. In the worst
case the commit that added a graceful refusal turned a survivable error into a
hard crash. Passing `markup=False` holds whatever the exception happens to
contain; compacting the text does not, since it only works while the offending
brackets stay off the first line.
The composed sentence was also malformed. The join assumed trailing punctuation,
but only one of the three `CheckpointReadError` shapes has it and the other two
are multi-line, so the remedy landed after a YAML caret or a pydantic URL —
eight and thirteen lines respectively.
`ExecutionScreen._compact_error_detail` already reduced multi-line error text to
one truncated line, so it moves to `tui/errors.py` as `compact_error_detail` over
`str` rather than growing a second copy. The execution screen keeps its
phase-prefix trimming and strips those prefixes before compacting, as before, so
a long message loses the same characters it used to.
`App.run_test` takes `notifications=False`, which is why no existing test caught
this: toasts are never mounted, so an unrenderable message raises nothing. The
two regression tests pass `notifications=True` and render for real.
luisorofino
force-pushed
the
loa/refresh-flow-resume
branch
from
August 7, 2026 09:51
a8ef01d to
6c226de
Compare
Contributor
Validation ReportAll 21 validations passed. Show details
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What does this PR do?
Recomputes a flow's resume state whenever a TUI screen becomes active again, and collapses the resume-state API onto a single entry point.
Refresh on
ScreenResumeFlowScreenreads and applies resume state fromon_screen_resume, the event Textual posts to a screen that becomes active. Textual posts it onpush_screenas well aspop_screen, so one handler covers both the first open and every return — there is noon_mountcounterpart to keep in sync, and no duplicate read per open.FlowCardholds the resume state in a singleresume_statereactive rather than parallel display flags, withresumablederived from it.display = True, so a stale button survived a completed run.One way to read resume state
CheckpointManager.resume_state(resolved_flow)reads the checkpoint file once and returns aResumeStateofcompletedandfrontierphases.is_resumableis simply a non-empty frontier: a flow that never ran has nothing to start from, so resuming it is just a launch.has_resumable_run,resume_completed_phases, and the module-levelresolve_resume_state.runs.pykeeps only its real job — locating run directories — behind oneflow_resume_state(flow, runs_dir).ConfigErrortranslation moves toPhaseOrchestrator, where the "delete it and restart" advice is actionable. The runtime module no longer imports the config error type.A corrupt checkpoint is reported, not hidden
ResumeStatecarries anerror, so "nothing was recorded" and "the recording could not be read" stop collapsing onto the same value.resume_state()still raisesCheckpointReadErrorand the orchestrator keeps its precise message;flow_resume_stateconverts it instead of discarding it, so one corrupt file still cannot crash a grid of flows.FlowCardrenders a third footer state,✕ checkpoint unreadable. Previously a corrupt file was indistinguishable from a flow that had never run, which left the remedy undiscoverable from the TUI.Behaviour changes
PhaseOrchestratorvalidatesis_resumable— the same property the TUI uses — and raises with a reason distinguishing the two cases.Screen.dismissinvokes the result callback before popping, so the launch modal's dismiss commits a tick ahead of theScreenResumerefresh — the app would compute the correct answer immediately after acting on the stale one._do_resumere-checks both before opening the modal and in the dismiss callback, resyncs the button, and reports why. The orchestrator check remains the CLI backstop.CheckpointManager.read()raised an unhandledAttributeErroron a checkpoint file holding valid YAML that is not a mapping. The TUI's bareexcept Exceptionhad been masking it; narrowing that handler toCheckpointReadErrorsurfaced it.read()now raisesCheckpointReadErrorlike it does for every other unreadable file, so a corrupt file can no longer crash the flow grid on startup.Motivation
Whether a flow can be resumed is derived from its checkpoint file, so the data on disk is always correct. The TUI only read it while first building a screen, which left the display stale for the rest of the session:
MainScreenis never popped.FlowScreenis resumed rather than re-mounted. The user had to navigate back to the grid and re-enter the flow so a fresh screen was built.The result was confusing rather than broken — the resume genuinely worked, nothing in the UI said so.
Follow-up in AI-7066: interrupting a run before any phase completes writes no checkpoint at all, so an interrupted run cannot be told apart from one that never started. Telling those apart needs a run marker written at start; it is not recoverable from what is on disk today.
Review checklist (to be filled by reviewers)
qa/requiredif this PR needs QA validation, orqa/skip-qaif it does not. Exactly one of the two is required.backport/<branch-name>label to the PR and it will automatically open a backport PR once this one is merged