Skip to content

feat: make settled threads a lifecycle state - #651

Merged
Tryanks merged 11 commits into
mainfrom
feat/settled-state
Oct 8, 2026
Merged

Tryanks merged 11 commits into
mainfrom
feat/settled-state

Conversation

@Tryanks

@Tryanks Tryanks commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

accepted send / steer / callback ─ admission: clear Settled|Active (meta write), stamp unsettled_at
  queued human work, live run, approval/blocking input, provider background tasks,
  pending scheduled wake, unsettled dispatched child ─ resident blockers (in memory)
canonical log ─ ThreadActivity: last message (any origin) / human message / run start / run end, failed
  resident: folded as records are written     cold: folded from the log tail (last clock-moving page)
host sweep: startup → drain + 60 s; settings change → full sweep
            TurnCompleted / SessionClosed / ProviderStartFailed / BackgroundTasksChanged{0} → that thread only
settle (manual, synchronous; or automatic at last activity)
  target only → detach its provider to idle (pump drops a detached provider's queued events)
  → cancel automatic queue → close terminals: mark at settle, fresh inspection, still settled & mark unchanged
sidebar: Active → Settled shelf (settled_at, then updated_at; 10, then +25; viewed row kept)
  Settle / Un-settle hover · Archive → acknowledged → 5 s Undo (mod+z)

Closes #526. Phase B1 of #535; removes idle auto-archive (#515).

Settled is a lifecycle override with a durable per-thread auto-settle switch, global and per-project inactivity settings (1–90 days or never, validated once in SettingsPatch::apply), and a Failed chip from SessionActivity.failed. Explicit archive and orchestrate archive_on_complete keep their behavior. Wire notes are recorded without a version bump.

Deferred to #532's follow-up PR: the PR-terminal rule and the two-minute adoption grace (upstream ThreadSettlementService.ts:86–104), which only matters for PR-driven settlement because inactivity needs at least a day. auto_settle_on_merge is stored, defaults on, and its row stays hidden. Pins (and settle clearing a pin) are #528's; Undo of settle is un-settle only.

Known staged gap: until #532 links PRs, an externally open but unlinked PR can inactivity-settle; later discovery does not reopen it.

Idle terminals follow upstream TerminalManager.closeIdle (apps/server/src/terminal/Manager.ts:3131–3172): shell-agnostic, a busy shell builtin is not kept. Inspection is unix-only; Windows and an inspection error keep the terminal open. On macOS a $SHELL of /bin/sh is a shim that execs bash, so its process name never matches and its terminals stay open.

Evidence

Surface Before After
Desktop light sidebar (hover Settle, expanded shelf) Before desktop sidebar Desktop sidebar
Desktop light settings (THREADS, one override) Before desktop settings Desktop settings
Phone light sidebar Before phone sidebar Phone sidebar
Phone light Thread behavior Before phone settings Phone thread behavior

After shots are English, from a fresh synthetic profile (one project, 8 active and 14 settled threads), rendered by the real app window through GPUI's Metal render_to_image with scripted input, because screen capture was not permitted in this session; macOS window chrome (traffic lights) is therefore absent. Also inspected, not attached: the expanded shelf with Show 6 more and after it; a viewed settled row under the collapsed shelf; hover Settle and hover Un-settle; the thread menu's Auto-settle caption; the Undo toast on desktop (outline + ⌘Z, and mod+z undoes) and phone (ghost, no hint); the project rules dialog and its menu; the phone rules page (no dropdown or dialog); dark theme at 720 pt. In the desktop-hosted phone preview the 80 pt traffic-light inset truncates "Thread behavior" (as it truncates "Archived Threads" on main); on a device the title box is 185 pt and the phone rules page is titled with the project name.

Runtime command-boundary tests: legacy settled data and project overrides survive restart, and a cold thread ages from its log tail, not a rename; manual settle is thread-local, refuses queued human work, approvals and blocking input, cancels automatic queue and async questions, and a later send gets the new provider; message origins survive queue acknowledgement and callback steering, and an uncorrelated provider message stores none; a valid child completion reopens only its parent while a stale, archived or deleted one does not; an unsettled dispatched child holds its lead, settling it detaches only its own provider and releases the lead; real macOS PTYs: an idle shell closes with output kept, a running child keeps its shell, input typed after the settle keeps it (passed with $SHELL zsh and bash).

Checks on the final commit:

cargo fmt --all --check            exit 0
cargo clippy --workspace --all-targets --locked -- -D warnings
  Finished `dev` profile [unoptimized + debuginfo] target(s)
cargo nextest run --workspace --locked
  Summary [  59.343s] 1006 tests run: 1006 passed, 13 skipped
cargo machete
  cargo-machete didn't find any unused dependencies in this directory. Good job!

Mobile, Web, Linux and Windows run in CI. No credentialed live-provider run.

Large profile, a copy of a real data directory under a throwaway TCODE_DATA_DIR (debug build, temporary probe outside the diff, copy deleted):

3192 threads, 942 unarchived
index snapshot served at 302 ms after start (projection runs off the host mailbox)
startup sweep: read 4731 ms (cold tail projection of 942 threads), total 4798 ms
periodic sweep: read 1 ms, total 114 ms
first start settled 150 threads; index 636173 → 643970 bytes (settled rows stay in the index)

The earlier full-log fold took 66 s on the same profile. No equivalence to the old archived-index size is claimed: archived rows left the index, settled rows stay.

Test audit: every removed, added or rewritten case and its CONTRIBUTING decision

Steps refer to CONTRIBUTING's "When a test fails, or is met on the way."

Removed:

  • auto_archive_respects_idle_rank_exemptions_and_descendant_cascades, group_sessions_places_children_after_their_parent (core/project.rs); unarchived_thread_survives_the_next_auto_archive_sweep (runtime); launch_runs_the_auto_archive_sweep_across_every_project, project_header_resets_only_its_own_thread_expansion (sidebar): Step 2, idle auto-archive, family ordering and active truncation no longer exist; explicit unarchive and per-project collapse remain covered by their existing cases.
  • settled_commands_persist_without_closing_the_selected_conversation, settling_rejects_busy_descendants_and_accepted_input_reactivates_ancestors (runtime): Step 2, the descendant cascade and ancestor reactivation are removed by the issue; the target-only contract is owned by manual_settlement_is_thread_local_cancels_automatic_queue_and_blocks_human_admission and child_completion_admission_reopens_only_its_parent_and_rejects_stale_targets.
  • thread_navigation_matches_displayed_rows_and_disclosures (sidebar): Step 2, replaced by settled_shelf_pages_without_expanding_for_selection_and_navigation_matches_rows.

Renamed or moved:

  • group_sessions_orders_by_activity → group_sessions_orders_projects_by_activity_or_name (Step 2: the within-project activity order is gone; project ordering kept).
  • orchestrate_send_reactivates_the_child_and_its_settled_parent → orchestrate_send_revives_only_its_archived_child (Step 3: target-only admission; the parent stays settled).
  • compact_families_keep_activity_order_indent_and_collapse → compact_children_keep_indent_and_shared_collapse (Step 1: created-time fixtures drive the specified active order; its SessionActivity fixture gains failed: false).
  • flat_rows_start_reordering_from_their_previous_positions, working_thread_title_stays_inside_row_at_every_sidebar_width (sidebar): moved within the tests module; created-time fixtures, assertions unchanged.

Added: inactivity_settles_at_the_last_activity_unless_blocked (core policy, including provider background work and a pending scheduled wake); runtime message_origins_survive_queue_acknowledgement_and_callback_steering, cold_lifecycle_migrates_legacy_settlement_and_ages_from_events_after_rename, manual_settlement_is_thread_local_cancels_automatic_queue_and_blocks_human_admission, settling_closes_only_idle_real_shells_and_spares_new_terminal_input (macOS), child_completion_admission_reopens_only_its_parent_and_rejects_stale_targets (driven through TurnCompleted, waits with run_until), settling_detaches_only_its_own_provider_and_releases_its_parent; sidebar settled_shelf_pages_without_expanding_for_selection_and_navigation_matches_rows.

Rewritten, kept:

  • older_settings_preserve_access_policy_and_accept_partial_feature_blocks (core/settings.rs): Step 2, the deleted archive defaults are replaced by the settlement defaults an older file now gets (3 days, merge on).
  • version_six_index_visits_output_and_elision_literal_json (protocol): Step 2, the literal wire gains "failed":false in both SessionActivity literals and the failed fixture field; event_envelopes_keep_stored_record_shape_and_optional_request_id: the record fixture gains origin: None.
  • reset_settings_clears_preferences_but_keeps_credentials_installs_and_unknown_keys (runtime): Step 2, the archive preference fixture line and its assertion are removed; reset and security assertions kept.
  • turn_running_for_is_independent_of_active_or_parked_location (runtime): Step 3, background work reports Waiting, not Working.
  • space_index_and_scope_project_only_the_members_projects (runtime): Step 1, the fixture host has auto-settle off so startup settlement of its historical threads cannot interleave index events; assertions unchanged.
  • thread_shortcuts_follow_list_order_and_leave_model_picker_numbers_alone (ui/shell.rs): Steps 1 and 3, mounts the gpui-base Root as run_shell does and extends with hover Settle, a focused input keeping mod+z, and mod+z undoing.
  • compact_thread_distinguishes_host_waiting_activity, compact_menu_dismissal_preserves_selection_and_clears_row_surface, compact_scroll_reuses_model_and_renders_only_viewport, grouped_layout_lays_out_only_the_visible_threads, returning_from_a_thread_reveals_its_row_below_the_top_edge, thread_rows_carry_the_provider_mark_until_the_setting_is_off, compact_recent_orders_projects_and_switches_to_persisted_grouped_view (sidebar): Step 1, fixtures follow the specified active order or menu height; observable assertions kept.
  • editable_settings_wait_for_the_host_and_then_keep_the_user_edit (settings_page), model_picker_focuses_its_search_only_off_phone (shell): Step 2, deleted auto-archive fixture fields removed.
  • Fixture-only origin: None on StoredEvent/SessionEventRecord literals (no assertion change): core/session.rs a_turn_finalized_without_a_timestamp_rejects_later_tool_transitions, streamed_deltas_continue_the_open_turn_not_an_earlier_turn_with_the_same_id, synthetic_entry_ids_do_not_depend_on_how_much_earlier_history_is_folded, the at helpers in session.rs and superseded.rs; runtime session_history_snapshot_pages_and_absolute_tail_cursors, history_snapshot_and_pages_start_at_turn_boundaries, history_byte_budget_preserves_contiguous_records_and_reports_shrinking, history_sends_output_previews_and_reads_whole_outputs_on_request, superseded_turn_changes_cross_without_diffs, an_appended_snapshot_drops_the_superseded_ones_diffs_in_the_same_commit, history_windows_are_byte_budgeted, the tool_call helper, pipe.rs legacy_files_start_process; ui/store merged_windows_visits_and_summaries_apply_as_changes, a_full_remote_device_attributes_only_space_members, the recorded helper. Fixture-only failed: false: the session_status helpers in shell.rs and store/snapshots.rs.

Unchanged from main after this revision: the agent adapter tests (codex steer id included), provider_native_subagent_events_create_and_feed_read_only_mirror_session, mux_clients_target_independent_drafts_and_receive_only_their_session_tail, clients_opening_a_cold_thread_through_the_mux_get_each_record_once, and the composer queue test.

Merge Danger

Door: two-way

Logs and terminal output are kept; Undo never restores providers, watches or exact timestamps. Older binaries ignore the new metadata fields and message origins; rolling back restores idle auto-archive with its default settings.

Blast Radius: lifecycle

On first start the default three-day rule settles every unblocked top-level thread whose last activity is older than three days, including non-resident ones: 150 of 942 on the measured profile. The cold projection reads each unarchived thread's log tail once at startup (about 4.7 s in a debug build on that profile) off the host mailbox; a thread whose log has no message or run records is read whole and never ages. Busy or uninspectable terminals stay open. The PR rule and grace (#532), pins and arrangement (#528), and Agents/roster (#637) are not in this PR.

Tryanks added 11 commits October 8, 2026 15:38
Applies the review of #651: idle terminals are told by an activity mark and a
fresh process inspection instead of shell integration; pending requests are
resident-only; manual settle is synchronous; detach-to-idle serves settle and
archive only; admission and run dispatch records, the message admission id,
activity clocks on the wire, pin state and the PR rule with its grace are
removed; one owner validates the 1-90 day range; events re-evaluate only their
thread; cold threads fold from the tail of their logs.
The hover Settle / Un-settle control is a ghost xsmall Button; the phone Undo
is a ghost button without a shortcut hint; phone Thread behavior lists every
project as a row that pushes its rules page, which lists its choices instead
of opening a dropdown. Settled rows show and sort by settled_at, then
updated_at, and the Failed chip reads SessionActivity.failed.
…ules page

A clicked hover button stays hidden once its row moves (focus-visible, not
focus, reveals it); the project rules dialog uses the settings dropdown
trigger and menu rows; the phone rules page is titled with the project name,
which fits the nav bar where the dialog's sentence does not.
@Tryanks
Tryanks marked this pull request as ready for review October 8, 2026 13:43
@Tryanks
Tryanks merged commit 133eeef into main Oct 8, 2026
7 checks passed
@Tryanks
Tryanks deleted the feat/settled-state branch October 8, 2026 13:43
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.

Thread lifecycle: Settled as a real state, automatic settlement, idle auto-archive removed (B1)

1 participant