feat(threads): persist manual active thread order#9729

merged4
main
sidebar/active-order
openedSep 4, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago

Active threads always use their default order, so clients cannot save a manual arrangement. This adds a capability-gated thread.active.reorder command and persisted fractional order keys, using existing thread metadata events for compatibility with older clients. Reordering leaves activity timestamps unchanged; settling clears the saved active position, while pinning and snoozing preserve it.

Reordering reserves retained keys from hidden rows without writing to those rows, including when a keyless section first receives an arrangement.

Verified with 218 focused server, contract, and client-runtime tests covering command decisions, projection migration and replay, snapshots, key allocation, and ordering. This layer has no visual changes; the dependent client PRs contain the UI evidence.

Prepared with Codex.


Note

Medium Risk Touches orchestration command handling, event projection, and a DB migration on core thread list state; behavior is heavily tested but incorrect replay or invariant gaps could mis-order threads across clients.

Overview Adds manual ordering for active (non-pinned) threads behind a new threadActiveReorder server capability and a thread.active.reorder command that writes fractional activeOrderKey values via the existing thread.meta-updated event (older clients can ignore the new field).

Server/orchestration: The decider rejects reorder on archived, deleted, pinned, or settled threads; reorder does not advance thread activity timestamps. Settling clears activeOrderKey; pin/snooze/metadata upserts preserve it. Projection pipeline, snapshots, and SQLite migration 048 add active_order_key with replay tests so lifecycle upserts do not drop saved placement.

Client-runtime: Exposes reorderActiveThread, shell/detail merge for activeOrderKey/unsettledAt, reducer/projector parity, sortActiveThreadsByOrderKey (new/reopened threads ahead of keyed order), and wider generateSpreadPinOrderKeys for large active lists reusing the pinned reorder planner.

Reviewed by Cursor Bugbot for commit 3c0b9d93d3c9c50933d305c5643f70dda727fcbe. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Persist manual active thread order across client, server, and projection

  • Adds ThreadActiveReorderCommand to the client and orchestration command unions, carrying a thread ID and a trimmed non-empty order key. The server decider emits a thread.meta-updated event with the key while preserving the thread's existing activity timestamp; deleted, pinned, and settled threads are rejected.
  • Adds an optional nullable activeOrderKey to OrchestrationThread, OrchestrationThreadShell, and ThreadMetaUpdatedPayload. The projector and applyThreadDetailEvent initialize it to null on creation, clear it on thread.settled, and apply it only when present in thread.meta-updated.
  • Client runtime gains reorderActiveThread in commands.ts and a serialized reorderActive environment command. sortActiveThreadsByOrderKey places keyless threads before keyed ones and applies deterministic thread/environment tie-breakers.
  • generateSpreadPinOrderKeys now widens the key space for larger lists. planPinnedReorder skips order keys held by hidden rows when inserting or materializing sections.
  • Migration 048 adds the active_order_key column to projected threads; the projection repository upserts and reads it. ServerEnvironment advertises threadActiveReorder in its capability descriptor.
  • Risk: mergeEnvironmentThread now takes activeOrderKey and unsettledAt from shell metadata instead of retaining cached detail values. Existing readers that relied on stale detail values for those fields will see shell-authoritative values.

Macroscope summarized 1052255.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.6 KiB13.6 KiB−31 B (−0.2%)15.1 KiB✅
CodexThread snapshot wire7.0 KiB7.0 KiB+9 B (+0.1%)7.3 KiB✅
CodexLive turn WebSocket wire6.6 KiB6.5 KiB−40 B (−0.6%)7.8 KiB✅
CodexLive turn WebSocket decoded57.1 KiB57.0 KiB−88 B (−0.2%)66.4 KiB✅
CodexLive turn messages108−2 (−20.0%)21✅
ClaudeTotal thread wire13.6 KiB13.6 KiB+7 B (+0.1%)15.1 KiB✅
ClaudeThread snapshot wire7.0 KiB7.1 KiB+2 B (+0.0%)7.3 KiB✅
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+5 B (+0.1%)7.8 KiB✅
ClaudeLive turn WebSocket decoded57.9 KiB57.9 KiB0 B (0.0%)66.4 KiB✅
ClaudeLive turn messages10100 (0.0%)21✅

Baseline: 29d03ec · PR result: d8f4d86 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 113.9 KiB
  • Claude decoded thread snapshot: 114.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change introduces persistent manual active-thread ordering across the client, orchestration protocol, server projections, and database schema, with new user-visible list behavior. The capability is enabled by default and the cross-layer state and migration changes exceed a small, self-contained addition.

Not approved because:

  • Monthly spending limit reached (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.

Added 4 commits

  • feat(threads): persist manual active thread order8566f00
  • fix(threads): reserve hidden thread order keysaa5d68a
  • test(server): expect active order on created threadsaae92da
  • test(server): combine snapshot assertions after rebased8f4d86