Skip to content

refactor(session): let the host own the running turn and its requests - #556

Merged
Tryanks merged 1 commit into
mainfrom
refactor/host-owned-live-turn
Sep 30, 2026
Merged

Tryanks merged 1 commit into
mainfrom
refactor/host-owned-live-turn

Conversation

@Tryanks

@Tryanks Tryanks commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Behaviour

  • Opening a thread whose agent is waiting on a question (most often on a phone after reconnecting) shows the question, whichever of the status and history arrives first.
  • Opening a running thread no longer downloads the whole running turn. fix(chat): keep the working indicator on a thread opened mid-turn #554 loaded history pages back-to-back until the turn's start arrived. Estimated from local logs, the largest turn in 155 of the 300 biggest threads exceeds 2 MB on the wire, and in 13 it exceeds 10 MB.
  • A status change no longer refolds the held history.

Cause

A client folds a byte-bounded window of the log, and it decided liveness itself: it called mark_idle unless the status said the turn ran. A fold settled by a stale cached status discarded the running turn and its pending question and approvals. #554 revived them with a refold on every running flip, and timed a window cut inside the turn by paging back to its start.

A window fold can place live state but cannot decide it. The window may start after the turn or its requests opened, and a provider can stop without a closing record (user shutdown, orchestrator cancel, host restart). The host already holds the complete fold and knows whether its provider is live.

Change

  • Protocol (version 7). SessionStatus carries running_turn (RunningTurn { turn, started_at }, the turn's index among every turn in the whole log), plus pending_approvals and pending_user_input with their content. These replace the pending_approval and pending_user_input flags.
  • Host. session_status_snapshot derives these from the resident timeline and approval registry. open_requests holds the one rule that requests count only while a turn is in flight, for both the status and the index activity summary. The rule matches what the client did before: fold requests AND turn_running. has_approval lost its last production caller and is removed.
  • Core.
    • Timeline::running_turn derives the value on the host.
    • Timeline::settle_running_turn marks which turn runs and fills its start. It discards nothing, so a later settle can revive the turn.
    • Timeline::shown_proposed_plan hides a streamed, unready plan while its turn is not running, instead of mark_idle discarding it.
  • Client.
    • fold_held_records is a pure fold.
    • The snapshot and history-page handlers settle the running turn after updating session_turn_offset.
    • A status change settles the replica in place.
    • The composer reads the question and approvals from the status.
    • reconcile_replica_liveness and running_turn_opening_unheld are removed.

Tests

  • the_status_settles_the_running_turn_whichever_topic_arrives_first (replaces fix(chat): keep the working indicator on a thread opened mid-turn #554's status test). For both arrival orders, the turn runs from the host's start and the question shows. A status whose running turn is not yet held marks no held turn. An idle status hides the question that the fold still holds.
  • a_window_cut_inside_the_running_turn_is_timed_from_the_host (replaces fix(chat): keep the working indicator on a thread opened mid-turn #554's paging test). A cut window runs from the status start, and no history page is requested.
  • status_carries_the_running_turn_and_its_question_only_while_in_flight (runtime). Checks the turn index and start across two turns. After a shutdown without a record, the timeline still runs but the status reports neither the turn nor the question.
  • proposed_plan_lifecycle… (core, extended). A settled-idle fold hides the streamed plan and a settle to running revives it. A final plan stays shown.
  • Fixture updates:
    • The approval-authority and async-question fixtures now have their turn in flight, as real requests do.
    • The async-question fixture delivers its events through the runtime handler, using a provider_event_for_test wrapper next to record_event_for_replica_test.

Mutation checks: reading the question from the fold, or skipping the in-place settle on a status change, each fails the client test.

Checks run

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo nextest run --workspace --locked: 955 passed
  • cargo machete
  • cargo check -p tcode-web --target wasm32-unknown-unknown and -p tcode-ios --target aarch64-apple-ios-sim, both with RUSTFLAGS=-D warnings

The Android check is left to CI (no NDK locally). Not exercised on a real phone. Clients and hosts must both be on protocol 7.

A client folds a byte-bounded window of a session's log and used to decide
liveness itself: mark_idle unless the status said the turn ran. That lost
the pending question and the working indicator whenever a stale cached
status settled the fold, and #554 patched it with a refold on every
running flip plus back-to-back history pages until the running turn's
start arrived, downloading the whole running turn (often several MB).

The host already holds the complete fold and knows whether its provider
is live, so SessionStatus now carries the running turn (its index in the
whole log and start time), the pending approvals and the pending
question, gated on an in-flight turn. The client folds held records
purely and settles which held turn runs from the status, in place and
without discarding anything, whenever either topic changes. An unready
plan is hidden while its turn is not running instead of being discarded.

PROTOCOL_VERSION 7.
@Tryanks
Tryanks merged commit 3f04ec0 into main Sep 30, 2026
13 of 14 checks passed
@Tryanks
Tryanks deleted the refactor/host-owned-live-turn branch September 30, 2026 12:31
yermakoffivan pushed a commit to yermakoffivan/tcode that referenced this pull request Sep 30, 2026
The host now owns a running turn's requests (Tryanks#556), so a request pushed
straight into the timeline no longer reaches the status the composer reads.
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