Repository navigation
Description
Research for removing the Settled task state, so a task has exactly two states: active and archived (deleted stays a separate, terminal operation that removes the row).
Findings below are from origin/master @ a9007473. No code was changed.
1. What "Settled" is today
A task's state is a three-value ladder, stored in one column and passed through two Rust enums:
| Layer | Anchor |
|---|---|
| SQLite column + CHECK | apps/maple-agent/crates/maple-agent/src/store/schema.rs:105 — state TEXT NOT NULL CHECK (state IN ('active','settled','archived')) |
| Store enum | store/tasks.rs:41 TaskState { Active, Settled, Archived } (as_str, read_task at :1465) |
| Service enum (serialized to the app) | agent/types.rs:839 AgentTaskState — #[serde(rename_all = "camelCase")], so the wire values are `"active" |
| Deleted | not a state. tasks.deleted_ms starts deletion and the collector removes the row (schema.rs comment; store/gc.rs) |
Behaviour attached to settled specifically:
- A run wakes it. Starting any run on a settled task flips it to active first (
agent/runs.rs:1445wake(), called from run admission). So "settled" means "no run since I last put it away". - Running tasks refuse to leave Active.
set_session_statefunnels non-active moves throughmaintenance_while_idleand reports "Stop the running agent before changing this task's state" (agent/tasks.rs:638). - Sidebar has a
Settledsection and its own page. Active and settled are two independent keyset pages per project:TaskSection::State(Active)andTaskSection::State(Settled)(store/paging/navigation.rs:459-495), each with its own cursor (NavigationProjectGroup.more={active, settled}). - Status grouping (
SidebarGroup::Status) exists only to split Active from Settled (app/src/ui/chat/sidebar.rs:1770-1775). - Hover "step" ladder: active → Settle → settled → Archive → archived → Delete (
sidebar.rs:417-475).
So "removing Settled" is not a one-line enum delete: it collapses a two-page, two-cursor, two-section, two-group model into one, and it changes a persisted SQL constraint and two versioned wire protocols.
2. Impact by layer
Reference count of typed Settled references (enum variants, section/page kinds, wire strings): ~134 across 17 files, in apps/maple-agent. Highest density is the sidebar paging/collection code, not the store.
A. Data model and storage
store/tasks.rs:TaskState::Settled,SerializedTaskSection::Settled(:137-162, this is the plain-string wire shape{"section":"settled"}),read_task(:1467),project_groupsSQLt.state IN ('active','settled')(:1067— projects must still group while their only tasks were settled).store/schema.rs: the table CHECK and the four partial indexes that filter on state. Migration chain is append-only (VERSION = 5,MIGRATIONSat:309);validate_versionreplays migrations 1..=N as the reference schema (:420) and diffs real schema rows, so any migration must be deterministic.- Migration decision (the main technical fork). SQLite cannot drop a value from a
CHECKin place:- (recommended, low risk) migration 6 =
UPDATE tasks SET state='active' WHERE state='settled', leave the column CHECK permissive. Storage tolerates a value the code never writes. No table rebuild, no FK churn. - (strict) migration 6 rebuilds
tasks(new table, copy with identicalids, drop, rename) and recreates 7 indexes. Six child tables referencetasks(id)withON DELETE CASCADE(schema.rs:151,167,188,214,221,271), so this needs foreign-key-safe ordering and a full verification pass. Only worth it if we want the constraint to be exact.
- (recommended, low risk) migration 6 =
store/integrity_constraints.rs:88asserts the literal constraint textstate IN ('active','settled','archived')— unchanged under the permissive option, must be updated under the strict one.crates/store-bench/src/run.rs:416seeds a third of its tasks as settled (bench distribution assumes three states).
B. Service / runtime API
agent/types.rs: drop the variant, andAgentTaskState::gerund()(:855) loses its"settling"arm.agent/store.rs:540,562: bidirectional store↔service mapping.agent/surface.rs:337: parses the task state string ("settled"), errors on unknown values — a legacy"settled"row would break surface attach unless it maps to Active.agent/runs.rs:1445:wake()becomes dead code and should be deleted, along with its test.agent/tasks.rs:638: doc + guard; with two states the guard is just "archived needs idle".acp/mod.rs:500: ACPdelete_sessionarchives — unaffected, but ACP test fixtures reference the enum (acp/tests.rs).
C. Wire formats — this is the release risk
TaskSection and the state string cross two versioned protocols, both with hard version refusal:
- app ↔ local host:
app/src/host/protocol.rsVERSION = 3;NavigationTaskQuerytravels as JSON params (app/src/backend/paging.rs:360,425). - app ↔ remote execution computer:
app/src/remote/wire.rsVERSION = 7, minimum 7, older peers get an explicit refusal (docs/remote-access.md:231).
Removing the "settled" variant means an old peer's {"section":"settled"} fails to deserialize ("Invalid host paging parameters") and a new app talking to an old remote (or the reverse) breaks paging. Options: bump both versions in the same release, or keep a transitional "settled" => active alias in deserialization for one release. Paging cursors embed the section in their scope and fail closed (store/paging/navigation.rs:1012); paging state is in-memory only, so a relaunch clears them — not a data migration problem.
NavigationProjectGroup.more = ProjectTaskCursors { active, settled } is also wire shape and must collapse to a single cursor.
D. Desktop UI
sidebar.rs:SidebarSection::Settled(:492-527, label +section-toggle-settledid),TaskRung::Settled/TaskMove::Settled(:417-475),menu_settle_id+settle-task-{id}element id (:283,320), menu composition "Settle task" (:2619),TaskAction::SetStatemapping (:2704,2736),settled_rows()/settled_expanded, section assembly partition (:1770),session_is_active()(:1823— becomes always-true and should go), page/LoadMorewiring (:1884-1892), plus two visual tests (:4799,4830).sidebar_paging.rs:PageKind::Settled(40 refs) — page ownership,ready/more/idssets,apply_live_summarytarget mapping (:1161),load_pagesection mapping (:1274), and the page-label table (:812).sidebar_prefix.rs:101: the local prefix keeps two streams (Active,Settled); becomes one.sidebar_ordering.rs:371:TaskGroup::Status(state == Settled && !running)— the status grouping's second bucket.sidebar_computers.rs(~20 refs): per-computer paging mirrors the same split —ComputerPageKind::Tasks { settled: bool },Project { root, settled },next_active/next_settled, concurrent dual queries (:232-282), andstate == "settled"bucketing (:427).sidebar_catalog.rs:48: comment/logic for "independently read active/settled prefixes are combined".computers.rs:38,138,dialogs.rs:421(doc),mod.rs:1108-1160test helpers (sidebar_settled,settle_task,unsettle_task),backend.rs:258/backend/local.rs:2441(doc comments onset_session_state),backend/paging.rs:132(string→enum mapping; its_ => Activefallback already gives graceful legacy handling).
Product questions the UI raises (see §4).
E. Tests to rewrite (not just recompile)
app/src/ui/chat/tests.rs — several tests are the settle behaviour: sections split pinned/active/settled, "settling a task moves it out of the inbox", "state records move a task back and forth", "a run shows a settled task active", ladder/menu/step assertions.
sidebar_paging.rs has ~40 in-file tests asserting Settled page membership and cursors.
store/tasks_tests.rs, store/paging/navigation_tests.rs, agent/tests.rs (a_run_wakes_a_settled_task_and_a_started_task_switches_models, also named in docs/scenario-checklist.md C7), agent/store/tests.rs.
Note test fixtures set SidebarGroup::Status by default (sidebar.rs:1498), so many sidebar tests implicitly depend on the Active/Settled split and will need re-baselining.
F. Docs
Only apps/maple-agent/docs/scenario-checklist.md:34 (C7) names the settled behaviour via a test name. docs/local-store.md does not document state values.
3. Explicitly not in scope (name collisions)
Do not touch these; they are unrelated uses of "settle":
crates/pi-coding-agent:AgentSettled,AgentSessionEvent::Settled,AgentBeforeSettle— run/turn lifecycle hooks.agent/timeline.rssettle_turn_permission_items,INTERRUPT_SETTLE_TIMEOUT,popup.settle(),maintenance_tests::settle()(test helper that pumps the UI),Promise.allSettledin Maple Research.
4. Decisions needed before implementation
- Where do existing settled tasks go on upgrade? Recommended: active (visible in the inbox; the user can archive). Archiving would silently hide work that was merely "put away". Nothing else in the app implies a different answer — a settled task is already listed and openable.
- Does the sidebar keep a section split at all? After the change the unarchived list is one bucket. Proposal: keep
Pinned+ the per-project/connection groups + theArchivedfold, and drop theActive/Settledsection labels together. This is the largest UX change and the one to confirm first. - What happens to the "Status" sidebar grouping (
SidebarGroup::Status)? Its only two buckets were Active and Settled, so it degenerates. Options: remove the option (persisted"status"already falls back toProjectvia#[serde(other)]), or keep it as "Active vs Archived". - Hover step button on an active task. Today it is "Settle" (check icon). It would become "Archive" (archive icon), with Delete appearing only on archived rows. Confirm that two rungs (active → archived → delete) is the intended ladder.
- Protocol strategy: single release with both wire versions bumped (host v3, remote v7), or one release of
"settled" => activealias tolerance first.
5. Suggested implementation order
- Store: drop
TaskState::Settled; one-section queries;project_groupsusesstate='active'; migration 6 normalizes rows; updateintegrity_constraints+ bench. - Service: drop
AgentTaskState::Settled, deletewake(), map legacy"settled"→ Active insurface.rs/backend/paging.rs/sidebar.rs:141. - Paging/navigation: single cursor per project group; delete the second stream and the
Tasks { settled }/Project { root, settled }page kinds; keep cursors failing closed. - UI: one unarchived section, menu/step ladder,
PageKind::Settledremoval,session_is_activeremoval, unread-marker comment. - Wire: bump
host::protocol::VERSIONandremote::wire::VERSION, updatedocs/remote-access.md. - Tests + docs: rewrite the settle-specific tests, update scenario checklist C7, run
just ciinapps/maple-agent.
Rough size: one focused PR is feasible but large — the store+service+wire half is mechanical once (1) is decided (≈1 day), and the sidebar/paging half is the risky part because it is where settled is load-bearing (≈2-3 days including tests). Choosing the permissive CHECK removes the only genuinely fiddly migration.
6. Notes
- Validation must be Local lane: backend/store change, schema migration, and app↔host protocol change. Needs: fresh-store open, upgrade-open from a store with settled rows, and a paired-remote paging check.
- Prior art: PR Agent: sink settled tasks within their project #1456 (closed) — "Agent: sink settled tasks within their project" — reworked settled ordering inside the project view (
sidebar.rs,sidebar_paging.rs, tests). That diff is exactly what this change deletes, so it must not be revived as-is.
Activity
marksftw commentedon Nov 10, 2026
I have an agent taking a stab at this.