rebase: merge main (#66, #61, #69) into fix/tui-noise-and-keychain-prompts - #70
Merged
Conversation
…that survives restart Roadmap Phase 0 — runtime truth and evidence durability. TaskGraph now records a versioned TaskAttempt per try of a task: parent task and attempt, retry lineage, role, assignee, model decision, workspace requirement, base revision, the authority the task held when the attempt started, reservation, usage, lease epoch, terminal reason, result, and evidence references. It is written to the session event stream, so a restarted process rebuilds lineage rather than re-deriving execution from whatever configuration is loaded later. Graph commands are now closures over the projection, rebuilt and rechecked after every optimistic-store catch-up, and all state is applied through replay. Child budget is reserved atomically with the child's creation and settled at its terminal event, returning unspent capacity to the parent, so concurrent reservations cannot exceed the parent's remainder. A result from an attempt that is no longer current, or whose lease epoch moved, is refused instead of overwriting the retry that replaced it. Execution `completed` and evidence `verified` are separate states, replay is paged, and `narrow_authority` re-derives a recovered task's grants against current policy, narrowing only. Validation records move to the session stream through a new `arsy_kernel::validation`: each carries the evidence artifact, a digest of the command computed here rather than supplied, the operation, the task and attempt, the grants the call held, and the workspace revision it ran against. `validate.status` answers `unvalidated`, `passed`, `failed`, or `stale` for the revision it is asked about, and says whether the log is durable. Workspace revision is a digest of HEAD and the porcelain status, so an edit made after a check passed makes that evidence stale. `plan.commit` is the explicit transition from the scratch plan to durable commitments: each uncommitted step becomes one TODO, once, with the TODO id recorded against the step. A turn with no session stream is offered no commit at all. Two pre-existing tests depended on their surroundings rather than on the code under test, and are made hermetic: the card test pins the half-block mark instead of inheriting whichever mark the running terminal produces, and the follow-up test queues its keystrokes before the turn instead of racing a sleep against the provider.
TaskRun resolves its provider once at open() and keeps it for the whole task (documented tradeoff in open_route's own comment), so a token that expires partway through a scripted run was never re-checked — dispatch just failed with the raw provider auth error. dispatch_with_refresh wraps dispatch(): on ProviderError::Auth from an OAuth-sourced credential, it re-resolves the same endpoint (exercising provider::stored's existing refresh-and-write-back path) and retries the identical request once, rebuilding the delegation supervisor fresh — safe, because an auth failure happens on the very first model call, before any delegation could have happened yet to lose. An API key error, or a refresh that itself fails, surfaces exactly as before. should_refresh_and_retry is the whole decision extracted to a pure, unit-tested predicate: retry only an Auth failure, only when the credential source is OAuth. Scope note: this covers arsy run (TaskRun), which re-resolves once per invocation, so the exercised window is a token expiring mid-invocation. It does not yet cover the interactive TUI session, which resolves its provider once per session (native_status/run_turn) — that is a larger change (Turn/Streamed need to carry the typed error, and the session-level Resolved needs to be mutable in place) tracked as a follow-up.
… TUI Bagian 2 (of 2): the interactive session resolves its provider once per route and caches it (resolve_route's HashMap), unlike arsy run which resolves fresh per invocation. A token that expires between turns of a long session was never re-checked, and every turn after that failed with the raw "OAuth access token has expired" provider error. The channel spawn_stream/streamed hand events across to the render thread on now carries the typed ProviderError instead of a stringified message, so Turn gains a `provider_error` field alongside the existing display-only `failure: Option<String>` (which two other producers — the round-limit and external Codex-CLI paths — still populate with a plain message that is not a ProviderError at all). native_status_with_refresh wraps native_status the same way dispatch_with_refresh wraps dispatch: on a stale OAuth token it re-resolves the same endpoint and repeats the identical round once. Because resolve_route hands out `&mut Resolved` into its cache now (get_mut instead of get), the refreshed token is written back into the session's own cache as a side effect of the retry, so turns after this one benefit too, not just this one. should_refresh_and_retry is renamed is_stale_oauth_token and now takes the error directly, shared by both dispatch_with_refresh (arsy run) and native_status_with_refresh (interactive session). Threading &mut through required run_turn, take_turn, native_turn, and resolve_route's Option<&provider::Resolved> to become Option<&mut provider::Resolved>; charge_turn and the dispatch match in run_turn use as_deref()/as_deref_mut() to reborrow without moving the option out from under the later charge_turn call. Scripted (the test fake ModelProvider for native_turn) gained a fail_first queue so a test can make one stream() call fail before falling through to its scripted rounds; resolved_failing_first builds one pre-set to OAuth-sourced. The new regression test proves the failure-and-refresh-attempt path deterministically (the stub endpoint cannot really be re-resolved, so refresh fails and the original failure surfaces, with no second stream call) — the same shape of guarantee Bagian 1's test gives for arsy run.
… tool call Anthropic's content-block index is shared across every block type in a message, but EventDecoder.blocks only grew when a tool_use block started, so its length diverged from index the moment any other block type (text, thinking) came first. block_start's own order check (blocks.len() != index) then rejected the very next tool_use block as out of order, aborting the whole stream mid-turn with ARSY-PRV-1000 provider response undecodable: content block N started out of order blocks is now a HashMap<usize, ToolBlock> keyed directly by the wire index instead of a densely-packed Vec, so a tool_use block's position in the block list no longer has to match its position among tool_use blocks specifically.
When a picker offers rows (model, session, effort), Up/Down keys should always navigate the offered menu, not fall through to history navigation. Previously, when a picker applied a filter that narrowed matches to zero, the navigate() function would see !self.menu().is_empty() as false and delegate Up/Down to history recall instead. This was especially visible with model selection where typing filtered the list. Now, when self.offered is Some, Up/Down always navigate the offered menu, even when filtering narrows matches. This maintains the intended picker behavior and prevents accidental history navigation.
…mid-session fix: refresh a stale OAuth token mid-session instead of failing the turn
fix(cli): prevent model picker from falling back to history navigation
feat(kernel): Phase 0 — runtime truth and evidence durability
…-index-desync fix: text/thinking block before a tool call is misread as an out-of-order block
main gained three features after this branch was cut, all of them inside functions this refactor relocated out of lib.rs: - OAuth token mid-session refresh (PR suiflex#66): dispatch_with_refresh, native_status_with_refresh, is_stale_oauth_token, and the &mut Resolved threading through run_turn/native_turn/resolve_route/ take_turn now live in run.rs, turn.rs, and picker/prompt.rs. TaskRun no longer holds &Invocation here, so the refresh re-resolves by resolved.endpoint.id directly rather than the CLI's --provider flag — the same endpoint either way, one field access instead of a stored reference. - Durable task attempts and evidence (PR suiflex#61, feat/phase0-runtime-truth): TaskNode.runtime and the agent_runtime lineage parameter, both auto-merged into orchestration.rs and most call sites; two call sites this refactor's new files reintroduced (run.rs enqueue/execute, turn.rs record_turn/run_turn) needed the same wiring by hand. - content_block_start index desync fix (PR suiflex#69): auto-merged clean, kernel-only, no overlap with this refactor. Verified: cargo build/clippy(-D warnings)/fmt/test --workspace --all-features all clean, 680 tests passing, including the token- refresh, tool-block-order, and flaky-follow-up regression tests.
6 tasks
mulhamna
approved these changes
Sep 20, 2026
mulhamna
merged commit Sep 20, 2026
21ad078
into
suiflex:fix/tui-noise-and-keychain-prompts
15 checks passed
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.
Why
fix/tui-noise-and-keychain-promptspredates three merges tomainthat all land inside functions this branch relocated out oflib.rsintorun.rs/turn.rs/picker/prompt.rs(run_turn,native_turn,native_status,resolve_route,dispatch,agent_runtimecall sites,TaskNodeliterals):feat/phase0-runtime-truth) — durable task attempts / evidencecontent_block_startindex desync fix (kernel-only, no actual overlap, merges clean)A plain
git merge origin/mainhere produces real conflicts inlib.rs,code.rs, andextensions.rs— not just noise, since the functions those PRs touched no longer exist at their old location inlib.rs.What this branch does
Merges
origin/mainin and manually re-ports what the merge alone can't carry across the refactor:dispatch_with_refresh/is_stale_oauth_token→run.rs, wired intoTaskRun::executein place of the baredispatchcall. SinceTaskRunhere no longer holds&Invocation, the refresh re-resolves byresolved.endpoint.iddirectly instead of the CLI's--providerflag — same endpoint either way.native_status_with_refresh,Turn.provider_error,spawn_stream/streamed(String→ProviderError) →turn.rs, and&mut provider::Resolvedthreaded throughrun_turn/native_turn/resolve_route/take_turn(was&Resolved).TaskNode.runtime+agent_runtime'slineageparameter — auto-merged almost everywhere; two call sites in the new files (run.rs'senqueue/execute,turn.rs'srecord_turn/run_turn) needed the same wiring added by hand since they're new code this branch introduced afterfeat/phase0-runtime-truthbranched.#69's kernel-only fix merges clean, no action needed.Verified
cargo build --workspace --all-featurescargo clippy --workspace --all-targets --all-features -- -D warnings— cleancargo fmt --all -- --check— cleancargo test --workspace --all-features— 680 tests, 0 failures, including the token-refresh regression test, the tool-block-order regression test, and the flaky follow-up-typed-mid-turn testfeat/phase0-runtime-truthfixedarsy --help)Feel free to squash/take only the parts you want — this is meant as a rebase reference, not a prescriptive diff.